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 458aaf73 [FEAT][RUST]Add tvm_ffi::optional in Rust (#630)
458aaf73 is described below
commit 458aaf73b62285e5cd4481b9d197ac13b7c8cdb2
Author: Linzhang Li <[email protected]>
AuthorDate: Sat Jul 4 11:55:45 2026 -0400
[FEAT][RUST]Add tvm_ffi::optional in Rust (#630)
This PR supports `ffi::optionalPod`, `ffi::optionalStr` in `Rust`. It
keeps the same memory layout as C++'s ffi::optional.
---------
Signed-off-by: yuchuan <[email protected]>
---
rust/tvm-ffi/src/lib.rs | 1 +
rust/tvm-ffi/src/option.rs | 350 ++++++++++++++++++++++++++++++++++++
rust/tvm-ffi/src/string.rs | 17 ++
rust/tvm-ffi/tests/test_optional.rs | 220 +++++++++++++++++++++++
tests/cpp/test_optional.cc | 46 +++++
5 files changed, 634 insertions(+)
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 891f734e..a05e3c06 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -27,6 +27,7 @@ pub mod function;
pub mod function_internal;
pub mod macros;
pub mod object;
+pub mod option;
pub mod string;
pub mod type_traits;
pub use tvm_ffi_sys;
diff --git a/rust/tvm-ffi/src/option.rs b/rust/tvm-ffi/src/option.rs
new file mode 100644
index 00000000..d40e3ce9
--- /dev/null
+++ b/rust/tvm-ffi/src/option.rs
@@ -0,0 +1,350 @@
+/*
+ * 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/string.rs b/rust/tvm-ffi/src/string.rs
index 94739a4c..70768d6f 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -284,6 +284,23 @@ 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
new file mode 100644
index 00000000..18eadec0
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_optional.rs
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+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 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]
+#[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>()");
+}
+
+#[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>()");
+}
+
+/// 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) {
+ 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}");
+ 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}");
+ assert!(none.get().is_none(), "disengaged roundtrip for {ty}");
+ }
+ check::<bool>(true);
+ check::<i8>(0x12);
+ check::<i16>(0x1234);
+ check::<i32>(0x1234_5678);
+ check::<i64>(0x1122_3344_5566_7788);
+ check::<u8>(0xAB);
+ check::<u16>(0xABCD);
+ check::<u32>(0xABCD_EF01);
+ check::<u64>(0xABCD_EF01_2345_6789);
+ check::<f32>(1.5);
+ check::<f64>(2.5);
+}
+
+#[test]
+fn roundtrip_get_set() {
+ let mut o = OptionPod::<i32>::some(42);
+ assert_eq!(o.get(), Some(42));
+ assert!(o.has_value());
+
+ o.set(None);
+ assert_eq!(o.get(), None);
+ assert!(o.is_none());
+
+ o.set(Some(-7));
+ assert_eq!(o.get(), Some(-7));
+}
+
+#[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!(back, Some(9));
+ assert_eq!(OptionPod::<u8>::default().get(), None);
+ assert_eq!(OptionPod::<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);
+ assert_eq!(some.clone().get(), Some(123));
+ let none = OptionPod::<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"));
+ 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
+}
+
+#[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");
+ 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() {
+ // small (inline) string
+ let s = OptionStr::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();
+ 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();
+ assert_eq!(from_some.as_str(), Some("x"));
+ let back: Option<String> = OptionStr::none().into();
+ assert!(back.is_none());
+ assert!(OptionStr::default().is_none());
+}
+
+#[test]
+fn optional_str_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 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"));
+ // both drop here: two dec_refs balancing the clone's inc_ref, no leak/UAF.
+}
+
+#[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"));
+ 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);
+
+ o.set(Some(String::from("x")));
+ assert_eq!(o.as_str(), Some("x"));
+}
+
+#[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;
+ assert!(none.is_none());
+}
diff --git a/tests/cpp/test_optional.cc b/tests/cpp/test_optional.cc
index a9a66629..f9dedd1d 100644
--- a/tests/cpp/test_optional.cc
+++ b/tests/cpp/test_optional.cc
@@ -22,6 +22,9 @@
#include <tvm/ffi/memory.h>
#include <tvm/ffi/optional.h>
+#include <cstdint>
+#include <cstring>
+
#include "./testing_object.h"
namespace {
@@ -202,4 +205,47 @@ TEST(Optional, Bytes) {
EXPECT_TRUE(opt_bytes != std::nullopt);
static_assert(sizeof(Optional<Bytes>) == sizeof(Bytes));
}
+
+// 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.
+template <typename... T>
+constexpr bool all_optional_layouts_match_rust_mirror_v =
+ ((sizeof(Optional<T>) == sizeof(T) + alignof(T) && alignof(Optional<T>) ==
alignof(T)) && ...);
+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);
+}
} // namespace