This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new 5411a642 [FFI] Back Optional and Variant by TVMFFIAny for a stable ABI
layout (#657)
5411a642 is described below
commit 5411a642d98bba14d4cb1e19a793c5b6758be019
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Jul 6 10:11:18 2026 +0800
[FFI] Back Optional and Variant by TVMFFIAny for a stable ABI layout (#657)
This PR makes `tvm::ffi::Optional<T>` and `tvm::ffi::Variant<...>`
layout stable across the contained type `T` by backing them with `Any`.
Note that `Optional<T>` still falls back to `std::optional<T>` when `T`
does not support Any storage.
This helps stabilize the future object-layout ABI of `Optional` and
`Variant`.
---
include/tvm/ffi/container/variant.h | 119 +++------
include/tvm/ffi/extra/json.h | 2 +-
include/tvm/ffi/extra/module.h | 2 +-
include/tvm/ffi/object.h | 5 +-
include/tvm/ffi/optional.h | 426 +++++++++++++------------------
include/tvm/ffi/reflection/access_path.h | 8 +-
rust/tvm-ffi/src/lib.rs | 3 +-
rust/tvm-ffi/src/option.rs | 350 -------------------------
rust/tvm-ffi/src/optional.rs | 200 +++++++++++++++
rust/tvm-ffi/src/string.rs | 17 --
rust/tvm-ffi/tests/test_optional.rs | 209 ++++++++-------
src/ffi/extra/json_writer.cc | 7 +-
tests/cpp/test_object.cc | 8 +-
tests/cpp/test_optional.cc | 111 ++++----
tests/cpp/test_variant.cc | 8 +-
15 files changed, 613 insertions(+), 862 deletions(-)
diff --git a/include/tvm/ffi/container/variant.h
b/include/tvm/ffi/container/variant.h
index 8ad8097f..2a027f56 100644
--- a/include/tvm/ffi/container/variant.h
+++ b/include/tvm/ffi/container/variant.h
@@ -34,77 +34,18 @@
namespace tvm {
namespace ffi {
-namespace details {
-/*!
- * \brief Base class for Variant.
- *
- * \tparam all_storage_object Whether all types are derived from ObjectRef.
- */
-template <bool all_storage_object = false>
-class VariantBase {
- public:
- TVM_FFI_INLINE bool same_as(const VariantBase<all_storage_object>& other)
const {
- return data_.same_as(other.data_);
- }
-
- protected:
- template <typename T>
- explicit VariantBase(T other) : data_(std::move(other)) {}
-
- TVM_FFI_INLINE void SetData(Any other_data) { data_ = std::move(other_data);
}
-
- TVM_FFI_INLINE Any MoveToAny() && { return std::move(data_); }
-
- TVM_FFI_INLINE AnyView ToAnyView() const { return data_.operator AnyView(); }
-
- Any data_;
-};
-
-// Specialization for all object ref case, backed by ObjectRef.
-template <>
-class VariantBase<true> : public ObjectRef {
- protected:
- template <typename T>
- explicit VariantBase(const T& other) : ObjectRef(other) {}
- template <typename T,
- typename = std::enable_if_t<!std::is_same_v<std::decay_t<T>,
VariantBase<true>>>>
- explicit VariantBase(T&& other) : ObjectRef(std::forward<T>(other)) {}
- explicit VariantBase(UnsafeInit tag) : ObjectRef(tag) {}
- explicit VariantBase(Any other)
- :
ObjectRef(details::AnyUnsafe::MoveFromAnyAfterCheck<ObjectRef>(std::move(other)))
{}
-
- TVM_FFI_INLINE void SetData(ObjectPtr<Object> other) { data_ =
std::move(other); }
-
- TVM_FFI_INLINE Any MoveToAny() && { return Any(ObjectRef(std::move(data_)));
}
-
- TVM_FFI_INLINE AnyView ToAnyView() const {
- TVMFFIAny any_data;
- if (data_ == nullptr) {
- any_data.type_index = TypeIndex::kTVMFFINone;
- any_data.zero_padding = 0;
- any_data.v_int64 = 0;
- } else {
- TVM_FFI_CLEAR_PTR_PADDING_IN_FFI_ANY(&any_data);
- any_data.type_index = data_->type_index();
- any_data.zero_padding = 0;
- any_data.v_obj =
details::ObjectUnsafe::TVMFFIObjectPtrFromObjectPtr<Object>(data_);
- }
- return AnyView::CopyFromTVMFFIAny(any_data);
- }
-};
-} // namespace details
-
/*!
* \brief A typed variant container.
*
- * When all values are ObjectRef, Variant is backed by ObjectRef,
- * otherwise it is backed by Any.
+ * \note Variant is always backed by a single Any (TVMFFIAny). Even when every
+ * alternative derives from ObjectRef, Variant is not ObjectRef-derived;
+ * this keeps the layout independent of the contained types
+ * (sizeof(Variant<...>) == sizeof(Any)).
*/
template <typename... V>
-class Variant : public details::VariantBase<details::all_object_ref_v<V...>> {
+class Variant {
public:
/// \cond Doxygen_Suppress
- using TParent = details::VariantBase<details::all_object_ref_v<V...>>;
static_assert(details::all_storage_enabled_v<V...>,
"All types used in Variant<...> must be compatible with Any");
/// \cond Doxygen_Suppress
@@ -123,37 +64,31 @@ class Variant : public
details::VariantBase<details::all_object_ref_v<V...>> {
* \brief Constructor from another variant
* \param other The other variant
*/
- Variant(const Variant<V...>& other) : TParent(other.data_) {}
+ Variant(const Variant<V...>& other) = default;
/*!
* \brief Constructor from another variant
* \param other The other variant
*/
- Variant(Variant<V...>&& other) noexcept : TParent(std::move(other.data_)) {}
+ Variant(Variant<V...>&& other) noexcept = default;
/*!
* \brief Assignment from another variant
* \param other The other variant
*/
- TVM_FFI_INLINE Variant& operator=(const Variant<V...>& other) {
- this->SetData(other.data_);
- return *this;
- }
+ Variant& operator=(const Variant<V...>& other) = default;
/*!
* \brief Assignment from another variant
* \param other The other variant
*/
- TVM_FFI_INLINE Variant& operator=(Variant<V...>&& other) noexcept {
- this->SetData(std::move(other.data_));
- return *this;
- }
+ Variant& operator=(Variant<V...>&& other) noexcept = default;
/*!
- * \brief Constructor from another variant
- * \param other The other variant
+ * \brief Constructor from a contained value
+ * \param other The value to store
*/
template <typename T, typename = enable_if_variant_contains_t<T>>
- Variant(T other) : TParent(std::move(other)) {} // NOLINT(*)
+ Variant(T other) : data_(std::move(other)) {} // NOLINT(*)
/*!
* \brief Assignment from another variant
@@ -171,7 +106,7 @@ class Variant : public
details::VariantBase<details::all_object_ref_v<V...>> {
*/
template <typename T, typename = enable_if_variant_contains_t<T>>
TVM_FFI_INLINE std::optional<T> as() const {
- return this->TParent::ToAnyView().template as<T>();
+ return ToAnyView().template as<T>();
}
/*!
@@ -182,7 +117,7 @@ class Variant : public
details::VariantBase<details::all_object_ref_v<V...>> {
*/
template <typename T, typename = std::enable_if_t<std::is_base_of_v<Object,
T>>>
TVM_FFI_INLINE const T* as() const {
- return this->TParent::ToAnyView().template as<const
T*>().value_or(nullptr);
+ return ToAnyView().template as<const T*>().value_or(nullptr);
}
/*!
@@ -192,7 +127,7 @@ class Variant : public
details::VariantBase<details::all_object_ref_v<V...>> {
*/
template <typename T, typename = enable_if_variant_contains_t<T>>
TVM_FFI_INLINE T get() const& {
- return this->TParent::ToAnyView().template cast<T>();
+ return ToAnyView().template cast<T>();
}
/*!
@@ -202,21 +137,30 @@ class Variant : public
details::VariantBase<details::all_object_ref_v<V...>> {
*/
template <typename T, typename = enable_if_variant_contains_t<T>>
TVM_FFI_INLINE T get() && {
- return std::move(*this).TParent::MoveToAny().template cast<T>();
+ return std::move(*this).MoveToAny().template cast<T>();
}
/*!
* \brief Get the type key of the variant
* \return The type key of the variant
*/
- TVM_FFI_INLINE std::string GetTypeKey() const { return
this->TParent::ToAnyView().GetTypeKey(); }
+ TVM_FFI_INLINE std::string GetTypeKey() const { return
ToAnyView().GetTypeKey(); }
+
+ /*!
+ * \brief Shallow-compare with another variant.
+ * \param other The other variant.
+ * \return Whether the two hold the same underlying value.
+ */
+ TVM_FFI_INLINE bool same_as(const Variant<V...>& other) const {
+ return data_.same_as(other.data_);
+ }
private:
friend struct TypeTraits<Variant<V...>>;
friend struct ObjectPtrHash;
friend struct ObjectPtrEqual;
// constructor from any
- explicit Variant(Any data) : TParent(std::move(data)) {}
+ explicit Variant(Any data) : data_(std::move(data)) {}
/*!
* \brief Get the object pointer from the variant
* \note This function is only available if all types used in Variant<...>
are derived from
@@ -227,11 +171,12 @@ class Variant : public
details::VariantBase<details::all_object_ref_v<V...>> {
static_assert(all_object_v,
"All types used in Variant<...> must be derived from
ObjectRef "
"to enable ObjectPtrHash/ObjectPtrEqual");
- return this->data_.get();
+ return details::AnyUnsafe::ObjectPtrFromAnyAfterCheck(this->data_);
}
- // rexpose to friend class
- using TParent::MoveToAny;
- using TParent::ToAnyView;
+ TVM_FFI_INLINE AnyView ToAnyView() const { return data_.operator AnyView(); }
+ TVM_FFI_INLINE Any MoveToAny() && { return std::move(data_); }
+ /*! \brief The underlying Any backing store. */
+ Any data_;
};
template <typename... V>
diff --git a/include/tvm/ffi/extra/json.h b/include/tvm/ffi/extra/json.h
index 24ab2f0d..e7bff00d 100644
--- a/include/tvm/ffi/extra/json.h
+++ b/include/tvm/ffi/extra/json.h
@@ -76,7 +76,7 @@ TVM_FFI_EXTRA_CXX_API json::Value Parse(const String&
json_str, String* error_ms
* \return The output JSON string.
*/
TVM_FFI_EXTRA_CXX_API String Stringify(const json::Value& value,
- Optional<int> indent = std::nullopt);
+ const Optional<int>& indent =
std::nullopt);
} // namespace json
} // namespace ffi
diff --git a/include/tvm/ffi/extra/module.h b/include/tvm/ffi/extra/module.h
index 5c2142ec..8a824a3e 100644
--- a/include/tvm/ffi/extra/module.h
+++ b/include/tvm/ffi/extra/module.h
@@ -70,7 +70,7 @@ class TVM_FFI_EXTRA_CXX_API ModuleObj : public Object {
* \param name The name of the function.
* \return True if the module implements the function, false otherwise.
*/
- virtual bool ImplementsFunction(const String& name) { return
GetFunction(name).defined(); }
+ virtual bool ImplementsFunction(const String& name) { return
GetFunction(name).has_value(); }
/*!
* \brief Get the docstring of the function, if available.
* \param name The name of the function.
diff --git a/include/tvm/ffi/object.h b/include/tvm/ffi/object.h
index 4868ab14..27bf8c38 100644
--- a/include/tvm/ffi/object.h
+++ b/include/tvm/ffi/object.h
@@ -670,8 +670,9 @@ class WeakObjectPtr {
* \brief Optional data type in FFI.
* \tparam T The underlying type of the optional.
*
- * \note Compared to std::optional, Optional<ObjectRef>
- * akes less storage as it used nullptr to represent nullopt.
+ * \note For storage-enabled T, Optional<T> is backed by a single TVMFFIAny
(Any)
+ * and uses kTVMFFINone to represent nullopt, so its layout is independent
of T.
+ * For non-storage-enabled T it falls back to std::optional<T>.
*/
template <typename T, typename = void>
class Optional;
diff --git a/include/tvm/ffi/optional.h b/include/tvm/ffi/optional.h
index eb63fafb..c79d5328 100644
--- a/include/tvm/ffi/optional.h
+++ b/include/tvm/ffi/optional.h
@@ -20,11 +20,17 @@
/*!
* \file tvm/ffi/optional.h
* \brief Runtime Optional container types.
- * \note Optional<T> specializes for T is ObjectRef and used nullptr to
indicate nullopt.
+ * \note Optional<T> uses a hybrid representation. For types that enable Any
+ * storage (`TypeTraits<T>::storage_enabled`), it is backed by a single
+ * TVMFFIAny (Any) with nullopt represented as kTVMFFINone, mirroring
+ * Variant<...>; the layout is then independent of T (sizeof ==
sizeof(Any))
+ * which keeps the ABI stable. For types that do not enable storage (e.g.
+ * non-owning view types) it falls back to std::optional<T>.
*/
#ifndef TVM_FFI_OPTIONAL_H_
#define TVM_FFI_OPTIONAL_H_
+#include <tvm/ffi/any.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/object.h>
#include <tvm/ffi/string.h>
@@ -44,59 +50,46 @@ inline constexpr bool is_optional_type_v = false;
template <typename T>
inline constexpr bool is_optional_type_v<Optional<T>> = true;
-
-// we can safely used ptr based optional for ObjectRef types
-// that do not have additional data members and virtual functions.
-template <typename T>
-inline constexpr bool use_ptr_based_optional_v =
- (std::is_base_of_v<ObjectRef, T> && !is_optional_type_v<T>);
/// \endcond
-// Specialization for non-ObjectRef types.
-// simply fallback to std::optional
+// Fallback specialization for types that do NOT enable Any storage
+// (`TypeTraits<T>::storage_enabled == false`), such as non-owning view types
+// that cannot be moved into an Any. These simply reuse std::optional<T>.
template <typename T>
-class Optional<T, std::enable_if_t<!use_ptr_based_optional_v<T> &&
!std::is_same_v<T, String> &&
- !std::is_same_v<T, Bytes>>> {
+class Optional<T, std::enable_if_t<!TypeTraits<T>::storage_enabled>> {
public:
// default constructors.
Optional() = default;
// NOLINTBEGIN(google-explicit-constructor)
- Optional(const Optional<T>& other) : data_(other.data_) {}
- Optional(Optional<T>&& other) noexcept : data_(std::move(other.data_)) {}
+ Optional(const Optional& other) = default;
+ Optional(Optional&& other) noexcept = default;
Optional(std::optional<T> other) : data_(std::move(other)) {}
Optional(std::nullopt_t) {}
Optional(T other) : data_(std::move(other)) {}
// NOLINTEND(google-explicit-constructor)
- TVM_FFI_INLINE Optional<T>& operator=(const Optional<T>& other) {
- data_ = other.data_;
- return *this;
- }
-
- TVM_FFI_INLINE Optional<T>& operator=(Optional<T>&& other) noexcept {
- data_ = std::move(other.data_);
- return *this;
- }
+ Optional& operator=(const Optional& other) = default;
+ Optional& operator=(Optional&& other) noexcept = default;
- TVM_FFI_INLINE Optional<T>& operator=(T other) {
+ TVM_FFI_INLINE Optional& operator=(T other) {
data_ = std::move(other);
return *this;
}
- TVM_FFI_INLINE Optional<T>& operator=(std::nullopt_t) {
+ TVM_FFI_INLINE Optional& operator=(std::nullopt_t) {
data_ = std::nullopt;
return *this;
}
TVM_FFI_INLINE const T& value() const& {
- if (!data_.has_value()) {
+ if (TVM_FFI_PREDICT_FALSE(!data_.has_value())) {
TVM_FFI_THROW(RuntimeError) << "Back optional access";
}
return *data_;
}
TVM_FFI_INLINE T&& value() && {
- if (!data_.has_value()) {
+ if (TVM_FFI_PREDICT_FALSE(!data_.has_value())) {
TVM_FFI_THROW(RuntimeError) << "Back optional access";
}
return *std::move(data_);
@@ -111,10 +104,8 @@ class Optional<T,
std::enable_if_t<!use_ptr_based_optional_v<T> && !std::is_same
TVM_FFI_INLINE bool has_value() const noexcept { return data_.has_value(); }
- TVM_FFI_INLINE bool operator==(const Optional<T>& other) const { return
data_ == other.data_; }
-
- TVM_FFI_INLINE bool operator!=(const Optional<T>& other) const { return
data_ != other.data_; }
-
+ TVM_FFI_INLINE bool operator==(const Optional& other) const { return data_
== other.data_; }
+ TVM_FFI_INLINE bool operator!=(const Optional& other) const { return data_
!= other.data_; }
template <typename U>
TVM_FFI_INLINE bool operator==(const U& other) const {
return data_ == other;
@@ -127,14 +118,12 @@ class Optional<T,
std::enable_if_t<!use_ptr_based_optional_v<T> && !std::is_same
// NOLINTBEGIN(bugprone-unchecked-optional-access)
/*!
* \brief Direct access to the value.
- * \return the xvalue reference to the stored value.
* \note only use this function after checking has_value()
*/
TVM_FFI_INLINE T&& operator*() && noexcept { return *std::move(data_); }
/*!
* \brief Direct access to the value.
- * \return the const reference to the stored value.
- * \note only use this function after checking has_value()
+ * \note only use this function after checking has_value()
*/
TVM_FFI_INLINE const T& operator*() const& noexcept { return *data_; }
// NOLINTEND(bugprone-unchecked-optional-access)
@@ -143,276 +132,203 @@ class Optional<T,
std::enable_if_t<!use_ptr_based_optional_v<T> && !std::is_same
std::optional<T> data_;
};
-// Specialization for String type, use nullptr to indicate nullopt
-template <typename T>
-class Optional<T, std::enable_if_t<std::is_same_v<T, String> ||
std::is_same_v<T, Bytes>>> {
- public:
- // default constructors.
- Optional() = default;
- // NOLINTBEGIN(google-explicit-constructor)
- Optional(const Optional<T>& other) : data_(other.data_) {}
- Optional(Optional<T>&& other) : data_(std::move(other.data_)) {}
- Optional(std::nullopt_t) {}
- Optional(T other) : data_(std::move(other)) {}
- // NOLINTEND(google-explicit-constructor)
-
- TVM_FFI_INLINE Optional<T>& operator=(const Optional<T>& other) {
- data_ = other.data_;
- return *this;
- }
-
- TVM_FFI_INLINE Optional<T>& operator=(Optional<T>&& other) {
- data_ = std::move(other.data_);
- return *this;
- }
-
- TVM_FFI_INLINE Optional<T>& operator=(T other) {
- data_ = std::move(other);
- return *this;
- }
-
- TVM_FFI_INLINE Optional<T>& operator=(std::nullopt_t) {
- T(details::BytesBaseCell(std::nullopt)).swap(data_);
- return *this;
- }
-
- TVM_FFI_INLINE const T& value() const& {
- if (data_.data_ == std::nullopt) {
- TVM_FFI_THROW(RuntimeError) << "Back optional access";
- }
- return data_;
- }
-
- TVM_FFI_INLINE String&& value() && {
- if (data_.data_ == std::nullopt) {
- TVM_FFI_THROW(RuntimeError) << "Back optional access";
- }
- return std::move(data_);
- }
-
- template <typename U = T>
- TVM_FFI_INLINE T value_or(U&& default_value) const {
- if (data_.data_ == std::nullopt) {
- return std::forward<U>(default_value);
- }
- return data_;
- }
-
- TVM_FFI_INLINE explicit operator bool() const noexcept { return data_.data_
!= std::nullopt; }
-
- TVM_FFI_INLINE bool has_value() const noexcept { return data_.data_ !=
std::nullopt; }
-
- TVM_FFI_INLINE bool operator==(const Optional<T>& other) const {
- if (data_.data_ == std::nullopt) {
- return other.data_.data_ == std::nullopt;
- }
- if (other.data_.data_ == std::nullopt) {
- return false;
- }
- return data_ == other.data_;
- }
-
- TVM_FFI_INLINE bool operator!=(const Optional<T>& other) const { return
!(*this == other); }
-
- template <typename U>
- TVM_FFI_INLINE bool operator==(const U& other) const {
- if constexpr (std::is_same_v<U, std::nullopt_t>) {
- return data_.data_ == std::nullopt;
- } else {
- if (data_.data_ == std::nullopt) {
- return false;
- }
- return data_ == other;
- }
- }
- template <typename U>
- TVM_FFI_INLINE bool operator!=(const U& other) const {
- if constexpr (std::is_same_v<U, std::nullopt_t>) {
- return data_.data_ != std::nullopt;
- } else {
- if (data_.data_ == std::nullopt) {
- return true;
- }
- return data_ != other;
- }
- }
-
- /*!
- * \brief Direct access to the value.
- * \return the xvalue reference to the stored value.
- * \note only use this function after checking has_value()
- */
- TVM_FFI_INLINE T&& operator*() && noexcept { return std::move(data_); }
- /*!
- * \brief Direct access to the value.
- * \return the const reference to the stored value.
- * \note only use this function after checking has_value()
- */
- TVM_FFI_INLINE const T& operator*() const& noexcept { return data_; }
-
- private:
- // this is a private initializer
- T data_{details::BytesBaseCell(std::nullopt)};
-};
-
-// Specialization for ObjectRef types.
-// nullptr is treated as std::nullopt.
+/*!
+ * \brief Optional container backed by a single TVMFFIAny (Any) for
storage-enabled T.
+ *
+ * Mirrors the Variant<...> implementation: the value is stored in a single Any
+ * and nullopt is represented as kTVMFFINone. The layout is therefore
independent
+ * of T (sizeof(Optional<T>) == sizeof(Any)) which keeps the ABI stable across
+ * contained types.
+ *
+ * \note None/null is treated as nullopt. For a nullable T, a null value reads
+ * back as "no value" (has_value() == false). This gives one stable
+ * cross-language ABI representation, consistent with Python's None. If a
+ * use case must distinguish nullopt from a stored null, use
std::optional
+ * instead.
+ *
+ * \tparam T The underlying value type (must enable Any storage).
+ */
template <typename T>
-class Optional<T, std::enable_if_t<use_ptr_based_optional_v<T>>> : public
ObjectRef {
+class Optional<T, std::enable_if_t<TypeTraits<T>::storage_enabled>> {
public:
- using ContainerType = typename T::ContainerType;
- static constexpr bool _type_container_is_exact = T::_type_container_is_exact;
+ /*! \brief default constructor, represents nullopt (Any() is kTVMFFINone). */
Optional() = default;
// NOLINTBEGIN(google-explicit-constructor)
- Optional(const Optional<T>& other) : ObjectRef(other) {}
- Optional(Optional<T>&& other) noexcept : ObjectRef(std::move(other)) {}
- explicit Optional(ffi::UnsafeInit tag) : ObjectRef(tag) {}
+ /*! \brief construct nullopt from std::nullopt. */
Optional(std::nullopt_t) {}
+ /*! \brief copy constructor. */
+ Optional(const Optional& other) = default;
+ /*! \brief move constructor. */
+ Optional(Optional&& other) noexcept = default;
+ /*! \brief construct from a value of type T (copy). */
+ Optional(const T& value) : data_(value) {}
+ /*! \brief construct from a value of type T (move). */
+ Optional(T&& value) : data_(std::move(value)) {}
+ /*! \brief construct from a std::optional<T>. */
Optional(std::optional<T> other) {
if (other.has_value()) {
- *this = *std::move(other);
+ data_ = Any(*std::move(other));
}
}
- Optional(T other) : ObjectRef(std::move(other)) {}
// NOLINTEND(google-explicit-constructor)
- TVM_FFI_INLINE Optional<T>& operator=(T other) {
- ObjectRef::operator=(std::move(other));
- return *this;
- }
+ /*! \brief copy assignment. */
+ Optional& operator=(const Optional& other) = default;
+ /*! \brief move assignment. */
+ Optional& operator=(Optional&& other) noexcept = default;
- TVM_FFI_INLINE Optional<T>& operator=(const Optional<T>& other) {
- data_ = other.data_;
+ TVM_FFI_INLINE Optional& operator=(T other) {
+ data_ = Any(std::move(other));
return *this;
}
- TVM_FFI_INLINE Optional<T>& operator=(std::nullptr_t) {
- data_ = nullptr;
+ TVM_FFI_INLINE Optional& operator=(std::nullopt_t) {
+ data_.reset();
return *this;
}
- TVM_FFI_INLINE Optional<T>& operator=(Optional<T>&& other) {
- data_ = std::move(other.data_);
+ TVM_FFI_INLINE Optional& operator=(std::nullptr_t) {
+ data_.reset();
return *this;
}
TVM_FFI_INLINE T value() const& {
- if (data_ == nullptr) {
+ if (TVM_FFI_PREDICT_FALSE(!has_value())) {
TVM_FFI_THROW(RuntimeError) << "Back optional access";
}
- return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_);
+ // The invariant guarantees the stored value is exactly a T, so decode it
+ // directly with the low-level after-check path (no conversion/cast).
+ return details::AnyUnsafe::CopyFromAnyViewAfterCheck<T>(data_);
}
TVM_FFI_INLINE T value() && {
- if (data_ == nullptr) {
+ if (TVM_FFI_PREDICT_FALSE(!has_value())) {
TVM_FFI_THROW(RuntimeError) << "Back optional access";
}
- return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(std::move(data_));
+ return details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
}
template <typename U = std::remove_cv_t<T>>
TVM_FFI_INLINE T value_or(U&& default_value) const {
- return data_ != nullptr ?
details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_)
- : T(std::forward<U>(default_value));
+ if (has_value()) {
+ return details::AnyUnsafe::CopyFromAnyViewAfterCheck<T>(data_);
+ }
+ return T(std::forward<U>(default_value));
}
- TVM_FFI_INLINE explicit operator bool() const { return data_ != nullptr; }
+ TVM_FFI_INLINE explicit operator bool() const noexcept { return has_value();
}
- TVM_FFI_INLINE bool has_value() const { return data_ != nullptr; }
+ TVM_FFI_INLINE bool has_value() const noexcept {
+ return data_.type_index() != TypeIndex::kTVMFFINone;
+ }
/*!
- * \brief Direct access to the value.
- * \return the const reference to the stored value.
- * \note only use this function after checking has_value()
+ * \brief Try to reinterpret the stored value as a type U (strict, no
conversion).
+ * \tparam U The type to reinterpret to.
+ * \return std::optional<U> for ObjectRef-like U, or const U* when U is an
Object type.
+ * \note Returns an empty result when nullopt or the strict type check fails.
*/
- TVM_FFI_INLINE T operator*() const& noexcept {
- return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_);
+ template <typename U>
+ TVM_FFI_INLINE auto as() const {
+ return data_.template as<U>();
}
/*!
* \brief Direct access to the value.
- * \return the const reference to the stored value.
- * \note only use this function after checking has_value()
+ * \note only use this function after checking has_value()
+ */
+ TVM_FFI_INLINE T operator*() const& {
+ return details::AnyUnsafe::CopyFromAnyViewAfterCheck<T>(data_);
+ }
+ /*!
+ * \brief Direct access to the value, moved out of the storage.
+ * \note only use this function after checking has_value()
*/
- TVM_FFI_INLINE T operator*() && noexcept {
- return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(std::move(data_));
+ TVM_FFI_INLINE T operator*() && {
+ return details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
}
+ // comparison with nullopt / nullptr
+ TVM_FFI_INLINE bool operator==(std::nullopt_t) const noexcept { return
!has_value(); }
+ TVM_FFI_INLINE bool operator!=(std::nullopt_t) const noexcept { return
has_value(); }
TVM_FFI_INLINE bool operator==(std::nullptr_t) const noexcept { return
!has_value(); }
TVM_FFI_INLINE bool operator!=(std::nullptr_t) const noexcept { return
has_value(); }
- // operator overloadings
- TVM_FFI_INLINE auto operator==(const Optional<T>& other) const {
+ // comparison with another Optional<T>
+ TVM_FFI_INLINE auto operator==(const Optional& other) const {
// support case where sub-class returns a symbolic ref type.
- return EQToOptional(other);
+ using RetType = decltype(std::declval<T>() == std::declval<T>());
+ if (data_.same_as(other.data_)) return RetType(true);
+ if (has_value() && other.has_value()) return **this == *other;
+ return RetType(false);
+ }
+ TVM_FFI_INLINE auto operator!=(const Optional& other) const {
+ using RetType = decltype(std::declval<T>() != std::declval<T>());
+ if (data_.same_as(other.data_)) return RetType(false);
+ if (has_value() && other.has_value()) return **this != *other;
+ return RetType(true);
}
- TVM_FFI_INLINE auto operator!=(const Optional<T>& other) const { return
NEToOptional(other); }
+ // comparison with a std::optional<T>
TVM_FFI_INLINE auto operator==(const std::optional<T>& other) const {
- // support case where sub-class returns a symbolic ref type.
- return EQToOptional(other);
+ using RetType = decltype(std::declval<T>() == std::declval<T>());
+ if (has_value() && other.has_value()) return **this == *other;
+ return RetType(!has_value() && !other.has_value());
}
TVM_FFI_INLINE auto operator!=(const std::optional<T>& other) const {
- return NEToOptional(other);
- }
-
- TVM_FFI_INLINE auto operator==(const T& other) const {
- using RetType = decltype(value() == other);
- if (same_as(other)) return RetType(true);
- if (has_value()) return operator*() == other;
- return RetType(false);
+ using RetType = decltype(std::declval<T>() != std::declval<T>());
+ if (has_value() && other.has_value()) return **this != *other;
+ return RetType(has_value() != other.has_value());
}
- TVM_FFI_INLINE auto operator!=(const T& other) const { return !(*this ==
other); }
-
- template <typename U>
+ // comparison with a value of another type U
+ template <typename U, typename = std::enable_if_t<!is_optional_type_v<U> &&
+ !std::is_same_v<U,
std::nullopt_t> &&
+ !std::is_same_v<U,
std::nullptr_t>>>
TVM_FFI_INLINE auto operator==(const U& other) const {
- using RetType = decltype(value() == other);
+ using RetType = decltype(std::declval<T>() == std::declval<U>());
+ if constexpr (std::is_base_of_v<ObjectRef, T> &&
std::is_base_of_v<ObjectRef, U>) {
+ // support case where sub-class returns a symbolic ref type.
+ if (data_.same_as(other)) return RetType(true);
+ }
if (!has_value()) return RetType(false);
- return operator*() == other;
+ return **this == other;
}
-
- template <typename U>
+ template <typename U, typename = std::enable_if_t<!is_optional_type_v<U> &&
+ !std::is_same_v<U,
std::nullopt_t> &&
+ !std::is_same_v<U,
std::nullptr_t>>>
TVM_FFI_INLINE auto operator!=(const U& other) const {
- using RetType = decltype(value() != other);
+ using RetType = decltype(std::declval<T>() != std::declval<U>());
+ if constexpr (std::is_base_of_v<ObjectRef, T> &&
std::is_base_of_v<ObjectRef, U>) {
+ if (data_.same_as(other)) return RetType(false);
+ }
if (!has_value()) return RetType(true);
- return operator*() != other;
+ return **this != other;
}
/*!
- * \return The internal object pointer with container type of T.
- * \note This function do not perform not-null checking.
+ * \brief Shallow-compare with another Optional<T>.
+ * \return Whether the two refer to the same underlying value.
*/
- TVM_FFI_INLINE const ContainerType* get() const {
- return static_cast<ContainerType*>(data_.get());
- }
+ TVM_FFI_INLINE bool same_as(const Optional& other) const { return
data_.same_as(other.data_); }
- private:
- template <typename U>
- TVM_FFI_INLINE auto EQToOptional(const U& other) const {
- // support case where sub-class returns a symbolic ref type.
- using RetType = decltype(operator*() == *other);
- if (same_as(other)) return RetType(true);
- if (has_value() && other.has_value()) {
- return operator*() == *other;
- } else {
- // one of them is nullptr.
- return RetType(false);
- }
+ /*!
+ * \brief Shallow-compare with a value of an ObjectRef type.
+ * \return Whether the two refer to the same underlying object.
+ */
+ template <typename U = T, typename =
std::enable_if_t<std::is_base_of_v<ObjectRef, U>>>
+ TVM_FFI_INLINE bool same_as(const U& other) const {
+ return data_.same_as(other);
}
- template <typename U>
- TVM_FFI_INLINE auto NEToOptional(const U& other) const {
- // support case where sub-class returns a symbolic ref type.
- using RetType = decltype(operator*() != *other);
- if (same_as(other)) return RetType(false);
- if (has_value() && other.has_value()) {
- return operator*() != *other;
- } else {
- // one of them is nullptr.
- return RetType(true);
- }
- }
+ private:
+ friend struct TypeTraits<Optional<T>>;
+ // construct directly from an Any backing store.
+ explicit Optional(Any data) : data_(std::move(data)) {}
+ TVM_FFI_INLINE AnyView ToAnyView() const { return data_.operator AnyView(); }
+ TVM_FFI_INLINE Any MoveToAny() && { return std::move(data_); }
+ /*! \brief The underlying Any backing store, kTVMFFINone represents nullopt.
*/
+ Any data_;
};
template <typename T>
@@ -420,19 +336,33 @@ inline constexpr bool
use_default_type_traits_v<Optional<T>> = false;
template <typename T>
struct TypeTraits<Optional<T>> : public TypeTraitsBase {
+ // storage_enabled propagates from T: Optional<T> can live in an Any exactly
+ // when T can. This keeps nested Optional<Optional<T>> and Optional<T> used
+ // inside Variant<...>/containers Any-backed iff T is storage-enabled.
+ static constexpr bool storage_enabled = TypeTraits<T>::storage_enabled;
+
TVM_FFI_INLINE static void CopyToAnyView(const Optional<T>& src, TVMFFIAny*
result) {
- if (src.has_value()) {
- TypeTraits<T>::CopyToAnyView(*src, result);
+ if constexpr (TypeTraits<T>::storage_enabled) {
+ // Storage-enabled: the Any already holds the exact representation.
+ *result = src.ToAnyView().CopyToTVMFFIAny();
} else {
- TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+ if (src.has_value()) {
+ TypeTraits<T>::CopyToAnyView(*src, result);
+ } else {
+ TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+ }
}
}
TVM_FFI_INLINE static void MoveToAny(Optional<T> src, TVMFFIAny* result) {
- if (src.has_value()) {
- TypeTraits<T>::MoveToAny(*std::move(src), result);
+ if constexpr (TypeTraits<T>::storage_enabled) {
+ *result =
details::AnyUnsafe::MoveAnyToTVMFFIAny(std::move(src).MoveToAny());
} else {
- TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+ if (src.has_value()) {
+ TypeTraits<T>::MoveToAny(*std::move(src), result);
+ } else {
+ TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+ }
}
}
@@ -442,17 +372,21 @@ struct TypeTraits<Optional<T>> : public TypeTraitsBase {
}
TVM_FFI_INLINE static Optional<T> CopyFromAnyViewAfterCheck(const TVMFFIAny*
src) {
- if (src->type_index == TypeIndex::kTVMFFINone) {
- return Optional<T>(std::nullopt);
+ if constexpr (TypeTraits<T>::storage_enabled) {
+ return Optional<T>(Any(AnyView::CopyFromTVMFFIAny(*src)));
+ } else {
+ if (src->type_index == TypeIndex::kTVMFFINone) return
Optional<T>(std::nullopt);
+ return Optional<T>(TypeTraits<T>::CopyFromAnyViewAfterCheck(src));
}
- return TypeTraits<T>::CopyFromAnyViewAfterCheck(src);
}
TVM_FFI_INLINE static Optional<T> MoveFromAnyAfterCheck(TVMFFIAny* src) {
- if (src->type_index == TypeIndex::kTVMFFINone) {
- return Optional<T>(std::nullopt);
+ if constexpr (TypeTraits<T>::storage_enabled) {
+ return Optional<T>(details::AnyUnsafe::MoveTVMFFIAnyToAny(src));
+ } else {
+ if (src->type_index == TypeIndex::kTVMFFINone) return
Optional<T>(std::nullopt);
+ return Optional<T>(TypeTraits<T>::MoveFromAnyAfterCheck(src));
}
- return TypeTraits<T>::MoveFromAnyAfterCheck(src);
}
TVM_FFI_INLINE static std::optional<Optional<T>> TryCastFromAnyView(const
TVMFFIAny* src) {
diff --git a/include/tvm/ffi/reflection/access_path.h
b/include/tvm/ffi/reflection/access_path.h
index e4ebc2aa..74d106ce 100644
--- a/include/tvm/ffi/reflection/access_path.h
+++ b/include/tvm/ffi/reflection/access_path.h
@@ -305,8 +305,8 @@ class AccessPathObj : public Object {
if (!(*lhs->step)->StepEqual(*(rhs->step))) {
return false;
}
- lhs = static_cast<const AccessPathObj*>(lhs->parent.get());
- rhs = static_cast<const AccessPathObj*>(rhs->parent.get());
+ lhs = lhs->parent.as<AccessPathObj>();
+ rhs = rhs->parent.as<AccessPathObj>();
// fast path for same pointer
if (lhs == rhs) return true;
TVM_FFI_ICHECK(lhs != nullptr);
@@ -415,7 +415,7 @@ inline Array<AccessStep> AccessPathObj::ToSteps() const {
while (current->parent.has_value()) {
TVM_FFI_ICHECK(current->step.has_value());
reverse_steps.push_back(*(current->step));
- current = static_cast<const AccessPathObj*>(current->parent.get());
+ current = current->parent.as<AccessPathObj>();
TVM_FFI_ICHECK(current != nullptr);
}
return Array<AccessStep>(reverse_steps.rbegin(), reverse_steps.rend());
@@ -432,7 +432,7 @@ inline bool AccessPathObj::IsPrefixOf(const AccessPath&
other) const {
const AccessPathObj* rhs_path = other.get();
while (rhs_path->depth > this->depth) {
TVM_FFI_ICHECK(rhs_path->parent.has_value());
- rhs_path = static_cast<const AccessPathObj*>(rhs_path->parent.get());
+ rhs_path = rhs_path->parent.as<AccessPathObj>();
}
return PathEqual(this, rhs_path);
}
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index a05e3c06..72b8d160 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -27,7 +27,7 @@ pub mod function;
pub mod function_internal;
pub mod macros;
pub mod object;
-pub mod option;
+pub mod optional;
pub mod string;
pub mod type_traits;
pub use tvm_ffi_sys;
@@ -46,6 +46,7 @@ pub use crate::error::{
pub use crate::extra::module::Module;
pub use crate::function::Function;
pub use crate::object::{Object, ObjectArc, ObjectCore,
ObjectCoreWithExtraItems, ObjectRefCore};
+pub use crate::optional::Optional;
pub use crate::string::{Bytes, String};
pub use crate::type_traits::AnyCompatible;
diff --git a/rust/tvm-ffi/src/option.rs b/rust/tvm-ffi/src/option.rs
deleted file mode 100644
index d40e3ce9..00000000
--- a/rust/tvm-ffi/src/option.rs
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-//! In-place mirrors of C++ `ffi::Optional<T>` (`include/tvm/ffi/optional.h`).
-//!
-//! `ffi::Optional<T>` stores its value inline, in one of three ABI layouts
-//! depending on `T`. The types here decode such a field's bytes directly — no
-//! FFI call, no allocation, no reflection getter/setter. Pick the counterpart
for
-//! the field's `T`:
-//!
-//! - POD scalar (`i32`, `f64`, `bool`, …) → [`OptionPod<T>`](OptionPod)
-//! - `String` → [`OptionStr`]
-//! - `ObjectRef` subtype → [`OptionObjRef<T>`](OptionObjRef), an alias of
plain
-//! `Option<T>` (a single nullable pointer, `nullptr` == `None`)
-//!
-//! # `OptionPod<T>` — POD scalars
-//! Mirrors the `std::optional<T>` fallback as `#[repr(C)] { value: T, engaged:
-//! bool }` (payload at offset 0, flag at `size_of::<T>()`), byte-verified
against
-//! libstdc++/libc++. `T` must implement [`OptionalCompatiblePod`] — the
marker trait
-//! carried by the fixed set of fixed-width scalars. Read with
[`get`](OptionPod::get),
-//! write with [`set`](OptionPod::set).
-//!
-//! # `OptionStr` — `String`
-//! The C++ `String` specialization keeps the 16-byte string cell inline and
marks
-//! `nullopt` with the `type_index == kTVMFFINone` sentinel; [`OptionStr`]
wraps
-//! [`String`] the same way and reuses its refcounting `Clone`/`Drop`. Borrow
with
-//! [`as_str`](OptionStr::as_str), write with [`set`](OptionStr::set).
-//! (`ffi::Optional<Bytes>` would follow the same pattern.)
-
-use crate::String;
-use std::fmt::{self, Debug};
-use std::mem::MaybeUninit;
-
-//-----------------------------------------------------
-// OptionPod<T> — POD scalars
-//-----------------------------------------------------
-
-/// Marker for a POD scalar `T` that can back an [`OptionPod<T>`]; see the
-/// [module docs](self).
-///
-/// Unsafe: an implementor guarantees `T` is trivially copyable and its Rust
-/// representation is byte-identical to the C++ field type (`i32` ↔ `int32_t`,
-/// `f64` ↔ `double`, …), so the mirror can overlay the C++ `std::optional<T>`.
-///
-/// Non-scalar payloads are rejected at compile time with a pointer to the
-/// right counterpart:
-///
-/// ```compile_fail,E0277
-/// // `Array` is an `ObjectRef` subtype → use `Option<Array<i64>>`
(`OptionObjRef`).
-/// let _ = tvm_ffi::option::OptionPod::<tvm_ffi::Array<i64>>::none();
-/// ```
-#[diagnostic::on_unimplemented(
- message = "`OptionPod<{Self}>` only mirrors `ffi::Optional` of fixed-width
POD scalars",
- label = "`{Self}` is not a fixed-width POD scalar",
- note = "for an `ObjectRef` subtype use plain `Option<{Self}>` (alias
`tvm_ffi::option::OptionObjRef`): the C++ side is a single nullable pointer",
- note = "for `String` use `tvm_ffi::option::OptionStr`"
-)]
-pub unsafe trait OptionalCompatiblePod: Copy {}
-
-/// In-place mirror of C++ `ffi::Optional<T>` for POD `T`, laid out as
-/// `std::optional<T>`: `{ T value @0; bool engaged @sizeof(T) }`.
-///
-/// Layout-compatible with the C++ type; see the [module docs](self).
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct OptionPod<T: OptionalCompatiblePod> {
- value: MaybeUninit<T>,
- engaged: bool,
-}
-
-impl<T: OptionalCompatiblePod> OptionPod<T> {
- /// Builds an engaged optional holding `value`.
- #[inline]
- pub fn some(value: T) -> Self {
- // Only payload+flag are written; padding isn't part of the ABI.
- Self {
- value: MaybeUninit::new(value),
- engaged: true,
- }
- }
-
- /// Builds a disengaged optional (`nullopt`).
- #[inline]
- pub fn none() -> Self {
- // Zeroed (not `uninit`) payload keeps the byte-image tests reading
init bytes.
- Self {
- value: MaybeUninit::zeroed(),
- engaged: false,
- }
- }
-
- /// Decodes the value in place. No FFI call, no allocation.
- #[inline]
- pub fn get(&self) -> Option<T> {
- if self.engaged {
- // The payload is written whenever `engaged` is set.
- Some(unsafe { self.value.assume_init() })
- } else {
- None
- }
- }
-
- /// Returns whether a value is present.
- #[inline]
- pub fn has_value(&self) -> bool {
- self.engaged
- }
-
- /// Returns whether the optional is `nullopt`.
- #[inline]
- pub fn is_none(&self) -> bool {
- !self.has_value()
- }
-
- /// Overwrites the value in place.
- ///
- /// Mirrors C++ assignment: `Some(v)` engages and stores `v`; `None`
- /// disengages without touching the payload bytes, as
`std::optional::reset`
- /// does for trivial `T`.
- #[inline]
- pub fn set(&mut self, value: Option<T>) {
- match value {
- Some(v) => {
- self.value = MaybeUninit::new(v);
- self.engaged = true;
- }
- None => self.engaged = false,
- }
- }
-}
-
-impl<T: OptionalCompatiblePod> Default for OptionPod<T> {
- /// `nullopt`, matching the C++ default constructor.
- #[inline]
- fn default() -> Self {
- Self::none()
- }
-}
-
-impl<T: OptionalCompatiblePod + PartialEq> PartialEq for OptionPod<T> {
- #[inline]
- fn eq(&self, other: &Self) -> bool {
- self.get() == other.get()
- }
-}
-
-impl<T: OptionalCompatiblePod + Eq> Eq for OptionPod<T> {}
-
-impl<T: OptionalCompatiblePod> From<Option<T>> for OptionPod<T> {
- #[inline]
- fn from(value: Option<T>) -> Self {
- match value {
- Some(v) => Self::some(v),
- None => Self::none(),
- }
- }
-}
-
-impl<T: OptionalCompatiblePod> From<OptionPod<T>> for Option<T> {
- #[inline]
- fn from(value: OptionPod<T>) -> Self {
- value.get()
- }
-}
-
-impl<T: OptionalCompatiblePod + Debug> Debug for OptionPod<T> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self.get() {
- Some(v) => write!(f, "OptionPod::Some({v:?})"),
- None => f.write_str("OptionPod::None"),
- }
- }
-}
-
-// Registers each supported scalar from one list: the `OptionalCompatiblePod`
impl and a
-// compile-time guard that `OptionPod<T>` matches the `std::optional<T>`
footprint
-// (`size == round_up(size_of::<T>()+1, align)`). One list keeps the impl and
its
-// layout check from drifting.
-macro_rules! impl_optional_compatible_pod {
- ($($t:ty),* $(,)?) => { $(
- // Fixed-width scalar; repr matches the C++ field's `std::optional`
- // fallback (layout proven by the `const` block below).
- unsafe impl OptionalCompatiblePod for $t {}
- const _: () = {
- let tsz = core::mem::size_of::<$t>();
- let tal = core::mem::align_of::<$t>();
- let expect = (tsz + 1).div_ceil(tal) * tal;
- assert!(core::mem::align_of::<OptionPod<$t>>() == tal);
- assert!(core::mem::size_of::<OptionPod<$t>>() == expect);
- };
- )* };
-}
-// Keep in sync with the static_assert guard at the end of
include/tvm/ffi/optional.h.
-impl_optional_compatible_pod!(bool, i8, i16, i32, i64, u8, u16, u32, u64, f32,
f64);
-
-//-----------------------------------------------------
-// OptionStr — String
-//-----------------------------------------------------
-
-/// In-place mirror of C++ `ffi::Optional<String>`: the 16-byte string cell
-/// itself, with `type_index == kTVMFFINone` meaning `nullopt` (the C++
-/// String/Bytes spec stores the sentinel in-cell, not as a separate flag).
-/// Reuses [`String`]'s `Clone`/`Drop`, whose refcounting is a no-op on the
-/// `nullopt` cell (`type_index` below `kTVMFFIStaticObjectBegin`).
-#[repr(transparent)]
-#[derive(Clone)]
-pub struct OptionStr {
- // Never handed out or accessed while disengaged (a `nullopt` cell is not a
- // valid string).
- inner: String,
-}
-
-// Must stay 16 bytes to overlay C++ `ffi::Optional<String>` (parity with the
POD
-// guard in `impl_optional_compatible_pod!`).
-const _: () = assert!(
- std::mem::size_of::<OptionStr>() == 16
- && std::mem::align_of::<OptionStr>() ==
std::mem::align_of::<crate::TVMFFIAny>()
-);
-
-impl OptionStr {
- /// An engaged optional holding `value`.
- #[inline]
- pub fn some(value: String) -> Self {
- Self { inner: value }
- }
-
- /// A disengaged optional (`nullopt`).
- #[inline]
- pub fn none() -> Self {
- Self {
- inner: String::none_cell(),
- }
- }
-
- /// Whether a value is present.
- #[inline]
- pub fn has_value(&self) -> bool {
- !self.inner.is_none_cell()
- }
-
- /// Whether the optional is `nullopt`.
- #[inline]
- pub fn is_none(&self) -> bool {
- self.inner.is_none_cell()
- }
-
- /// Borrows the engaged string as `&str`, or `None` when `nullopt`.
- #[inline]
- pub fn as_str(&self) -> Option<&str> {
- if self.has_value() {
- Some(self.inner.as_str())
- } else {
- None
- }
- }
-
- /// Takes the value out, consuming self.
- #[inline]
- pub fn get(self) -> Option<String> {
- let OptionStr { inner } = self; // no `Drop` impl, so the move is
allowed
- if inner.is_none_cell() {
- None
- } else {
- Some(inner)
- }
- }
-
- /// Overwrites the value in place, dropping the previous one first
(dec-ref'd
- /// if it was a heap string).
- #[inline]
- pub fn set(&mut self, value: Option<String>) {
- // Assignment drops the old `String` (dec_ref if heap) before moving
in the new.
- self.inner = match value {
- Some(s) => s,
- None => String::none_cell(),
- };
- }
-}
-
-impl Default for OptionStr {
- /// `nullopt`, matching the C++ default constructor.
- #[inline]
- fn default() -> Self {
- Self::none()
- }
-}
-
-impl From<Option<String>> for OptionStr {
- #[inline]
- fn from(value: Option<String>) -> Self {
- match value {
- Some(s) => Self::some(s),
- None => Self::none(),
- }
- }
-}
-
-impl From<OptionStr> for Option<String> {
- #[inline]
- fn from(value: OptionStr) -> Self {
- value.get()
- }
-}
-
-impl PartialEq for OptionStr {
- // NOT derivable: derived eq would run `String::eq` on a `nullopt` cell and
- // dereference its null object pointer; `as_str` checks the sentinel first.
- #[inline]
- fn eq(&self, other: &Self) -> bool {
- self.as_str() == other.as_str()
- }
-}
-
-impl Eq for OptionStr {}
-
-impl Debug for OptionStr {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self.as_str() {
- Some(s) => write!(f, "OptionStr::Some({s:?})"),
- None => f.write_str("OptionStr::None"),
- }
- }
-}
-
-//-----------------------------------------------------
-// OptionObjRef — ObjectRef subtypes
-//-----------------------------------------------------
-
-/// Alias of [`Option`] for `ObjectRef`-subtype fields.
-///
-/// C++ `ffi::Optional<SomeRef>` is a single nullable pointer, so
`Option<SomeRef>`
-/// (niche-optimized over the ref's non-null [`ObjectArc`](crate::ObjectArc)
-/// pointer) already mirrors it in place: `None` == `nullptr`. The alias only
-/// names that contract for consistency.
-pub type OptionObjRef<T> = Option<T>;
diff --git a/rust/tvm-ffi/src/optional.rs b/rust/tvm-ffi/src/optional.rs
new file mode 100644
index 00000000..33c85231
--- /dev/null
+++ b/rust/tvm-ffi/src/optional.rs
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+//! In-place mirror of C++ `ffi::Optional<T>` (`include/tvm/ffi/optional.h`).
+//!
+//! C++ `ffi::Optional<T>` is uniformly backed by a single 16-byte `TVMFFIAny`
+//! regardless of `T`, with `type_index == kTVMFFINone` meaning `nullopt`. This
+//! makes the layout independent of the contained type, so a single Rust type
+//! mirrors every `T`.
+//!
+//! [`Optional<T>`] is `#[repr(transparent)]` over [`Any`] (the same 16-byte
+//! `TVMFFIAny` cell) and decodes such a field's bytes in place — no FFI call,
no
+//! reflection getter/setter. It is named `Optional` (not `Option`) to
distinguish
+//! it from Rust's [`std::option::Option`], matching the C++ `ffi::Optional`
name.
+//!
+//! It replaces the earlier per-`T` mirrors (`OptionPod<T>` / `OptionStr` /
+//! `OptionObjRef<T>`): those tracked the three now-removed C++ storage layouts
+//! (`std::optional<T>`, the `String`/`Bytes` sentinel cell, and an `ObjectRef`
+//! pointer). With the uniform `TVMFFIAny` backing they collapse into this one
+//! type.
+
+use crate::any::Any;
+use crate::string::String;
+use crate::type_traits::AnyCompatible;
+use std::fmt::{self, Debug};
+use std::marker::PhantomData;
+use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
+
+/// In-place mirror of C++ `ffi::Optional<T>`: a single 16-byte `TVMFFIAny`
cell
+/// (wrapped as [`Any`]) whose `type_index == kTVMFFINone` encodes `nullopt`.
+///
+/// Layout-compatible with the C++ type (`size_of == 16`); see the [module
+/// docs](self). Reuses [`Any`]'s reference-counting `Clone`/`Drop`, which are
a
+/// no-op on the `nullopt` cell (`type_index` below
`kTVMFFIStaticObjectBegin`).
+#[repr(transparent)]
+pub struct Optional<T: AnyCompatible> {
+ // Holds either the value's `TVMFFIAny` representation or a `kTVMFFINone`
cell.
+ data: Any,
+ _marker: PhantomData<T>,
+}
+
+// Must stay 16 bytes / `TVMFFIAny`-aligned to overlay a C++ `ffi::Optional<T>`
+// field in place, independent of `T`.
+const _: () = assert!(
+ std::mem::size_of::<Optional<i64>>() == 16
+ && std::mem::size_of::<Optional<String>>() == 16
+ && std::mem::align_of::<Optional<i64>>() ==
std::mem::align_of::<crate::TVMFFIAny>()
+);
+
+impl<T: AnyCompatible> Optional<T> {
+ /// An engaged optional holding `value`.
+ #[inline]
+ pub fn some(value: T) -> Self {
+ Self {
+ data: Any::from(value),
+ _marker: PhantomData,
+ }
+ }
+
+ /// A disengaged optional (`nullopt`, a `kTVMFFINone` cell).
+ #[inline]
+ pub fn none() -> Self {
+ Self {
+ data: Any::new(),
+ _marker: PhantomData,
+ }
+ }
+
+ /// Whether a value is present.
+ #[inline]
+ pub fn has_value(&self) -> bool {
+ self.data.type_index() != TypeIndex::kTVMFFINone as i32
+ }
+
+ /// Whether the optional is `nullopt`.
+ #[inline]
+ pub fn is_none(&self) -> bool {
+ !self.has_value()
+ }
+
+ /// Decodes the value in place, cloning it out (ref-counted `inc_ref` for
+ /// object payloads). Returns `None` when `nullopt`. No FFI call.
+ #[inline]
+ pub fn get(&self) -> Option<T> {
+ self.data.try_as::<T>()
+ }
+
+ /// Takes the value out, consuming self (moves the payload, no `inc_ref`).
+ #[inline]
+ pub fn into_option(self) -> Option<T> {
+ if self.has_value() {
+ // Move the value out of the owning `Any` without dropping it, then
+ // transfer ownership of the payload into `T` (no inc/dec ref).
+ let mut raw = unsafe { Any::into_raw_ffi_any(self.data) };
+ Some(unsafe { T::move_from_any_after_check(&mut raw) })
+ } else {
+ None
+ }
+ }
+
+ /// Overwrites the value in place, dropping the previous one first
(dec-ref'd
+ /// if it was an object payload).
+ #[inline]
+ pub fn set(&mut self, value: Option<T>) {
+ // Assignment drops the old `Any` (dec_ref if object) before storing
the new.
+ self.data = match value {
+ Some(v) => Any::from(v),
+ None => Any::new(),
+ };
+ }
+}
+
+impl Optional<String> {
+ /// Borrows the engaged string as `&str`, or `None` when `nullopt`.
+ ///
+ /// Reinterprets the in-cell string without cloning: an engaged cell holds
a
+ /// `String`'s exact 16-byte representation, so `&Any` can be viewed as
+ /// `&String`.
+ #[inline]
+ pub fn as_str(&self) -> Option<&str> {
+ if self.has_value() {
+ // SAFETY: an engaged `Optional<String>` cell is byte-identical to
a
+ // `String` (both are `#[repr(transparent)]` over the 16-byte
cell),
+ // and the borrow is tied to `&self`.
+ let s = unsafe { &*(&self.data as *const Any as *const String) };
+ Some(s.as_str())
+ } else {
+ None
+ }
+ }
+}
+
+impl<T: AnyCompatible> Default for Optional<T> {
+ /// `nullopt`, matching the C++ default constructor.
+ #[inline]
+ fn default() -> Self {
+ Self::none()
+ }
+}
+
+impl<T: AnyCompatible> Clone for Optional<T> {
+ #[inline]
+ fn clone(&self) -> Self {
+ Self {
+ // `Any::clone` inc_refs an object payload; a `nullopt` cell is a
no-op.
+ data: self.data.clone(),
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<T: AnyCompatible + PartialEq> PartialEq for Optional<T> {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.get() == other.get()
+ }
+}
+
+impl<T: AnyCompatible + Eq> Eq for Optional<T> {}
+
+impl<T: AnyCompatible> From<Option<T>> for Optional<T> {
+ #[inline]
+ fn from(value: Option<T>) -> Self {
+ match value {
+ Some(v) => Self::some(v),
+ None => Self::none(),
+ }
+ }
+}
+
+impl<T: AnyCompatible> From<Optional<T>> for Option<T> {
+ #[inline]
+ fn from(value: Optional<T>) -> Self {
+ value.into_option()
+ }
+}
+
+impl<T: AnyCompatible + Debug> Debug for Optional<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self.get() {
+ Some(v) => write!(f, "Optional::Some({v:?})"),
+ None => f.write_str("Optional::None"),
+ }
+ }
+}
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index 70768d6f..94739a4c 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -284,23 +284,6 @@ impl String {
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }
}
-
- /// Whether the cell is the `nullopt` sentinel (`type_index ==
kTVMFFINone`).
- /// Such a cell is NOT a valid string; only
[`OptionStr`](crate::option::OptionStr)
- /// holds one, and never calls string accessors on it.
- #[inline]
- pub(crate) fn is_none_cell(&self) -> bool {
- self.data.type_index == TypeIndex::kTVMFFINone as i32
- }
-
- /// A `nullopt` sentinel cell for [`OptionStr`](crate::option::OptionStr).
- /// `TVMFFIAny::new()` is exactly the all-zero `kTVMFFINone` cell.
- #[inline]
- pub(crate) fn none_cell() -> Self {
- Self {
- data: TVMFFIAny::new(),
- }
- }
}
impl<T> From<T> for String
diff --git a/rust/tvm-ffi/tests/test_optional.rs
b/rust/tvm-ffi/tests/test_optional.rs
index 18eadec0..c90ca468 100644
--- a/rust/tvm-ffi/tests/test_optional.rs
+++ b/rust/tvm-ffi/tests/test_optional.rs
@@ -16,57 +16,70 @@
* specific language governing permissions and limitations
* under the License.
*/
-use tvm_ffi::option::{OptionObjRef, OptionPod, OptionStr,
OptionalCompatiblePod};
use tvm_ffi::*;
-/// Payload+flag bytes `[0, size_of::<T>()]`; padding is excluded (not ABI, not
-/// guaranteed initialized).
-fn image<T: OptionalCompatiblePod>(opt: &OptionPod<T>) -> Vec<u8> {
- let p = opt as *const OptionPod<T> as *const u8;
- let n = std::mem::size_of::<T>() + 1; // payload + flag, no padding
- // Payload and flag are always initialized; padding is not read.
- unsafe { std::slice::from_raw_parts(p, n).to_vec() }
+/// The 16-byte `TVMFFIAny` cell backing an `Optional<T>` (type_index@0,
+/// small_str_len@4, union@8); no padding.
+fn cell_image<T: AnyCompatible>(opt: &Optional<T>) -> [u8; 16] {
+ let p = opt as *const Optional<T> as *const u8;
+ let mut b = [0u8; 16];
+ // `Optional<T>` is a fully-initialized 16-byte cell.
+ unsafe { std::ptr::copy_nonoverlapping(p, b.as_mut_ptr(), 16) };
+ b
}
-/// The scalar's own bytes, to assert the optional's payload matches it
verbatim.
-fn raw_bytes<T: OptionalCompatiblePod>(v: &T) -> Vec<u8> {
- let p = v as *const T as *const u8;
- // size_of::<T>() bytes of an initialized `Copy` scalar.
- unsafe { std::slice::from_raw_parts(p, std::mem::size_of::<T>()).to_vec() }
+#[test]
+fn layout_is_uniform_16_bytes() {
+ // Independent of `T`: every `Optional<T>` is the 16-byte `TVMFFIAny` cell.
+ assert_eq!(std::mem::size_of::<Optional<i32>>(), 16);
+ assert_eq!(std::mem::size_of::<Optional<i64>>(), 16);
+ assert_eq!(std::mem::size_of::<Optional<bool>>(), 16);
+ assert_eq!(std::mem::size_of::<Optional<f64>>(), 16);
+ assert_eq!(std::mem::size_of::<Optional<String>>(), 16);
+ assert_eq!(std::mem::size_of::<Optional<Array<i64>>>(), 16);
+ assert_eq!(
+ std::mem::align_of::<Optional<i32>>(),
+ std::mem::align_of::<Optional<Array<i64>>>()
+ );
}
#[test]
-#[cfg(target_endian = "little")]
-fn byte_image_some_i32_matches_cpp() {
- // C++ probe (both STLs): some(0x12345678) => 78 56 34 12 | 01 ..
- let o = OptionPod::<i32>::some(0x1234_5678);
- let b = image(&o);
- assert_eq!(&b[0..4], &0x1234_5678_i32.to_le_bytes());
- assert_eq!(b[4], 1, "engaged flag must sit at offset size_of::<i32>()");
+fn none_is_all_zero_cell() {
+ // `kTVMFFINone == 0` and the union is zeroed, so `nullopt` is 16 zero
bytes.
+ assert_eq!(cell_image(&Optional::<i32>::none()), [0u8; 16]);
+ assert_eq!(cell_image(&Optional::<String>::none()), [0u8; 16]);
+ assert_eq!(cell_image(&Optional::<Array<i64>>::none()), [0u8; 16]);
}
#[test]
#[cfg(target_endian = "little")]
-fn byte_image_some_i64_matches_cpp() {
- let o = OptionPod::<i64>::some(0x1122_3344_5566_7788);
- let b = image(&o);
- assert_eq!(&b[0..8], &0x1122_3344_5566_7788_i64.to_le_bytes());
- assert_eq!(b[8], 1, "engaged flag must sit at offset size_of::<i64>()");
+fn byte_image_some_int_matches_ffi_any() {
+ // some(0x12345678) => type_index = kTVMFFIInt @0, v_int64 = value @8.
+ let o = Optional::<i32>::some(0x1234_5678);
+ let b = cell_image(&o);
+ assert_eq!(
+ &b[0..4],
+ &(TypeIndex::kTVMFFIInt as i32).to_le_bytes(),
+ "type_index must be kTVMFFIInt"
+ );
+ assert_eq!(&b[4..8], &[0u8; 4], "zero padding");
+ assert_eq!(
+ &b[8..16],
+ &0x1234_5678_i64.to_le_bytes(),
+ "payload sits in v_int64 @8"
+ );
}
-/// Payload@0 and flag@`size_of::<T>()` for every supported type, not just
i32/i64.
#[test]
-fn flag_offset_all_supported_types() {
- fn check<T: OptionalCompatiblePod + PartialEq + std::fmt::Debug>(val: T) {
+fn pod_roundtrip_all_supported_types() {
+ fn check<T: AnyCompatible + PartialEq + Copy + std::fmt::Debug>(val: T) {
let ty = std::any::type_name::<T>();
- let sz = std::mem::size_of::<T>();
- let some = OptionPod::<T>::some(val);
- let b = image(&some);
- assert_eq!(&b[0..sz], &raw_bytes(&val)[..], "payload@0 for {ty}");
- assert_eq!(b[sz], 1, "engaged flag must sit at offset size_of for
{ty}");
+ let some = Optional::<T>::some(val);
+ assert!(some.has_value(), "engaged has_value for {ty}");
assert_eq!(some.get(), Some(val), "engaged roundtrip for {ty}");
- let none = OptionPod::<T>::none();
- assert_eq!(image(&none)[sz], 0, "flag must be clear for none for
{ty}");
+
+ let none = Optional::<T>::none();
+ assert!(none.is_none(), "disengaged is_none for {ty}");
assert!(none.get().is_none(), "disengaged roundtrip for {ty}");
}
check::<bool>(true);
@@ -84,7 +97,7 @@ fn flag_offset_all_supported_types() {
#[test]
fn roundtrip_get_set() {
- let mut o = OptionPod::<i32>::some(42);
+ let mut o = Optional::<i32>::some(42);
assert_eq!(o.get(), Some(42));
assert!(o.has_value());
@@ -98,90 +111,84 @@ fn roundtrip_get_set() {
#[test]
fn conversions_and_default() {
- assert_eq!(OptionPod::<f64>::from(Some(2.5)).get(), Some(2.5));
- assert_eq!(OptionPod::<f64>::from(None).get(), None);
- let back: Option<i16> = OptionPod::<i16>::some(9).into();
+ assert_eq!(Optional::<f64>::from(Some(2.5)).get(), Some(2.5));
+ assert_eq!(Optional::<f64>::from(None).get(), None);
+ let back: Option<i16> = Optional::<i16>::some(9).into();
assert_eq!(back, Some(9));
- assert_eq!(OptionPod::<u8>::default().get(), None);
- assert_eq!(OptionPod::<bool>::some(true).get(), Some(true));
+ assert_eq!(Optional::<u8>::default().get(), None);
+ assert_eq!(Optional::<bool>::some(true).get(), Some(true));
}
#[test]
-#[allow(clippy::clone_on_copy)] // explicitly exercising the Clone impl
-fn clone_preserves_state() {
- let some = OptionPod::<i64>::some(123);
+fn clone_preserves_state_pod() {
+ let some = Optional::<i64>::some(123);
assert_eq!(some.clone().get(), Some(123));
- let none = OptionPod::<i64>::none();
+ let none = Optional::<i64>::none();
assert_eq!(none.clone().get(), None);
}
#[test]
-fn equality() {
- assert_eq!(OptionPod::<i32>::some(1), OptionPod::<i32>::some(1));
- assert_ne!(OptionPod::<i32>::some(1), OptionPod::<i32>::some(2));
- assert_ne!(OptionPod::<i32>::some(1), OptionPod::<i32>::none());
- assert_eq!(OptionPod::<i32>::none(), OptionPod::<i32>::none());
- // `set(None)` leaves stale payload bytes; equality must still see plain
None.
- let mut stale = OptionPod::<i32>::some(7);
- stale.set(None);
- assert_eq!(stale, OptionPod::<i32>::none());
-
- let a = || OptionStr::some(String::from("a"));
+fn equality_pod_and_string() {
+ assert_eq!(Optional::<i32>::some(1), Optional::<i32>::some(1));
+ assert_ne!(Optional::<i32>::some(1), Optional::<i32>::some(2));
+ assert_ne!(Optional::<i32>::some(1), Optional::<i32>::none());
+ assert_eq!(Optional::<i32>::none(), Optional::<i32>::none());
+ // `set(None)` resets to a plain `nullopt` cell; equality must still hold.
+ let mut cleared = Optional::<i32>::some(7);
+ cleared.set(None);
+ assert_eq!(cleared, Optional::<i32>::none());
+
+ let a = || Optional::<String>::some(String::from("a"));
assert_eq!(a(), a());
- assert_ne!(a(), OptionStr::some(String::from("b")));
- assert_ne!(a(), OptionStr::none());
- assert_eq!(OptionStr::none(), OptionStr::none());
-}
-
-// 16-byte cell (type_index@0, small_str_len@4, union@8); no padding.
-fn str_image(o: &OptionStr) -> [u8; 16] {
- let p = o as *const OptionStr as *const u8;
- let mut b = [0u8; 16];
- // OptionStr is a fully-initialized 16-byte TVMFFIAny cell.
- unsafe { std::ptr::copy_nonoverlapping(p, b.as_mut_ptr(), 16) };
- b
+ assert_ne!(a(), Optional::<String>::some(String::from("b")));
+ assert_ne!(a(), Optional::<String>::none());
+ assert_eq!(Optional::<String>::none(), Optional::<String>::none());
}
#[test]
#[cfg(target_endian = "little")]
-fn optional_str_byte_image_matches_cpp() {
- // C++ probe: ffi::Optional<String> none => 16 zero bytes;
- // some("hi") => 0b 00 00 00 | 02 00 00 00 | 68 69 00
...
- assert_eq!(str_image(&OptionStr::none()), [0u8; 16]);
- let some = OptionStr::some(String::from("hi"));
- let b = str_image(&some);
- assert_eq!(&b[0..4], &[0x0b, 0, 0, 0], "type_index = kTVMFFISmallStr");
+fn string_byte_image_matches_ffi_any() {
+ // ffi::Optional<String> none => 16 zero bytes;
+ // some("hi") => kTVMFFISmallStr @0 | small_str_len=2 @4 | "hi" inline @8.
+ assert_eq!(cell_image(&Optional::<String>::none()), [0u8; 16]);
+ let some = Optional::<String>::some(String::from("hi"));
+ let b = cell_image(&some);
+ assert_eq!(
+ &b[0..4],
+ &(TypeIndex::kTVMFFISmallStr as i32).to_le_bytes(),
+ "type_index = kTVMFFISmallStr"
+ );
assert_eq!(&b[4..8], &[0x02, 0, 0, 0], "small_str_len = 2");
assert_eq!(&b[8..10], b"hi", "inline payload");
}
#[test]
-fn optional_str_roundtrip_and_conversions() {
+fn string_roundtrip_and_as_str() {
// small (inline) string
- let s = OptionStr::some(String::from("hi"));
+ let s = Optional::<String>::some(String::from("hi"));
assert!(s.has_value());
assert_eq!(s.as_str(), Some("hi"));
assert_eq!(s.get().as_deref(), Some("hi"));
// nullopt
- let n = OptionStr::none();
+ let n = Optional::<String>::none();
assert!(n.is_none());
assert_eq!(n.as_str(), None);
assert_eq!(n.get(), None);
// conversions + default
- let from_some: OptionStr = Some(String::from("x")).into();
+ let from_some: Optional<String> = Some(String::from("x")).into();
assert_eq!(from_some.as_str(), Some("x"));
- let back: Option<String> = OptionStr::none().into();
+ let back: Option<String> = Optional::<String>::none().into();
assert!(back.is_none());
- assert!(OptionStr::default().is_none());
+ assert!(Optional::<String>::default().is_none());
}
#[test]
-fn optional_str_heap_clone_no_double_free() {
+fn string_heap_clone_no_double_free() {
// long (heap) string exercises refcounted Clone/Drop through the wrapper.
let long = String::from("a-very-long-heap-allocated-string-value");
- let a = OptionStr::some(long);
+ let a = Optional::<String>::some(long);
let b = a.clone();
assert_eq!(a.as_str(), Some("a-very-long-heap-allocated-string-value"));
assert_eq!(b.as_str(), Some("a-very-long-heap-allocated-string-value"));
@@ -189,16 +196,15 @@ fn optional_str_heap_clone_no_double_free() {
}
#[test]
-fn optional_str_set_in_place() {
- // Start engaged with a heap string, then replace it: `set` must drop
- // (dec_ref) the old heap string before moving the new one in.
- let mut o =
OptionStr::some(String::from("first-long-heap-allocated-value"));
+fn string_set_in_place() {
+ // Replace an engaged heap string: `set` must drop (dec_ref) the old heap
+ // string before storing the new one.
+ let mut o =
Optional::<String>::some(String::from("first-long-heap-allocated-value"));
assert_eq!(o.as_str(), Some("first-long-heap-allocated-value"));
o.set(Some(String::from("second-long-heap-allocated-value")));
assert_eq!(o.as_str(), Some("second-long-heap-allocated-value"));
- // disengage (drops the heap string), then re-engage
o.set(None);
assert!(o.is_none());
assert_eq!(o.as_str(), None);
@@ -208,13 +214,24 @@ fn optional_str_set_in_place() {
}
#[test]
-fn option_obj_ref_is_nullable_pointer() {
- // `OptionObjRef<SomeRef>` == `Option<SomeRef>`: the niche over the ref's
- // non-null object pointer makes it a single nullable pointer, `None` ==
`nullptr`.
- assert_eq!(
- std::mem::size_of::<OptionObjRef<Array<i64>>>(),
- std::mem::size_of::<*mut ()>()
- );
- let none: OptionObjRef<Array<i64>> = None;
+fn object_ref_payload_roundtrip_and_clone() {
+ // An ObjectRef payload is now the same 16-byte cell (was an 8-byte
pointer).
+ let arr = Array::new(vec![1i64, 2, 3]);
+ let opt = Optional::<Array<i64>>::some(arr);
+ assert!(opt.has_value());
+ let got = opt.get().expect("engaged");
+ assert_eq!(got.len(), 3);
+
+ // clone shares the underlying object; both drop without double-free.
+ let cloned = opt.clone();
+ assert!(cloned.has_value());
+ assert_eq!(cloned.get().expect("engaged").len(), 3);
+
+ // move the payload out
+ let moved: Option<Array<i64>> = opt.into_option();
+ assert_eq!(moved.expect("moved").len(), 3);
+
+ let none = Optional::<Array<i64>>::none();
assert!(none.is_none());
+ assert!(none.get().is_none());
}
diff --git a/src/ffi/extra/json_writer.cc b/src/ffi/extra/json_writer.cc
index 77f4411a..fe906048 100644
--- a/src/ffi/extra/json_writer.cc
+++ b/src/ffi/extra/json_writer.cc
@@ -44,8 +44,7 @@ namespace json {
class JSONWriter {
public:
- // NOLINTNEXTLINE(performance-unnecessary-value-param)
- static String Stringify(const json::Value& value, Optional<int> indent) {
+ static String Stringify(const json::Value& value, const Optional<int>&
indent) {
JSONWriter writer(indent.value_or(0));
writer.WriteValue(value);
return String(std::move(writer.result_));
@@ -270,8 +269,8 @@ class JSONWriter {
std::unordered_set<const void*> active_lists_;
};
-String Stringify(const json::Value& value, Optional<int> indent) {
- return JSONWriter::Stringify(value, indent); //
NOLINT(performance-unnecessary-value-param)
+String Stringify(const json::Value& value, const Optional<int>& indent) {
+ return JSONWriter::Stringify(value, indent);
}
TVM_FFI_STATIC_INIT_BLOCK() {
diff --git a/tests/cpp/test_object.cc b/tests/cpp/test_object.cc
index a597e425..620fb119 100644
--- a/tests/cpp/test_object.cc
+++ b/tests/cpp/test_object.cc
@@ -80,7 +80,9 @@ inline constexpr bool object_ref_contains_is_enabled_v<
static_assert(ObjectRef::_type_container_is_exact);
static_assert(TNumber::_type_container_is_exact);
static_assert(TInt::_type_container_is_exact);
-static_assert(Optional<TInt>::_type_container_is_exact);
+// Optional<T> is uniformly Any-backed and is no longer an ObjectRef, so it
does
+// not participate in the ObjectRef container concept.
+static_assert(!std::is_base_of_v<ObjectRef, Optional<TInt>>);
static_assert(!TIntOrFloatRef::_type_container_is_exact);
static_assert(!Array<TInt>::_type_container_is_exact);
static_assert(!List<TInt>::_type_container_is_exact);
@@ -91,7 +93,9 @@ static_assert(!Variant<TInt,
TFloat>::_type_container_is_exact);
static_assert(object_ref_contains_v<TNumber, TIntObj>);
static_assert(object_ref_contains_v<TInt, TIntObj>);
-static_assert(object_ref_contains_v<Optional<TInt>, TIntObj>);
+// Optional<T> is no longer an ObjectRef, so it is outside the
object-ref-contains
+// concept entirely (the trait is not even enabled for it).
+static_assert(!object_ref_contains_is_enabled_v<Optional<TInt>, TIntObj>);
static_assert(!object_ref_contains_v<TInt, TFloatObj>);
static_assert(!object_ref_contains_v<Array<TInt>, ArrayObj>);
static_assert(object_ref_contains_v<TIntOrFloatRef, TIntObj>);
diff --git a/tests/cpp/test_optional.cc b/tests/cpp/test_optional.cc
index f9dedd1d..e0a696e5 100644
--- a/tests/cpp/test_optional.cc
+++ b/tests/cpp/test_optional.cc
@@ -19,6 +19,7 @@
#include <gtest/gtest.h>
#include <tvm/ffi/any.h>
#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/memory.h>
#include <tvm/ffi/optional.h>
@@ -32,10 +33,42 @@ namespace {
using namespace tvm::ffi;
using namespace tvm::ffi::testing;
+// Regression guard: TypeTraits<Optional<T>>::storage_enabled must PROPAGATE
from
+// TypeTraits<T>::storage_enabled (pass-through), not be hardcoded. This keeps
a
+// nested Optional<Optional<T>> and Optional<T> used inside
Variant<...>/containers
+// Any-backed exactly when T itself is storage-enabled.
+static_assert(TypeTraits<Optional<int>>::storage_enabled ==
TypeTraits<int>::storage_enabled);
+static_assert(TypeTraits<Optional<int>>::storage_enabled, "int is
storage-enabled");
+static_assert(TypeTraits<Optional<TInt>>::storage_enabled ==
TypeTraits<TInt>::storage_enabled);
+static_assert(TypeTraits<Optional<TInt>>::storage_enabled, "ObjectRef is
storage-enabled");
+// A non-owning view type is NOT storage-enabled; the pass-through must carry
that.
+static_assert(!TypeTraits<TensorView>::storage_enabled);
+static_assert(TypeTraits<Optional<TensorView>>::storage_enabled ==
+ TypeTraits<TensorView>::storage_enabled);
+static_assert(!TypeTraits<Optional<TensorView>>::storage_enabled,
+ "Optional<view> must not be storage-enabled");
+// Because Optional<int>/Optional<TInt> are storage-enabled, the outer
+// Optional<Optional<T>> uses the Any-backed representation (sizeof ==
sizeof(Any)),
+// and Optional<T> is accepted as a storage type (e.g. inside
Variant/containers).
+static_assert(sizeof(Optional<Optional<int>>) == sizeof(Any));
+static_assert(sizeof(Optional<Optional<TInt>>) == sizeof(Any));
+static_assert(details::storage_enabled_v<Optional<int>>);
+
+TEST(Optional, StorageEnabledPassThrough) {
+ // Optional<int> is storage-enabled, so a nested Optional round-trips through
+ // Any via the Any-backed path.
+ Optional<Optional<int>> nested = Optional<int>(7);
+ Any any = nested;
+ auto back = any.cast<Optional<Optional<int>>>();
+ EXPECT_TRUE(back.has_value());
+ EXPECT_EQ(back.value().value(), 7);
+}
+
TEST(Optional, TInt) {
Optional<TInt> x;
Optional<TInt> y = TInt(11);
- static_assert(sizeof(Optional<TInt>) == sizeof(ObjectRef));
+ // Optional<T> is uniformly backed by a single Any (TVMFFIAny) regardless of
T.
+ static_assert(sizeof(Optional<TInt>) == sizeof(Any));
EXPECT_TRUE(!x.has_value());
EXPECT_EQ(x.value_or(TInt(12))->value, 12);
@@ -51,7 +84,6 @@ TEST(Optional, TInt) {
// move from any to optional
auto y2 = std::move(z_any).cast<Optional<TInt>>();
- EXPECT_EQ(y2.use_count(), 1);
EXPECT_TRUE(y2.has_value());
EXPECT_EQ(y2.value_or(TInt(12))->value, 11);
}
@@ -59,7 +91,8 @@ TEST(Optional, TInt) {
TEST(Optional, double) {
Optional<double> x;
Optional<double> y = 11.0;
- static_assert(sizeof(Optional<double>) > sizeof(ObjectRef));
+ // Layout is independent of the contained type: sizeof(Optional<T>) ==
sizeof(Any).
+ static_assert(sizeof(Optional<double>) == sizeof(Any));
EXPECT_TRUE(!x.has_value());
EXPECT_EQ(x.value_or(12), 12);
@@ -120,20 +153,31 @@ TEST(Optional, AnyConvertArray) {
TEST(Optional, OptionalOfOptional) {
// testcase of optional<optional>
+ //
+ // Because nullopt is uniformly represented as kTVMFFINone in the single
+ // TVMFFIAny backing, an engaged outer Optional that holds an empty inner
+ // Optional collapses to nullopt (the two states share the kTVMFFINone bit
+ // pattern). This matches the behavior of round-tripping a nested Optional
+ // through Any. Engaged-outer/engaged-inner still nests correctly.
Optional<Optional<int>> opt_opt_int;
EXPECT_TRUE(!opt_opt_int.has_value());
+ // engaged outer holding an empty inner collapses to nullopt.
Optional<Optional<int>> opt_opt_int2 = Optional<int>(std::nullopt);
- EXPECT_TRUE(opt_opt_int2.has_value());
- EXPECT_TRUE(!opt_opt_int2.value().has_value());
+ EXPECT_TRUE(!opt_opt_int2.has_value());
+
+ // engaged outer holding an engaged inner nests correctly.
+ Optional<Optional<int>> opt_opt_int3 = Optional<int>(7);
+ EXPECT_TRUE(opt_opt_int3.has_value());
+ EXPECT_TRUE(opt_opt_int3.value().has_value());
+ EXPECT_EQ(opt_opt_int3.value().value(), 7);
// Optional<Optional<ObjectRef>>
Optional<Optional<TInt>> opt_opt_tint;
EXPECT_TRUE(!opt_opt_tint.has_value());
Optional<Optional<TInt>> opt_opt_tint2 = Optional<TInt>(std::nullopt);
- EXPECT_TRUE(opt_opt_tint2.has_value());
- EXPECT_TRUE(!opt_opt_tint2.value().has_value());
+ EXPECT_TRUE(!opt_opt_tint2.has_value());
opt_opt_tint2 = std::nullopt;
EXPECT_TRUE(!opt_opt_tint2.has_value());
@@ -191,7 +235,7 @@ TEST(Optional, String) {
EXPECT_TRUE(opt_str == "hello");
EXPECT_TRUE(opt_str == String("hello"));
EXPECT_TRUE(opt_str != std::nullopt);
- static_assert(sizeof(Optional<String>) == sizeof(String));
+ static_assert(sizeof(Optional<String>) == sizeof(Any));
}
TEST(Optional, Bytes) {
@@ -203,49 +247,18 @@ TEST(Optional, Bytes) {
EXPECT_TRUE(opt_bytes.has_value());
EXPECT_EQ(opt_bytes.value().operator std::string(), "hello");
EXPECT_TRUE(opt_bytes != std::nullopt);
- static_assert(sizeof(Optional<Bytes>) == sizeof(Bytes));
+ static_assert(sizeof(Optional<Bytes>) == sizeof(Any));
}
-// The Rust binding (rust/tvm-ffi/src/option.rs) mirrors Optional<T> in place
as
-// `{ T value; bool engaged; }`; fail early if a toolchain's std::optional
deviates from that.
+// Optional<T> is uniformly backed by a single TVMFFIAny (Any), so its layout
is
+// independent of the contained type: every Optional<T> has the size and
+// alignment of a TVMFFIAny. (The Rust binding's in-place Optional<T> mirror
is a
+// separate follow-up that adopts this uniform 16-byte representation.)
template <typename... T>
-constexpr bool all_optional_layouts_match_rust_mirror_v =
- ((sizeof(Optional<T>) == sizeof(T) + alignof(T) && alignof(Optional<T>) ==
alignof(T)) && ...);
+constexpr bool all_optional_layouts_uniform_v =
+ ((sizeof(Optional<T>) == sizeof(TVMFFIAny) && alignof(Optional<T>) ==
alignof(TVMFFIAny)) &&
+ ...);
static_assert(
- all_optional_layouts_match_rust_mirror_v<bool, int8_t, int16_t, int32_t,
int64_t, uint8_t,
- uint16_t, uint32_t, uint64_t,
float, double>);
-// Same contract for the String/Bytes specialization (Rust OptionStr overlays
the cell).
-static_assert(sizeof(Optional<String>) == sizeof(TVMFFIAny) &&
- sizeof(Optional<Bytes>) == sizeof(TVMFFIAny));
-
-// The overlay also assumes the field order `{ T value @0; bool engaged
@sizeof(T) }`;
-// pin that here (the static_asserts above pin only size/align).
-template <typename T>
-void CheckRustMirrorFieldOrder(T v) {
- Optional<T> opt(v);
- const char* base = reinterpret_cast<const char*>(&opt);
- T payload;
- std::memcpy(&payload, base, sizeof(T));
- EXPECT_EQ(payload, v) << "payload must sit at offset 0";
- bool engaged = false;
- std::memcpy(&engaged, base + sizeof(T), sizeof(bool));
- EXPECT_TRUE(engaged) << "engaged flag must sit at offset sizeof(T)";
- opt = std::nullopt;
- std::memcpy(&engaged, base + sizeof(T), sizeof(bool));
- EXPECT_FALSE(engaged) << "disengaging must clear the flag at offset
sizeof(T)";
-}
-
-TEST(Optional, RustMirrorFieldOrder) {
- CheckRustMirrorFieldOrder<bool>(true);
- CheckRustMirrorFieldOrder<int8_t>(0x12);
- CheckRustMirrorFieldOrder<int16_t>(0x1234);
- CheckRustMirrorFieldOrder<int32_t>(0x12345678);
- CheckRustMirrorFieldOrder<int64_t>(0x1122334455667788LL);
- CheckRustMirrorFieldOrder<uint8_t>(0xAB);
- CheckRustMirrorFieldOrder<uint16_t>(0xABCD);
- CheckRustMirrorFieldOrder<uint32_t>(0xABCDEF01u);
- CheckRustMirrorFieldOrder<uint64_t>(0xABCDEF0123456789ULL);
- CheckRustMirrorFieldOrder<float>(1.5f);
- CheckRustMirrorFieldOrder<double>(2.5);
-}
+ all_optional_layouts_uniform_v<bool, int8_t, int16_t, int32_t, int64_t,
uint8_t, uint16_t,
+ uint32_t, uint64_t, float, double, String,
Bytes>);
} // namespace
diff --git a/tests/cpp/test_variant.cc b/tests/cpp/test_variant.cc
index 40a55769..77291108 100644
--- a/tests/cpp/test_variant.cc
+++ b/tests/cpp/test_variant.cc
@@ -138,7 +138,9 @@ TEST(Variant, Upcast) {
TEST(Variant, AllObjectRef) {
Variant<TInt, Array<TInt>> v0 = TInt(1);
EXPECT_EQ(v0.get<TInt>()->value, 1);
- static_assert(std::is_base_of_v<ObjectRef, decltype(v0)>);
+ // Variant is uniformly backed by a single Any (TVMFFIAny), even when every
+ // alternative derives from ObjectRef, so it is not ObjectRef-derived.
+ static_assert(!std::is_base_of_v<ObjectRef, decltype(v0)>);
Any any0 = v0;
EXPECT_EQ(any0.cast<TInt>()->value, 1);
auto v2 = any0.cast<Variant<TInt, Array<TInt>>>();
@@ -148,7 +150,9 @@ TEST(Variant, AllObjectRef) {
EXPECT_EQ(v0.get<Array<TInt>>().size(), 2);
EXPECT_EQ(v0.get<Array<TInt>>()[0]->value, 2);
EXPECT_EQ(v0.get<Array<TInt>>()[1]->value, 3);
- EXPECT_EQ(sizeof(v0), sizeof(ObjectRef));
+ // The uniform TVMFFIAny backing makes the layout independent of the
contained
+ // alternatives: sizeof(Variant<...>) == sizeof(Any).
+ EXPECT_EQ(sizeof(v0), sizeof(Any));
}
TEST(Variant, PODSameAs) {