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 c8be16ed [FEAT][RUST] Add AnyCompatible::to_any. (#712)
c8be16ed is described below

commit c8be16ed56d0fc25ec4261f16f440079aa3c5da7
Author: Linzhang Li <[email protected]>
AuthorDate: Wed Aug 26 19:33:43 2026 -0400

    [FEAT][RUST] Add AnyCompatible::to_any. (#712)
    
    ## Summary
    
    Add `AnyCompatible::to_any(&self) -> Any`, the by-reference counterpart
    of `Any::from(value)`.
    
    `From<AnyView> for Any` now dispatches inline instead of always calling
    `TVMFFIAnyViewToOwnedAny`: an object increfs, a self-contained cell is a
    bitwise copy, and only the three borrowed forms Rust never produces
    (`kTVMFFIRawStr`, `kTVMFFIByteArrayPtr`, `kTVMFFIObjectRValueRef`) still
    go to the runtime, behind `#[cold] #[inline(never)]`. This mirrors C++
    `details::InplaceConvertAnyViewToAny`. `is_plain_inline` moves from
    `extra::structural_common` into `any.rs`.
    
    ## Motivation
    
    `Any::from` takes ownership. A value reachable only through a shared
    reference — a field behind a `Deref` into object storage, e.g. `node.a`
    on a `&AddObj` — cannot be moved out, so every such site has to write
    `Any::from(node.a.clone())` today.
    
    `impl From<&T> for Any` cannot be added instead: `&` is fundamental, so
    a downstream crate may implement `AnyCompatible` for its own `&T` and
    the two impls overlap (E0119). A provided trait method has no such
    conflict, and every `AnyCompatible` type — including `Option<T>` and the
    containers — picks it up for free.
    
    ## Usage
    
    ```rust
    fn first_operand(node: &AddObj) -> Any {
        node.a.to_any() // was: Any::from(node.a.clone())
    }
    ```
    
    `to_any()` leaves the borrowed value usable and retains object-backed
    values by increfing them.
    
    ## Efficiency and Test
    
    `to_any` costs whatever `From<AnyView> for Any` costs, which no longer
    crosses the C ABI on every conversion. Where the type index is a
    compile-time constant (`i64`, `Array`, `Map`) the dispatch folds away
    and `to_any` matches `Any::from`. For `String`/`Bytes` and derived
    object refs the index is read at runtime, so the normalizing branch
    survives and the caller keeps a stack frame. That is deliberate: a type
    whose view is a borrowed representation — the Rust counterpart of C++
    `RValueRef<T>` — needs that branch to stay.
    
    The result is on par with `Any::from`: neither emits a call or an unwind
    path, and on the object path both do the same single `lock incq`.
    Verified on the release asm (x86-64, `-C codegen-units=1`).
    
    Tests in `tests/test_any.rs`: `to_any` matches `Any::from` across the
    scalar, small-string/bytes, and object representations and increfs
    exactly once (given back on drop); and it works on fields reached
    through `Deref` — `String`, `Option<String>`, `i64` — where `Any::from`
    does not compile.
    
    Docs: new "Converting Borrowed Values into `Any`" section in
    `docs/guides/rust_lang_guide.md`.
    
    ---------
    
    Signed-off-by: yuchuan <[email protected]>
---
 docs/guides/rust_lang_guide.md              | 20 +++++++
 rust/tvm-ffi/src/any.rs                     | 45 +++++++++++++--
 rust/tvm-ffi/src/extra/structural_common.rs |  9 +--
 rust/tvm-ffi/src/extra/structural_mutate.rs |  8 +--
 rust/tvm-ffi/src/type_traits.rs             | 13 +++++
 rust/tvm-ffi/tests/test_any.rs              | 90 +++++++++++++++++++++++++++++
 6 files changed, 168 insertions(+), 17 deletions(-)

diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 5ef960a4..cae1c758 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -161,6 +161,26 @@ assert_eq!(i64::try_from(result)?, 3);
 `Function::from_type_method(type_index, name)` performs the same lookup when
 the type index is already known (e.g. from `Any::type_index`).
 
+### Converting Borrowed Values into `Any`
+
+`Any::from(value)` takes ownership of `value`. Use it when you own the value.
+When you only have a shared reference—for example, a field accessed through
+`&AddObj`—you cannot move the field into an `Any`. Call
+`AnyCompatible::to_any()` instead:
+
+```rust
+use tvm_ffi::{Any, AnyCompatible};
+
+fn first_operand(node: &AddObj) -> Any {
+    node.a.to_any()
+}
+```
+
+`to_any()` creates an owned `Any` while leaving the borrowed value usable. For
+object-backed values, it retains the object by incrementing its reference
+count. It is equivalent to `Any::from(node.a.clone())`, without requiring an
+explicit clone at the call site.
+
 ### Type-Erased Functions
 
 Create functions from Rust closures:
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 1ebf017c..514c0048 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -62,8 +62,14 @@ impl<'a> AnyView<'a> {
     ///
     /// # Safety
     ///
-    /// The caller must keep every resource referenced by `data` alive for the
-    /// returned view's complete lifetime.
+    /// `data.type_index` must describe the payload, and the caller must keep
+    /// every resource it references alive for the view's complete lifetime.
+    ///
+    /// `kTVMFFIObjectRValueRef` carries a further obligation: `v_ptr` must
+    /// point to a writable slot the caller uniquely owns, holding one strong
+    /// reference. Owning the view takes that reference and writes null back
+    /// through the pointer, so the view must be converted at most once and no
+    /// other access to the slot may overlap the conversion.
     #[inline]
     pub(crate) unsafe fn from_raw_ffi_any(data: TVMFFIAny) -> Self {
         Self {
@@ -291,15 +297,42 @@ impl<'a> From<&'a Any> for AnyView<'a> {
     }
 }
 
+/// Whether a `TVMFFIAny` cell owns everything it holds, so its owning form is 
a
+/// bitwise copy. Mirrors C++ `details::InplaceConvertAnyViewToAny`.
+#[inline]
+pub(crate) fn is_plain_inline(type_index: i32) -> bool {
+    type_index < TypeIndex::kTVMFFIRawStr as i32
+        || type_index == TypeIndex::kTVMFFISmallStr as i32
+        || type_index == TypeIndex::kTVMFFISmallBytes as i32
+}
+
 // convert AnyView to Any
 impl From<AnyView<'_>> for Any {
     #[inline]
     fn from(value: AnyView<'_>) -> Self {
-        unsafe {
-            let mut data = TVMFFIAny::new();
-            crate::check_safe_call!(TVMFFIAnyViewToOwnedAny(&value.data, &mut 
data)).unwrap();
-            Self { data }
+        let data = value.data;
+        // Owning a borrowed object is the same incref `Any::clone` does below.
+        if data.type_index >= TypeIndex::kTVMFFIStaticObjectBegin as i32 {
+            unsafe { object::unsafe_::inc_ref(data.data_union.v_obj) };
+            return Self { data };
+        }
+        if is_plain_inline(data.type_index) {
+            return Self { data };
         }
+        // What is left borrows foreign storage and needs the runtime.
+        any_view_to_owned_via_runtime(data)
+    }
+}
+
+/// Out of line so the inlined conversions above stay leaf code, without the
+/// stack frame and unwind path this call needs.
+#[cold]
+#[inline(never)]
+fn any_view_to_owned_via_runtime(view: TVMFFIAny) -> Any {
+    unsafe {
+        let mut data = TVMFFIAny::new();
+        crate::check_safe_call!(TVMFFIAnyViewToOwnedAny(&view, &mut 
data)).unwrap();
+        Any { data }
     }
 }
 
diff --git a/rust/tvm-ffi/src/extra/structural_common.rs 
b/rust/tvm-ffi/src/extra/structural_common.rs
index de4d3f15..7b52efc1 100644
--- a/rust/tvm-ffi/src/extra/structural_common.rs
+++ b/rust/tvm-ffi/src/extra/structural_common.rs
@@ -139,7 +139,7 @@ impl StructuralValue {
 /// returns `None` instead of constructing an invalid owning value.
 #[inline]
 pub(crate) fn try_to_owned_without_normalization(raw: TVMFFIAny) -> 
Option<Any> {
-    if is_plain_inline_leaf(raw.type_index) {
+    if is_plain_inline(raw.type_index) {
         return Some(unsafe { Any::from_raw_ffi_any(raw) });
     }
     if raw.type_index >= TVMFFITypeIndex::kTVMFFIStaticObjectBegin as i32 {
@@ -153,12 +153,7 @@ pub(crate) fn try_to_owned_without_normalization(raw: 
TVMFFIAny) -> Option<Any>
     None
 }
 
-#[inline]
-pub(crate) fn is_plain_inline_leaf(type_index: i32) -> bool {
-    type_index < TVMFFITypeIndex::kTVMFFIRawStr as i32
-        || type_index == TVMFFITypeIndex::kTVMFFISmallStr as i32
-        || type_index == TVMFFITypeIndex::kTVMFFISmallBytes as i32
-}
+pub(crate) use crate::any::is_plain_inline;
 
 /// Subtype check with the base's inheritance depth supplied by the caller
 /// (`ObjectCore::TYPE_DEPTH`), so only the object's type info is fetched.
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 18dfa78b..1c6b0c3e 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -48,7 +48,7 @@ use crate::tvm_ffi_sys::{
 use crate::tvm_ffi_sys::{TVMFFIObjectHandle, TVMFFISEqHashKind};
 
 use super::structural_common::{
-    impl_callback_chain_tuple_arities, is_plain_inline_leaf, 
try_to_owned_without_normalization,
+    impl_callback_chain_tuple_arities, is_plain_inline, 
try_to_owned_without_normalization,
     with_structural_error_context,
 };
 use super::structural_visit::{
@@ -1114,17 +1114,17 @@ impl<D: MapDispatch> NativeMapper<D> {
         def_region_kind: DefRegionKind,
         permit: Permit,
     ) -> Result<Any> {
-        // Plain inline leaves have no children or structural identity.  Map
+        // Plain inline values have no children or structural identity.  Map
         // them directly instead of routing through identity lookup and the
         // default-mutation path, whose owning conversion crosses the C ABI.
         // Raw strings, byte-array views, and ObjectRValueRef are deliberately
         // excluded because converting those borrowed special values into an
         // Any performs normalization rather than a bitwise copy.
-        if is_plain_inline_leaf(raw.type_index) {
+        if is_plain_inline(raw.type_index) {
             let value = MapValue::from_raw(raw);
             return match self.dispatch.dispatch_map(&value, def_region_kind) {
                 Some(result) => result,
-                // SAFETY: `is_plain_inline_leaf` excludes every borrowed
+                // SAFETY: `is_plain_inline` excludes every borrowed
                 // representation that needs normalization.  These values own
                 // no external resource, so their owning form is the same
                 // bitwise TVMFFIAny value.
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index 9e7d0871..f5ff4540 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+use crate::any::{Any, AnyView};
 use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 use tvm_ffi_sys::{TVMFFIAny, TVMFFIGetTypeInfo};
 
@@ -68,6 +69,18 @@ pub unsafe trait AnyCompatible: Sized {
     }
     /// the type string of the type
     fn type_str() -> String;
+
+    /// Borrow `self` into an owned [`Any`], increfing object-backed values.
+    ///
+    /// The by-reference counterpart of `Any::from(value)`, for fields reached
+    /// through a `Deref` into shared object storage. `impl From<&T> for Any`
+    /// cannot be added instead: `&` is fundamental, so a downstream crate may
+    /// implement `AnyCompatible` for its own `&T` and the two impls overlap
+    /// (E0119).
+    #[inline]
+    fn to_any(&self) -> Any {
+        AnyView::from(self).into()
+    }
 }
 
 /// AnyCompatible for bool
diff --git a/rust/tvm-ffi/tests/test_any.rs b/rust/tvm-ffi/tests/test_any.rs
index b950419a..dfa2ea16 100644
--- a/rust/tvm-ffi/tests/test_any.rs
+++ b/rust/tvm-ffi/tests/test_any.rs
@@ -347,3 +347,93 @@ fn test_any_dl_device() {
     assert_eq!(converted_cuda_view.device_type, DLDeviceType::kDLCUDA);
     assert_eq!(converted_cuda_view.device_id, 1);
 }
+
+//---------------------------------------------------------------------------
+// AnyCompatible::to_any
+//---------------------------------------------------------------------------
+
+// Fields only reachable through `Deref`, where `Any::from` is not applicable.
+#[repr(C)]
+struct FieldHolderObj {
+    base: Object,
+    lhs: String,
+    rhs: Option<String>,
+    count: i64,
+}
+
+unsafe impl ObjectCore for FieldHolderObj {
+    const TYPE_KEY: &'static str = Object::TYPE_KEY;
+    const TYPE_DEPTH: i32 = Object::TYPE_DEPTH;
+    #[inline]
+    fn type_index() -> i32 {
+        Object::type_index()
+    }
+    #[inline]
+    unsafe fn object_header_mut(this: &mut Self) -> &mut TVMFFIObject {
+        Object::object_header_mut(&mut this.base)
+    }
+}
+
+#[test]
+fn test_to_any_matches_from_value() {
+    macro_rules! check {
+        ($value:expr, $ty:ty, $type_index:ident) => {{
+            let value: $ty = $value;
+            let any = value.to_any();
+            assert_eq!(any.type_index(), TypeIndex::$type_index as i32);
+            assert_eq!(any.type_index(), Any::from(value).type_index());
+            assert_eq!(any.try_as::<$ty>(), Some($value));
+            assert_eq!(any.debug_strong_count(), None);
+        }};
+    }
+
+    check!(-7i64, i64, kTVMFFIInt);
+    check!(true, bool, kTVMFFIBool);
+    check!(3.5f64, f64, kTVMFFIFloat);
+    check!(String::from("hello"), String, kTVMFFISmallStr);
+    check!(Bytes::from(&[1u8, 2, 3]), Bytes, kTVMFFISmallBytes);
+    assert_eq!(().to_any().type_index(), TypeIndex::kTVMFFINone as i32);
+    assert_eq!(
+        Option::<i64>::None.to_any().type_index(),
+        TypeIndex::kTVMFFINone as i32
+    );
+
+    // Object-backed: one incref, same as the `Any::from(x.clone())` it
+    // replaces, given back on drop.
+    let s = String::from("hello world this is a long string");
+    let any = s.to_any();
+    assert_eq!(any.type_index(), TypeIndex::kTVMFFIStr as i32);
+    assert_eq!(any.debug_strong_count(), Some(2));
+    assert_eq!(any.try_as::<String>().unwrap(), s);
+    drop(any);
+    assert_eq!(AnyView::from(&s).debug_strong_count(), Some(1));
+}
+
+/// The field is only reachable as a `&`, so `Any::from` cannot take it.
+#[test]
+fn test_to_any_from_borrowed_object_field() {
+    let lhs = String::from("hello world this is a long string");
+    let holder = ObjectArc::new(FieldHolderObj {
+        base: Object::new(),
+        lhs: lhs.clone(),
+        rhs: Some(lhs.clone()),
+        count: 11,
+    });
+    // `lhs` plus the two copies stored in the node.
+    assert_eq!(AnyView::from(&lhs).debug_strong_count(), Some(3));
+
+    let any = holder.lhs.to_any();
+    assert_eq!(any.debug_strong_count(), Some(4));
+    assert_eq!(any.try_as::<String>().unwrap(), lhs);
+
+    let any_opt = holder.rhs.to_any();
+    assert_eq!(
+        any_opt.try_as::<Option<String>>().unwrap(),
+        Some(lhs.clone())
+    );
+    assert_eq!(holder.count.to_any().try_as::<i64>(), Some(11));
+
+    drop(any);
+    drop(any_opt);
+    assert_eq!(AnyView::from(&lhs).debug_strong_count(), Some(3));
+}

Reply via email to