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));
+}