This is an automated email from the ASF dual-hosted git repository.
yongwww 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 12dbf053 [FFI] Keep object optionals pointer-sized (#701)
12dbf053 is described below
commit 12dbf053b3d9ba4ebd9da3123b1aeca79cf74229
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Aug 5 06:31:44 2026 +0800
[FFI] Keep object optionals pointer-sized (#701)
This restores the established pointer-sized ABI for object optionals
while preserving the Any-backed representation for non-object values.
---
addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc | 4 +-
addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h | 2 +-
docs/guides/cpp_lang_guide.md | 2 +-
docs/guides/export_func_cls.rst | 2 +-
examples/quickstart/load/load_cpp.cc | 2 +-
examples/quickstart/load/load_cuda.cc | 2 +-
examples/stable_c_abi/src/load.c | 2 +-
include/tvm/ffi/extra/module.h | 11 +-
include/tvm/ffi/object.h | 6 +-
include/tvm/ffi/optional.h | 330 ++++++++++++++++++++++++--
rust/tvm-ffi/src/lib.rs | 2 +-
rust/tvm-ffi/src/optional.rs | 94 ++++++--
rust/tvm-ffi/tests/test_optional.rs | 53 ++---
src/ffi/extra/library_module.cc | 4 +-
src/ffi/extra/module.cc | 13 +-
src/ffi/extra/module_internal.h | 14 +-
tests/cpp/test_object.cc | 11 +-
tests/cpp/test_object_ptr.cc | 24 ++
tests/cpp/test_optional.cc | 46 +++-
tests/python/test_dataclass_gen_abi_cpp.py | 6 +-
20 files changed, 504 insertions(+), 126 deletions(-)
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
index f7f8e38c..1de989df 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
@@ -333,7 +333,7 @@ llvm::orc::JITDylib& ORCJITDynamicLibraryObj::GetJITDylib()
{
return *dylib_;
}
-Function ORCJITDynamicLibraryObj::GetFunction(const String& name) {
+Optional<Function> ORCJITDynamicLibraryObj::GetFunction(const String& name) {
// Pure symbol lookup. Context symbols were injected once at load time (see
// Finalize), so this holds no lock and does no refresh — the returned
// Function, once resolved, is invoked lock-free on the hot path.
@@ -345,7 +345,7 @@ Function ORCJITDynamicLibraryObj::GetFunction(const String&
name) {
auto* wrapper = new DylibFnContextWithModule{GetRef<Module>(this)};
return Function::FromExternC(wrapper, c_func,
DeleteDylibFnContextWithModule);
}
- return nullptr;
+ return std::nullopt;
}
//-------------------------------------
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
index 9ca8cc25..b458b127 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
@@ -63,7 +63,7 @@ class ORCJITDynamicLibraryObj : public ModuleObj {
const char* kind() const final { return "orcjit"; }
- Function GetFunction(const String& name) override;
+ Optional<Function> GetFunction(const String& name) override;
private:
/*!
diff --git a/docs/guides/cpp_lang_guide.md b/docs/guides/cpp_lang_guide.md
index b6e1c019..d964421c 100644
--- a/docs/guides/cpp_lang_guide.md
+++ b/docs/guides/cpp_lang_guide.md
@@ -336,7 +336,7 @@ The metadata contains:
ffi::Module mod = ffi::Module::LoadFromFile("path/to/export_lib.so");
// Get the function
-ffi::Function func = mod->GetFunction("add_one");
+ffi::Function func = mod->GetFunction("add_one").value();
// Query metadata (type schema information)
ffi::Optional<ffi::String> metadata = mod->GetFunctionMetadata("add_one");
diff --git a/docs/guides/export_func_cls.rst b/docs/guides/export_func_cls.rst
index 2d74ee2f..ac528450 100644
--- a/docs/guides/export_func_cls.rst
+++ b/docs/guides/export_func_cls.rst
@@ -101,7 +101,7 @@ library and retrieve functions by name:
namespace ffi = tvm::ffi;
ffi::Module mod = ffi::Module::LoadFromFile("path/to/library.so");
- ffi::Function func = mod->GetFunction("add_two");
+ ffi::Function func = mod->GetFunction("add_two").value();
int result = func(40).cast<int>(); // -> 42
diff --git a/examples/quickstart/load/load_cpp.cc
b/examples/quickstart/load/load_cpp.cc
index 5278c73c..cee261ed 100644
--- a/examples/quickstart/load/load_cpp.cc
+++ b/examples/quickstart/load/load_cpp.cc
@@ -32,7 +32,7 @@ void Run(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
// Load shared library `build/add_one_cpu.so`
ffi::Module mod = ffi::Module::LoadFromFile("build/add_one_cpu.so");
// Look up `add_one_cpu` function
- ffi::Function add_one_cpu = mod->GetFunction("add_one_cpu");
+ ffi::Function add_one_cpu = mod->GetFunction("add_one_cpu").value();
// Call the function
add_one_cpu(x, y);
}
diff --git a/examples/quickstart/load/load_cuda.cc
b/examples/quickstart/load/load_cuda.cc
index db83d06e..07e43ffa 100644
--- a/examples/quickstart/load/load_cuda.cc
+++ b/examples/quickstart/load/load_cuda.cc
@@ -32,7 +32,7 @@ void Run(tvm::ffi::TensorView x, tvm::ffi::TensorView y) {
// Load shared library `build/add_one_cuda.so`
ffi::Module mod = ffi::Module::LoadFromFile("build/add_one_cuda.so");
// Look up `add_one_cuda` function
- ffi::Function add_one_cuda = mod->GetFunction("add_one_cuda");
+ ffi::Function add_one_cuda = mod->GetFunction("add_one_cuda").value();
// Call the function with CUDA tensors
add_one_cuda(x, y);
}
diff --git a/examples/stable_c_abi/src/load.c b/examples/stable_c_abi/src/load.c
index b384a4e4..5f207e90 100644
--- a/examples/stable_c_abi/src/load.c
+++ b/examples/stable_c_abi/src/load.c
@@ -45,7 +45,7 @@ int Run(DLTensor* x, DLTensor* y) {
// Step 2. Get function `add_one_cpu` from module
// Equivalent to:
- // func = mod->GetFunction("add_one_cpu", /*query_imports=*/false)
+ // func = mod->GetFunction("add_one_cpu", /*query_imports=*/false).value()
call_args[0] = (TVMFFIAny){.type_index = mod.type_index, .v_obj = mod.v_obj};
call_args[1] = (TVMFFIAny){.type_index = kTVMFFIRawStr, .v_c_str =
"add_one_cpu"};
call_args[2] = (TVMFFIAny){.type_index = kTVMFFIBool, .v_int64 = 0};
diff --git a/include/tvm/ffi/extra/module.h b/include/tvm/ffi/extra/module.h
index d7b07c19..8a824a3e 100644
--- a/include/tvm/ffi/extra/module.h
+++ b/include/tvm/ffi/extra/module.h
@@ -57,10 +57,9 @@ class TVM_FFI_EXTRA_CXX_API ModuleObj : public Object {
/*!
* \brief Get a ffi::Function from the module.
* \param name The name of the function.
- * \return The function, or nullptr if it is not found.
- * \note The nullable Function return keeps this virtual interface
pointer-sized.
+ * \return The function.
*/
- virtual Function GetFunction(const String& name) = 0;
+ virtual Optional<Function> GetFunction(const String& name) = 0;
/*!
* \brief Returns true if this module has a definition for a function of \p
name.
*
@@ -71,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) != nullptr; }
+ 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.
@@ -143,9 +142,9 @@ class TVM_FFI_EXTRA_CXX_API ModuleObj : public Object {
* \brief Overloaded function to optionally query from imports.
* \param name The name of the function.
* \param query_imports Whether to query imported modules.
- * \return The function, or nullptr if it is not found.
+ * \return The function.
*/
- Function GetFunction(const String& name, bool query_imports);
+ Optional<Function> GetFunction(const String& name, bool query_imports);
/*!
* \brief Overloaded function to optionally query from imports.
* \param name The name of the function.
diff --git a/include/tvm/ffi/object.h b/include/tvm/ffi/object.h
index 25ee7699..988a291a 100644
--- a/include/tvm/ffi/object.h
+++ b/include/tvm/ffi/object.h
@@ -780,9 +780,9 @@ class WeakObjectPtr {
* \brief Optional data type in FFI.
* \tparam T The underlying type of the optional.
*
- * \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>.
+ * \note ObjectRef, ObjectPtr, and Arc values use a nullable ObjectPtr-backed
+ * representation. Other storage-enabled T use one TVMFFIAny, while
+ * non-storage-enabled T fall 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 c79d5328..8f46b991 100644
--- a/include/tvm/ffi/optional.h
+++ b/include/tvm/ffi/optional.h
@@ -20,12 +20,11 @@
/*!
* \file tvm/ffi/optional.h
* \brief Runtime Optional container types.
- * \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>.
+ * \note Optional<T> uses a hybrid representation. ObjectRef, ObjectPtr, and
Arc
+ * values keep the established one-pointer ObjectPtr representation, with
+ * nullptr representing nullopt. Other types that enable Any storage are
+ * backed by one TVMFFIAny. Types that do not enable storage (for
example,
+ * non-owning view types) fall back to std::optional<T>.
*/
#ifndef TVM_FFI_OPTIONAL_H_
#define TVM_FFI_OPTIONAL_H_
@@ -50,13 +49,37 @@ inline constexpr bool is_optional_type_v = false;
template <typename T>
inline constexpr bool is_optional_type_v<Optional<T>> = true;
+
+// ObjectRef values have historically used their nullable ObjectPtr storage
+// directly. Keep nested Optional<Optional<T>> out of this specialization so
+// the outer Optional still has a distinct Any-backed representation.
+template <typename T>
+inline constexpr bool use_object_ref_optional_v =
+ std::is_base_of_v<ObjectRef, T> && !is_optional_type_v<T>;
+
+template <typename T>
+inline constexpr bool is_object_ptr_type_v = false;
+
+template <typename TObject>
+inline constexpr bool is_object_ptr_type_v<ObjectPtr<TObject>> = true;
+
+template <typename T>
+inline constexpr bool is_arc_type_v = false;
+
+template <typename TObject>
+inline constexpr bool is_arc_type_v<Arc<TObject>> = true;
+
+template <typename T>
+inline constexpr bool use_object_ptr_optional_v =
+ use_object_ref_optional_v<T> || is_object_ptr_type_v<T> ||
is_arc_type_v<T>;
/// \endcond
// 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<!TypeTraits<T>::storage_enabled>> {
+class Optional<T,
+ std::enable_if_t<!TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>>> {
public:
// default constructors.
Optional() = default;
@@ -149,7 +172,8 @@ class Optional<T,
std::enable_if_t<!TypeTraits<T>::storage_enabled>> {
* \tparam T The underlying value type (must enable Any storage).
*/
template <typename T>
-class Optional<T, std::enable_if_t<TypeTraits<T>::storage_enabled>> {
+class Optional<T,
+ std::enable_if_t<TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>>> {
public:
/*! \brief default constructor, represents nullopt (Any() is kTVMFFINone). */
Optional() = default;
@@ -331,19 +355,293 @@ class Optional<T,
std::enable_if_t<TypeTraits<T>::storage_enabled>> {
Any data_;
};
+/*!
+ * \brief Pointer-sized Optional specialization for ObjectRef types.
+ *
+ * ObjectRef already owns an ObjectPtr<Object>. Reusing that storage keeps the
+ * long-standing nullable-pointer ABI: nullptr is nullopt, and an engaged value
+ * is represented by the object's pointer with no TVMFFIAny wrapper.
+ */
+template <typename T>
+class Optional<T, std::enable_if_t<use_object_ref_optional_v<T>>> : public
ObjectRef {
+ public:
+ using ContainerType = typename T::ContainerType;
+ static constexpr bool _type_container_is_exact = T::_type_container_is_exact;
+
+ Optional() = default;
+ // NOLINTBEGIN(google-explicit-constructor)
+ Optional(const Optional&) = default;
+ Optional(Optional&&) noexcept = default;
+ explicit Optional(UnsafeInit tag) : ObjectRef(tag) {}
+ Optional(std::nullopt_t) {}
+ Optional(std::nullptr_t) {}
+ Optional(std::optional<T> other) {
+ if (other.has_value()) {
+ *this = *std::move(other);
+ }
+ }
+ Optional(T other) : ObjectRef(std::move(other)) {}
+ // NOLINTEND(google-explicit-constructor)
+
+ Optional& operator=(const Optional&) = default;
+ Optional& operator=(Optional&&) noexcept = default;
+
+ TVM_FFI_INLINE Optional& operator=(T other) {
+ ObjectRef::operator=(std::move(other));
+ return *this;
+ }
+
+ TVM_FFI_INLINE Optional& operator=(std::nullopt_t) {
+ data_ = nullptr;
+ return *this;
+ }
+
+ TVM_FFI_INLINE Optional& operator=(std::nullptr_t) {
+ data_ = nullptr;
+ return *this;
+ }
+
+ TVM_FFI_INLINE T value() const& {
+ if (TVM_FFI_PREDICT_FALSE(!has_value())) {
+ TVM_FFI_THROW(RuntimeError) << "Back optional access";
+ }
+ return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_);
+ }
+
+ TVM_FFI_INLINE T value() && {
+ if (TVM_FFI_PREDICT_FALSE(!has_value())) {
+ TVM_FFI_THROW(RuntimeError) << "Back optional access";
+ }
+ return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(std::move(data_));
+ }
+
+ template <typename U = std::remove_cv_t<T>>
+ TVM_FFI_INLINE T value_or(U&& default_value) const {
+ return has_value() ?
details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_)
+ : T(std::forward<U>(default_value));
+ }
+
+ TVM_FFI_INLINE explicit operator bool() const noexcept { return has_value();
}
+ TVM_FFI_INLINE bool has_value() const noexcept { return data_ != nullptr; }
+
+ TVM_FFI_INLINE T operator*() const& noexcept {
+ return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_);
+ }
+
+ TVM_FFI_INLINE T operator*() && noexcept {
+ return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(std::move(data_));
+ }
+
+ 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(); }
+
+ TVM_FFI_INLINE auto operator==(const Optional& other) const { return
EQToOptional(other); }
+ TVM_FFI_INLINE auto operator!=(const Optional& other) const { return
NEToOptional(other); }
+
+ TVM_FFI_INLINE auto operator==(const std::optional<T>& other) const {
+ return EQToOptional(other);
+ }
+ 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 (!has_value()) return RetType(false);
+ if (same_as(other)) return RetType(true);
+ return operator*() == other;
+ }
+
+ TVM_FFI_INLINE auto operator!=(const T& other) const { return !(*this ==
other); }
+
+ template <typename U>
+ TVM_FFI_INLINE auto operator==(const U& other) const {
+ using RetType = decltype(value() == other);
+ if (!has_value()) return RetType(false);
+ return operator*() == other;
+ }
+
+ template <typename U>
+ TVM_FFI_INLINE auto operator!=(const U& other) const {
+ using RetType = decltype(value() != other);
+ if (!has_value()) return RetType(true);
+ return operator*() != other;
+ }
+
+ TVM_FFI_INLINE const ContainerType* get() const {
+ return static_cast<ContainerType*>(data_.get());
+ }
+
+ private:
+ template <typename U>
+ TVM_FFI_INLINE auto EQToOptional(const U& other) const {
+ using RetType = decltype(operator*() == *other);
+ if (!has_value() || !other.has_value()) {
+ return RetType(has_value() == other.has_value());
+ }
+ if (same_as(*other)) return RetType(true);
+ return operator*() == *other;
+ }
+
+ template <typename U>
+ TVM_FFI_INLINE auto NEToOptional(const U& other) const {
+ using RetType = decltype(operator*() != *other);
+ if (!has_value() || !other.has_value()) {
+ return RetType(has_value() != other.has_value());
+ }
+ if (same_as(*other)) return RetType(false);
+ return operator*() != *other;
+ }
+};
+
+namespace details {
+
+template <typename T>
+struct OptionalObjectPtrTraits;
+
+template <typename TObject>
+struct OptionalObjectPtrTraits<ObjectPtr<TObject>> {
+ using ContainerType = TObject;
+ using StorageType = ObjectPtr<TObject>;
+
+ TVM_FFI_INLINE static ObjectPtr<TObject> Copy(const StorageType& value) {
return value; }
+ TVM_FFI_INLINE static ObjectPtr<TObject> Move(StorageType&& value) { return
std::move(value); }
+};
+
+template <typename TObject>
+struct OptionalObjectPtrTraits<Arc<TObject>> {
+ using ContainerType = TObject;
+ using StorageType = ObjectPtr<TObject>;
+
+ TVM_FFI_INLINE static Arc<TObject> Copy(const StorageType& value) {
+ return ObjectUnsafe::ArcFromObjectPtr(StorageType(value));
+ }
+ TVM_FFI_INLINE static Arc<TObject> Move(StorageType&& value) {
+ return ObjectUnsafe::ArcFromObjectPtr(std::move(value));
+ }
+};
+
+} // namespace details
+
+/*!
+ * \brief Pointer-sized Optional specialization for ObjectPtr and Arc values.
+ *
+ * Both pointer classes share ObjectPtr<T>'s one-pointer representation. Arc is
+ * non-null in its public API; Optional<Arc<T>> adds nullptr as the disengaged
+ * state while returning Arc<T> only after a presence check.
+ */
+template <typename T>
+class Optional<T, std::enable_if_t<is_object_ptr_type_v<T> ||
is_arc_type_v<T>>>
+ : public details::OptionalObjectPtrTraits<T>::StorageType {
+ private:
+ using Traits = details::OptionalObjectPtrTraits<T>;
+ using StorageType = typename Traits::StorageType;
+
+ public:
+ using ContainerType = typename Traits::ContainerType;
+
+ Optional() = default;
+ // NOLINTBEGIN(google-explicit-constructor)
+ Optional(const Optional&) = default;
+ Optional(Optional&&) noexcept = default;
+ Optional(std::nullopt_t) : StorageType(nullptr) {}
+ Optional(std::nullptr_t) : StorageType(nullptr) {}
+ Optional(std::optional<T> other) {
+ if (other.has_value()) {
+ static_cast<StorageType&>(*this) = StorageType(std::move(*other));
+ }
+ }
+ Optional(T value) : StorageType(std::move(value)) {}
+ // NOLINTEND(google-explicit-constructor)
+
+ Optional& operator=(const Optional&) = default;
+ Optional& operator=(Optional&&) noexcept = default;
+
+ TVM_FFI_INLINE Optional& operator=(T value) {
+ static_cast<StorageType&>(*this) = StorageType(std::move(value));
+ return *this;
+ }
+
+ TVM_FFI_INLINE Optional& operator=(std::nullopt_t) {
+ StorageType::reset();
+ return *this;
+ }
+
+ TVM_FFI_INLINE Optional& operator=(std::nullptr_t) {
+ StorageType::reset();
+ return *this;
+ }
+
+ TVM_FFI_INLINE T value() const& {
+ if (TVM_FFI_PREDICT_FALSE(!has_value())) {
+ TVM_FFI_THROW(RuntimeError) << "Back optional access";
+ }
+ return Traits::Copy(static_cast<const StorageType&>(*this));
+ }
+
+ TVM_FFI_INLINE T value() && {
+ if (TVM_FFI_PREDICT_FALSE(!has_value())) {
+ TVM_FFI_THROW(RuntimeError) << "Back optional access";
+ }
+ return Traits::Move(std::move(static_cast<StorageType&>(*this)));
+ }
+
+ template <typename U = T>
+ TVM_FFI_INLINE T value_or(U&& default_value) const {
+ return has_value() ? Traits::Copy(static_cast<const StorageType&>(*this))
+ : T(std::forward<U>(default_value));
+ }
+
+ TVM_FFI_INLINE explicit operator bool() const noexcept { return has_value();
}
+ TVM_FFI_INLINE bool has_value() const noexcept { return StorageType::get()
!= nullptr; }
+
+ TVM_FFI_INLINE T operator*() const& noexcept {
+ return Traits::Copy(static_cast<const StorageType&>(*this));
+ }
+
+ TVM_FFI_INLINE T operator*() && noexcept {
+ return Traits::Move(std::move(static_cast<StorageType&>(*this)));
+ }
+
+ 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(); }
+
+ TVM_FFI_INLINE bool operator==(const Optional& other) const noexcept {
+ return StorageType::get() == other.get();
+ }
+ TVM_FFI_INLINE bool operator!=(const Optional& other) const noexcept {
return !(*this == other); }
+ TVM_FFI_INLINE bool operator==(const T& other) const noexcept {
+ return StorageType::get() == other.get();
+ }
+ TVM_FFI_INLINE bool operator!=(const T& other) const noexcept { return
!(*this == other); }
+
+ TVM_FFI_INLINE bool same_as(const Optional& other) const noexcept {
+ return StorageType::get() == other.get();
+ }
+ TVM_FFI_INLINE bool same_as(const T& other) const noexcept {
+ return StorageType::get() == other.get();
+ }
+
+ using StorageType::get;
+ using StorageType::unique;
+ using StorageType::use_count;
+};
+
template <typename T>
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.
+ // Optional<T> can live in Any exactly when T can, independently of whether
+ // its in-memory representation is Any-backed or ObjectPtr-backed.
static constexpr bool storage_enabled = TypeTraits<T>::storage_enabled;
TVM_FFI_INLINE static void CopyToAnyView(const Optional<T>& src, TVMFFIAny*
result) {
- if constexpr (TypeTraits<T>::storage_enabled) {
- // Storage-enabled: the Any already holds the exact representation.
+ if constexpr (TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>) {
*result = src.ToAnyView().CopyToTVMFFIAny();
} else {
if (src.has_value()) {
@@ -355,7 +653,7 @@ struct TypeTraits<Optional<T>> : public TypeTraitsBase {
}
TVM_FFI_INLINE static void MoveToAny(Optional<T> src, TVMFFIAny* result) {
- if constexpr (TypeTraits<T>::storage_enabled) {
+ if constexpr (TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>) {
*result =
details::AnyUnsafe::MoveAnyToTVMFFIAny(std::move(src).MoveToAny());
} else {
if (src.has_value()) {
@@ -372,7 +670,7 @@ struct TypeTraits<Optional<T>> : public TypeTraitsBase {
}
TVM_FFI_INLINE static Optional<T> CopyFromAnyViewAfterCheck(const TVMFFIAny*
src) {
- if constexpr (TypeTraits<T>::storage_enabled) {
+ if constexpr (TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>) {
return Optional<T>(Any(AnyView::CopyFromTVMFFIAny(*src)));
} else {
if (src->type_index == TypeIndex::kTVMFFINone) return
Optional<T>(std::nullopt);
@@ -381,7 +679,7 @@ struct TypeTraits<Optional<T>> : public TypeTraitsBase {
}
TVM_FFI_INLINE static Optional<T> MoveFromAnyAfterCheck(TVMFFIAny* src) {
- if constexpr (TypeTraits<T>::storage_enabled) {
+ if constexpr (TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>) {
return Optional<T>(details::AnyUnsafe::MoveTVMFFIAnyToAny(src));
} else {
if (src->type_index == TypeIndex::kTVMFFINone) return
Optional<T>(std::nullopt);
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 86579857..03540b0f 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -54,7 +54,7 @@ pub use crate::extra::structural_visit::{
pub use crate::function::Function;
pub use crate::object::ObjectRefCast;
pub use crate::object::{Object, ObjectArc, ObjectCore,
ObjectCoreWithExtraItems, ObjectRefCore};
-pub use crate::optional::Optional;
+pub use crate::optional::{Optional, OptionalCompatible};
pub use crate::string::{Bytes, String};
pub use crate::type_traits::AnyCompatible;
pub use tvm_ffi_macros::{dispatch, match_any};
diff --git a/rust/tvm-ffi/src/optional.rs b/rust/tvm-ffi/src/optional.rs
index 33c85231..907127df 100644
--- a/rust/tvm-ffi/src/optional.rs
+++ b/rust/tvm-ffi/src/optional.rs
@@ -16,31 +16,77 @@
* specific language governing permissions and limitations
* under the License.
*/
-//! In-place mirror of C++ `ffi::Optional<T>` (`include/tvm/ffi/optional.h`).
+//! In-place mirror of C++ `ffi::Optional<T>` for non-object-pointer `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`.
+//! C++ `ffi::Optional<T>` is backed by one 16-byte `TVMFFIAny` for scalar,
+//! string, and other non-object-pointer values, with
+//! `type_index == kTVMFFINone` meaning `nullopt`. [`Optional<T>`] mirrors that
+//! representation.
//!
-//! [`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.
+//! ObjectRef, ObjectPtr, and Arc cases are intentionally excluded. Their C++
+//! optional is one nullable object pointer, which Rust's niche-optimized
+//! [`std::option::Option<X>`] already mirrors. Using `Optional<X>` for an
object
+//! class is rejected at compile time so the two ABI layouts cannot be
confused.
//!
-//! 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.
+//! `Optional<T>` is `#[repr(transparent)]` over [`Any`] and decodes the cell
in
+//! place. It is named `Optional` (not `Option`) to match the C++ type while
+//! keeping the pointer-backed object case visually distinct.
use crate::any::Any;
-use crate::string::String;
+use crate::string::{Bytes, String};
use crate::type_traits::AnyCompatible;
use std::fmt::{self, Debug};
use std::marker::PhantomData;
use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
+/// Marker for values whose C++ `ffi::Optional<T>` uses the 16-byte
+/// `TVMFFIAny` representation.
+///
+/// Object classes deliberately do not implement this trait. Use `Option<X>`
+/// for those values; it is the pointer-sized mirror of C++'s nullable
+/// `ObjectPtr`-backed optional.
+///
+/// ```compile_fail,E0277
+/// use tvm_ffi::{Array, Optional};
+/// let _ = Optional::<Array<i64>>::none();
+/// ```
+#[diagnostic::on_unimplemented(
+ message = "`Optional<{Self}>` only mirrors non-object-pointer
`ffi::Optional` values",
+ label = "`{Self}` uses the object-pointer optional representation",
+ note = "use `Option<{Self}>` for object classes; it is the compatible
nullable-pointer layout"
+)]
+pub unsafe trait OptionalCompatible: AnyCompatible {}
+
+macro_rules! impl_optional_compatible {
+ ($($t:ty),* $(,)?) => {
+ $(unsafe impl OptionalCompatible for $t {})*
+ };
+}
+
+impl_optional_compatible!(
+ bool,
+ i8,
+ i16,
+ i32,
+ i64,
+ isize,
+ u8,
+ u16,
+ u32,
+ u64,
+ usize,
+ f32,
+ f64,
+ *mut core::ffi::c_void,
+ crate::DLDataType,
+ crate::DLDevice,
+ String,
+ Bytes,
+);
+
+unsafe impl<T: OptionalCompatible> OptionalCompatible for Option<T> {}
+
/// In-place mirror of C++ `ffi::Optional<T>`: a single 16-byte `TVMFFIAny`
cell
/// (wrapped as [`Any`]) whose `type_index == kTVMFFINone` encodes `nullopt`.
///
@@ -48,7 +94,7 @@ use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
/// 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> {
+pub struct Optional<T: OptionalCompatible> {
// Holds either the value's `TVMFFIAny` representation or a `kTVMFFINone`
cell.
data: Any,
_marker: PhantomData<T>,
@@ -62,7 +108,7 @@ const _: () = assert!(
&& std::mem::align_of::<Optional<i64>>() ==
std::mem::align_of::<crate::TVMFFIAny>()
);
-impl<T: AnyCompatible> Optional<T> {
+impl<T: OptionalCompatible> Optional<T> {
/// An engaged optional holding `value`.
#[inline]
pub fn some(value: T) -> Self {
@@ -145,7 +191,7 @@ impl Optional<String> {
}
}
-impl<T: AnyCompatible> Default for Optional<T> {
+impl<T: OptionalCompatible> Default for Optional<T> {
/// `nullopt`, matching the C++ default constructor.
#[inline]
fn default() -> Self {
@@ -153,7 +199,7 @@ impl<T: AnyCompatible> Default for Optional<T> {
}
}
-impl<T: AnyCompatible> Clone for Optional<T> {
+impl<T: OptionalCompatible> Clone for Optional<T> {
#[inline]
fn clone(&self) -> Self {
Self {
@@ -164,16 +210,16 @@ impl<T: AnyCompatible> Clone for Optional<T> {
}
}
-impl<T: AnyCompatible + PartialEq> PartialEq for Optional<T> {
+impl<T: OptionalCompatible + 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: OptionalCompatible + Eq> Eq for Optional<T> {}
-impl<T: AnyCompatible> From<Option<T>> for Optional<T> {
+impl<T: OptionalCompatible> From<Option<T>> for Optional<T> {
#[inline]
fn from(value: Option<T>) -> Self {
match value {
@@ -183,14 +229,14 @@ impl<T: AnyCompatible> From<Option<T>> for Optional<T> {
}
}
-impl<T: AnyCompatible> From<Optional<T>> for Option<T> {
+impl<T: OptionalCompatible> From<Optional<T>> for Option<T> {
#[inline]
fn from(value: Optional<T>) -> Self {
value.into_option()
}
}
-impl<T: AnyCompatible + Debug> Debug for Optional<T> {
+impl<T: OptionalCompatible + Debug> Debug for Optional<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.get() {
Some(v) => write!(f, "Optional::Some({v:?})"),
diff --git a/rust/tvm-ffi/tests/test_optional.rs
b/rust/tvm-ffi/tests/test_optional.rs
index c90ca468..d69e7d4e 100644
--- a/rust/tvm-ffi/tests/test_optional.rs
+++ b/rust/tvm-ffi/tests/test_optional.rs
@@ -20,7 +20,7 @@ use tvm_ffi::*;
/// 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] {
+fn cell_image<T: OptionalCompatible>(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.
@@ -29,26 +29,42 @@ fn cell_image<T: AnyCompatible>(opt: &Optional<T>) -> [u8;
16] {
}
#[test]
-fn layout_is_uniform_16_bytes() {
- // Independent of `T`: every `Optional<T>` is the 16-byte `TVMFFIAny` cell.
+fn non_object_layout_is_uniform_16_bytes() {
+ // Every supported non-object 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>>>()
+ std::mem::align_of::<Optional<String>>()
);
}
+#[test]
+fn object_option_uses_nullable_pointer_layout() {
+ // Object classes intentionally use Rust Option<X>, not Optional<X>.
+ // ObjectArc is non-null, so Option<Array<_>> uses its null-pointer niche.
+ assert_eq!(
+ std::mem::size_of::<Option<Array<i64>>>(),
+ std::mem::size_of::<Array<i64>>()
+ );
+ assert_eq!(
+ std::mem::size_of::<Option<Array<i64>>>(),
+ std::mem::size_of::<*mut ()>()
+ );
+ let none: Option<Array<i64>> = None;
+ assert!(none.is_none());
+ let some = Some(Array::new(vec![1i64, 2, 3]));
+ assert_eq!(some.as_ref().expect("engaged").len(), 3);
+}
+
#[test]
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]
@@ -72,7 +88,7 @@ fn byte_image_some_int_matches_ffi_any() {
#[test]
fn pod_roundtrip_all_supported_types() {
- fn check<T: AnyCompatible + PartialEq + Copy + std::fmt::Debug>(val: T) {
+ fn check<T: OptionalCompatible + PartialEq + Copy + std::fmt::Debug>(val:
T) {
let ty = std::any::type_name::<T>();
let some = Optional::<T>::some(val);
assert!(some.has_value(), "engaged has_value for {ty}");
@@ -212,26 +228,3 @@ fn string_set_in_place() {
o.set(Some(String::from("x")));
assert_eq!(o.as_str(), Some("x"));
}
-
-#[test]
-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/library_module.cc b/src/ffi/extra/library_module.cc
index c69518eb..cc992aa2 100644
--- a/src/ffi/extra/library_module.cc
+++ b/src/ffi/extra/library_module.cc
@@ -42,7 +42,7 @@ class LibraryModuleObj final : public ModuleObj {
/*! \brief Get the property of the runtime module .*/
int GetPropertyMask() const final { return Module::kBinarySerializable |
Module::kRunnable; };
- ffi::Function GetFunction(const String& name) final {
+ Optional<ffi::Function> GetFunction(const String& name) final {
TVMFFISafeCallType faddr;
faddr =
reinterpret_cast<TVMFFISafeCallType>(lib_->GetSymbolWithSymbolPrefix(name));
// ensure the function keeps the Library Module alive
@@ -55,7 +55,7 @@ class LibraryModuleObj final : public ModuleObj {
args.size(),
reinterpret_cast<TVMFFIAny*>(rv)));
});
}
- return nullptr;
+ return std::nullopt;
}
Optional<String> GetFunctionMetadata(const String& name) final {
diff --git a/src/ffi/extra/module.cc b/src/ffi/extra/module.cc
index 9c254a12..31bb95bb 100644
--- a/src/ffi/extra/module.cc
+++ b/src/ffi/extra/module.cc
@@ -58,19 +58,18 @@ class ModuleGlobals {
std::mutex mutex_;
};
-Function ModuleObj::GetFunction(const String& name, bool query_imports) {
- if (Function func = this->GetFunction(name); func != nullptr) {
- return func;
+Optional<Function> ModuleObj::GetFunction(const String& name, bool
query_imports) {
+ if (auto opt_func = this->GetFunction(name)) {
+ return opt_func;
}
if (query_imports) {
for (const Any& import : imports_) {
- if (Function func = import.cast<Module>()->GetFunction(name,
query_imports);
- func != nullptr) {
- return func;
+ if (auto opt_func = import.cast<Module>()->GetFunction(name,
query_imports)) {
+ return *opt_func;
}
}
}
- return nullptr;
+ return std::nullopt;
}
Optional<String> ModuleObj::GetFunctionMetadata(const String& name, bool
query_imports) {
diff --git a/src/ffi/extra/module_internal.h b/src/ffi/extra/module_internal.h
index 1a5742ab..4519d9ad 100644
--- a/src/ffi/extra/module_internal.h
+++ b/src/ffi/extra/module_internal.h
@@ -76,21 +76,21 @@ struct ModuleObj::InternalUnsafe {
return const_cast<FunctionObj*>((*it).second.operator->());
}
- Function func = [&]() -> Function {
+ auto opt_func = [&]() -> std::optional<Function> {
for (const Any& import : module->imports_) {
- if (Function func = import.cast<Module>()->GetFunction(s_name, true);
func != nullptr) {
- return func;
+ if (auto opt_func = import.cast<Module>()->GetFunction(s_name, true)) {
+ return *opt_func;
}
}
// try global at last
- return tvm::ffi::Function::GetGlobal(s_name).value_or(nullptr);
+ return tvm::ffi::Function::GetGlobal(s_name);
}();
- if (func == nullptr) {
+ if (!opt_func.has_value()) {
TVM_FFI_THROW(RuntimeError) << "Cannot find function " << name
<< " in the imported modules or global
registry.";
}
- module->import_lookup_cache_.Set(s_name, func);
- return const_cast<FunctionObj*>(func.operator->());
+ module->import_lookup_cache_.Set(s_name, *opt_func);
+ return const_cast<FunctionObj*>((*opt_func).operator->());
}
static void RegisterReflection() {
diff --git a/tests/cpp/test_object.cc b/tests/cpp/test_object.cc
index b4e70359..0c4b066a 100644
--- a/tests/cpp/test_object.cc
+++ b/tests/cpp/test_object.cc
@@ -82,9 +82,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);
-// 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>>);
+// ObjectRef optionals retain the original ObjectRef/ObjectPtr representation
+// and continue to 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);
@@ -95,15 +95,14 @@ 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>);
-// 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<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>);
static_assert(object_ref_contains_v<TIntOrFloatRef, TFloatObj>);
static_assert(!object_ref_contains_v<TIntOrFloatRef, TNumberObj>);
static_assert(object_ref_contains_is_enabled_v<TInt, TIntObj>);
+static_assert(object_ref_contains_is_enabled_v<Optional<TInt>, TIntObj>);
static_assert(object_ref_contains_is_enabled_v<TIntOrFloatRef, TIntObj>);
static_assert(!object_ref_contains_is_enabled_v<int, TIntObj>);
static_assert(!object_ref_contains_is_enabled_v<TIntObj, TIntObj>);
diff --git a/tests/cpp/test_object_ptr.cc b/tests/cpp/test_object_ptr.cc
index 6e87ccc0..4f77fc7b 100644
--- a/tests/cpp/test_object_ptr.cc
+++ b/tests/cpp/test_object_ptr.cc
@@ -316,6 +316,13 @@ TEST(Arc, ConstructionOwnershipAndUpcast) {
TEST(Arc, AnyRoundTripAndSchemas) {
Arc<GeneratedDerivedObj> derived = make_arc<GeneratedDerivedObj>(7);
+ using BasePtr = ObjectPtr<GeneratedBaseObj>;
+ using BaseArc = Arc<GeneratedBaseObj>;
+ static_assert(sizeof(Optional<BasePtr>) == sizeof(BasePtr));
+ static_assert(alignof(Optional<BasePtr>) == alignof(BasePtr));
+ static_assert(sizeof(Optional<BaseArc>) == sizeof(BasePtr));
+ static_assert(alignof(Optional<BaseArc>) == alignof(BasePtr));
+
Any value = derived;
EXPECT_EQ(derived.use_count(), 2);
@@ -337,9 +344,26 @@ TEST(Arc, AnyRoundTripAndSchemas) {
EXPECT_EQ(TypeTraits<Optional<Arc<GeneratedBaseObj>>>::TypeSchema(),
R"({"type":"Optional","args":[{"type":"testing.GeneratedBase"}]})");
+ BasePtr ptr = derived;
+ Optional<BasePtr> optional_ptr = ptr;
+ ASSERT_TRUE(optional_ptr.has_value());
+ EXPECT_EQ(optional_ptr.get(), ptr.get());
+ Optional<BasePtr> ptr_roundtrip =
Any(optional_ptr).cast<Optional<BasePtr>>();
+ ASSERT_TRUE(ptr_roundtrip.has_value());
+ EXPECT_EQ(ptr_roundtrip.get(), ptr.get());
+ BasePtr moved_ptr = std::move(optional_ptr).value();
+ EXPECT_EQ(moved_ptr.get(), ptr.get());
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ EXPECT_FALSE(optional_ptr.has_value());
+
Optional<Arc<GeneratedBaseObj>> present = Arc<GeneratedBaseObj>(derived);
ASSERT_TRUE(present.has_value());
+ EXPECT_EQ(present.get(), static_cast<GeneratedBaseObj*>(derived.get()));
EXPECT_EQ(present.value().get(),
static_cast<GeneratedBaseObj*>(derived.get()));
+ Arc<GeneratedBaseObj> moved_arc = std::move(present).value();
+ EXPECT_EQ(moved_arc.get(), static_cast<GeneratedBaseObj*>(derived.get()));
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ EXPECT_FALSE(present.has_value());
Optional<Arc<GeneratedBaseObj>> absent = std::nullopt;
EXPECT_FALSE(Any(absent).cast<Optional<Arc<GeneratedBaseObj>>>().has_value());
}
diff --git a/tests/cpp/test_optional.cc b/tests/cpp/test_optional.cc
index e0a696e5..2b562a97 100644
--- a/tests/cpp/test_optional.cc
+++ b/tests/cpp/test_optional.cc
@@ -47,9 +47,12 @@
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).
+// ObjectRef optionals are pointer-backed, while an outer Optional<Optional<T>>
+// remains Any-backed because nested optionals are intentionally distinct.
+static_assert(sizeof(Optional<TInt>) == sizeof(ObjectPtr<Object>));
+static_assert(alignof(Optional<TInt>) == alignof(ObjectPtr<Object>));
+static_assert(sizeof(Optional<Function>) == sizeof(Function));
+static_assert(alignof(Optional<Function>) == alignof(Function));
static_assert(sizeof(Optional<Optional<int>>) == sizeof(Any));
static_assert(sizeof(Optional<Optional<TInt>>) == sizeof(Any));
static_assert(details::storage_enabled_v<Optional<int>>);
@@ -67,8 +70,7 @@ TEST(Optional, StorageEnabledPassThrough) {
TEST(Optional, TInt) {
Optional<TInt> x;
Optional<TInt> y = TInt(11);
- // Optional<T> is uniformly backed by a single Any (TVMFFIAny) regardless of
T.
- static_assert(sizeof(Optional<TInt>) == sizeof(Any));
+ static_assert(sizeof(Optional<TInt>) == sizeof(ObjectPtr<Object>));
EXPECT_TRUE(!x.has_value());
EXPECT_EQ(x.value_or(TInt(12))->value, 12);
@@ -88,6 +90,26 @@ TEST(Optional, TInt) {
EXPECT_EQ(y2.value_or(TInt(12))->value, 11);
}
+TEST(Optional, ObjectRefPointerABI) {
+ static_assert(sizeof(Optional<ObjectRef>) == sizeof(ObjectPtr<Object>));
+ static_assert(alignof(Optional<ObjectRef>) == alignof(ObjectPtr<Object>));
+
+ ObjectRef value = TInt(19);
+ Optional<ObjectRef> present = value;
+ ASSERT_TRUE(present.has_value());
+ EXPECT_TRUE(present.same_as(value));
+ EXPECT_EQ(present.use_count(), value.use_count());
+
+ Any encoded = present;
+ Optional<ObjectRef> decoded = encoded.cast<Optional<ObjectRef>>();
+ ASSERT_TRUE(decoded.has_value());
+ EXPECT_TRUE(decoded.same_as(value));
+
+ Optional<ObjectRef> absent = std::nullopt;
+ EXPECT_EQ(absent.get(), nullptr);
+ EXPECT_FALSE(Any(absent).cast<Optional<ObjectRef>>().has_value());
+}
+
TEST(Optional, double) {
Optional<double> x;
Optional<double> y = 11.0;
@@ -250,15 +272,13 @@ TEST(Optional, Bytes) {
static_assert(sizeof(Optional<Bytes>) == sizeof(Any));
}
-// 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.)
+// Non-object values keep the uniform TVMFFIAny representation. Object pointer
+// values are covered above by pointer-size ABI assertions.
template <typename... T>
-constexpr bool all_optional_layouts_uniform_v =
+constexpr bool all_non_object_optional_layouts_uniform_v =
((sizeof(Optional<T>) == sizeof(TVMFFIAny) && alignof(Optional<T>) ==
alignof(TVMFFIAny)) &&
...);
-static_assert(
- 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>);
+static_assert(all_non_object_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/python/test_dataclass_gen_abi_cpp.py
b/tests/python/test_dataclass_gen_abi_cpp.py
index 98345c51..56ee7e65 100644
--- a/tests/python/test_dataclass_gen_abi_cpp.py
+++ b/tests/python/test_dataclass_gen_abi_cpp.py
@@ -728,10 +728,10 @@ struct alignas(8) ObjectContainersObj : public
::tvm::ffi::Object {
::tvm::ffi::Dict<::tvm::ffi::String,
::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>> dictionary; //
offset=48, size=8, align=8
::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj> item; //
offset=56, size=8, align=8
::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj>
nullable_item; // offset=64, size=8, align=8
-
::tvm::ffi::Optional<::tvm::ffi::Arc<::testing::gen_abi_cpp::native::RawTensorObj>>
optional_item; // offset=72, size=16, align=8
+ ::tvm::ffi::ObjectPtr<::testing::gen_abi_cpp::native::RawTensorObj>
optional_item; // offset=72, size=8, align=8
};
-static_assert(sizeof(ObjectContainersObj) == 88);
+static_assert(sizeof(ObjectContainersObj) == 80);
static_assert(alignof(ObjectContainersObj) == 8);
static_assert(sizeof(decltype(ObjectContainersObj::array_items)) == 8);
static_assert(alignof(decltype(ObjectContainersObj::array_items)) == 8);
@@ -751,7 +751,7 @@ static_assert(offsetof(ObjectContainersObj, item) == 56);
static_assert(sizeof(decltype(ObjectContainersObj::nullable_item)) == 8);
static_assert(alignof(decltype(ObjectContainersObj::nullable_item)) == 8);
static_assert(offsetof(ObjectContainersObj, nullable_item) == 64);
-static_assert(sizeof(decltype(ObjectContainersObj::optional_item)) == 16);
+static_assert(sizeof(decltype(ObjectContainersObj::optional_item)) == 8);
static_assert(alignof(decltype(ObjectContainersObj::optional_item)) == 8);
static_assert(offsetof(ObjectContainersObj, optional_item) == 72);