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 fc6835de [Rust] NativeFunction typed hooks, Display through the
runtime printer, reflection::type_key, EnumDef size check (#954)
fc6835de is described below
commit fc6835de6da9876841448337f55a5abc4e791888
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Nov 6 22:23:28 2026 -0500
[Rust] NativeFunction typed hooks, Display through the runtime printer,
reflection::type_key, EnumDef size check (#954)
Registries hold typed hooks (operator validators, type inference, type
attributes) as either an opaque pointer to a native C ABI function or a
packed `Function` registered in its place.
`tvm_ffi::reflection::NativeFunction<Args, R>` is such a hook.
- Reading: `unsafe fn from_any(Any)`. A native function carries its
signature only by declaration, so the caller vouches that it takes
`Args` and returns `R`, as its registering code declares. A value that
is neither an opaque pointer nor a `Function` is a `TypeError`.
- Calling is safe: `call` takes a tuple of borrowed objects, managed
references or `Any` values and returns `Result<R>`; a native function is
called directly with the argument addresses and a packed one through
`call_packed`, and an error returned by either becomes `Err`.
- Building: `From<F>` for a function item or a closure that captures
nothing, `Fn(A0, ...) -> Result<R>`, generates the C ABI adapter
(zero-sized and `Copy`, checked at compile time). As with C++ `noexcept`
hooks, failure is reported through the `Result`; a panic aborts rather
than unwind into C++.
- Storing: `Any::from(hook)` gives the opaque pointer (or the
`Function`) for a registry slot such as `TypeAttrDef::attr`; store it
only in a slot whose callers use exactly this signature.
`Any`, `AnyView`, `Array`, `List`, `Map` and every
`#[derive(ObjectRef)]` type now implement `Display` as their runtime
`repr` (`ffi.ReprPrint`), so values format with `{}`. `Debug` is
unchanged. This is a source-breaking change: a downstream
`#[derive(ObjectRef)]` type that already implements `Display` must add
`#[object_ref(skip_display)]` to keep its own implementation, as `Error`
does.
`reflection::type_key(type_index)` returns the registered type key of an
index, or `None` when no type has it.
`reflection::field_default::<T>(name)` returns the default value `T`
registers for its own field `name`, or `None` when the field is missing,
has no default, or its default comes from a factory.
`EnumDef::new` allocates each value as an `EnumObj`; it now also checks
the registered object size of the enum type, so an opaque Rust mirror of
a type whose objects have fields beyond `EnumObj` is a `TypeError`
instead of an undersized allocation.
---
rust/tvm-ffi-macros/src/object_macros.rs | 17 +-
rust/tvm-ffi/src/any.rs | 33 +++
rust/tvm-ffi/src/collections/array.rs | 7 +
rust/tvm-ffi/src/collections/list.rs | 7 +
rust/tvm-ffi/src/collections/map.rs | 7 +
rust/tvm-ffi/src/derive.rs | 4 +-
rust/tvm-ffi/src/error.rs | 2 +-
rust/tvm-ffi/src/object.rs | 10 +-
rust/tvm-ffi/src/reflection.rs | 9 +-
rust/tvm-ffi/src/reflection/accessor.rs | 89 +++++-
rust/tvm-ffi/src/reflection/enum_def.rs | 26 +-
rust/tvm-ffi/src/reflection/native_function.rs | 359 +++++++++++++++++++++++++
rust/tvm-ffi/src/type_traits.rs | 14 +
rust/tvm-ffi/tests/test_cast.rs | 13 +
rust/tvm-ffi/tests/test_object_reflection.rs | 51 +++-
15 files changed, 629 insertions(+), 19 deletions(-)
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs
b/rust/tvm-ffi-macros/src/object_macros.rs
index 2a7cdacf..8911e4c9 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -651,6 +651,17 @@ pub(crate) fn expand_object_ref(
}
});
}
+ // The runtime `repr`, as `AnyView` formats it.
+ if !options.skip_display {
+ expanded.extend(quote! {
+ impl ::core::fmt::Display for #struct_name {
+ fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) ->
::core::fmt::Result {
+ let object =
&**#tvm_ffi_crate::object::ObjectRefCore::data(self);
+
::core::fmt::Display::fmt(&#tvm_ffi_crate::AnyView::from_object(object), f)
+ }
+ }
+ });
+ }
// `ObjectRef` itself skips these: `any.rs` implements them for it.
if struct_name != "ObjectRef" {
expanded.extend(quote! {
@@ -705,6 +716,7 @@ struct ObjectRefOptions {
parent: Option<syn::Type>,
skip_deref: bool,
skip_debug: bool,
+ skip_display: bool,
skip_any_compatible: bool,
}
@@ -748,13 +760,16 @@ fn object_ref_options(derive_input: &DeriveInput) ->
ObjectRefOptions {
ObjectRefOption::Crate(path) => options.tvm_ffi_crate =
Some(path),
ObjectRefOption::Flag(flag) if flag == "skip_deref" =>
options.skip_deref = true,
ObjectRefOption::Flag(flag) if flag == "skip_debug" =>
options.skip_debug = true,
+ ObjectRefOption::Flag(flag) if flag == "skip_display" => {
+ options.skip_display = true
+ }
ObjectRefOption::Flag(flag) if flag == "skip_any_compatible"
=> {
options.skip_any_compatible = true
}
ObjectRefOption::Flag(flag) => abort!(
flag,
"unknown `object_ref` option; expected `parent = ...`,
`skip_deref`, \
- `skip_debug`, `skip_any_compatible` or `crate = ...`"
+ `skip_debug`, `skip_display`, `skip_any_compatible` or
`crate = ...`"
),
}
}
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 35afcd28..ac30bea5 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -393,6 +393,39 @@ impl<'a> From<&'a Any> for AnyView<'a> {
}
}
+/// Formats the value as its `repr`, the text the runtime printer gives in
+/// every language, such as `(1, 2)` for an array or `"hi"` for a string.
+/// Object references format the same way.
+///
+/// # Examples
+///
+/// ```
+/// use tvm_ffi::{AnyView, Array};
+///
+/// let xs = Array::from_vec(vec![1_i64, 2]);
+/// assert_eq!(AnyView::from(&xs).to_string(), "(1, 2)");
+/// assert_eq!(xs.to_string(), "(1, 2)");
+/// ```
+impl std::fmt::Display for AnyView<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let repr = crate::cached_global_func!("ffi.ReprPrint")
+ .call_packed(&[*self])
+ .and_then(crate::String::try_from);
+ match repr {
+ Ok(text) => f.write_str(text.as_str()),
+ // Formatting cannot fail, so a printer error falls back to the
type key.
+ Err(_) =>
f.write_str(&object::type_key_of_index(self.type_index())),
+ }
+ }
+}
+
+/// Formats the value as its `repr`; see [`AnyView`].
+impl std::fmt::Display for Any {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ std::fmt::Display::fmt(&AnyView::from(self), f)
+ }
+}
+
/// Whether a `TVMFFIAny` cell owns everything it holds, so its owning form is
a
/// bitwise copy.
#[inline]
diff --git a/rust/tvm-ffi/src/collections/array.rs
b/rust/tvm-ffi/src/collections/array.rs
index 344ebeeb..ddadee64 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -124,6 +124,13 @@ impl<T: ContainerElement + Clone> Debug for Array<T> {
}
}
+/// Formats the array as its `repr`; see [`AnyView`].
+impl<T: ContainerElement + Clone> std::fmt::Display for Array<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
std::fmt::Display::fmt(&AnyView::from_object(&**ObjectRefCore::data(self)), f)
+ }
+}
+
impl<T: ContainerElement + Clone> Default for Array<T> {
fn default() -> Self {
Self::new()
diff --git a/rust/tvm-ffi/src/collections/list.rs
b/rust/tvm-ffi/src/collections/list.rs
index c65a2ebc..e1650779 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -552,6 +552,13 @@ impl<T> Debug for List<T> {
}
}
+/// Formats the list as its `repr`; see [`AnyView`].
+impl<T: ContainerElement> std::fmt::Display for List<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
std::fmt::Display::fmt(&AnyView::from_object(&**ObjectRefCore::data(self)), f)
+ }
+}
+
impl<T: ContainerElement> FromIterator<T> for List<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let items: Vec<T> = iter.into_iter().collect();
diff --git a/rust/tvm-ffi/src/collections/map.rs
b/rust/tvm-ffi/src/collections/map.rs
index 2da14c0d..94278fdb 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -400,6 +400,13 @@ where
}
}
+/// Formats the map as its `repr`; see [`AnyView`].
+impl<K: ContainerElement, V: ContainerElement> std::fmt::Display for Map<K, V>
{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
std::fmt::Display::fmt(&AnyView::from_object(&**ObjectRefCore::data(self)), f)
+ }
+}
+
impl<K, V> FromIterator<(K, V)> for Map<K, V>
where
K: ContainerElement,
diff --git a/rust/tvm-ffi/src/derive.rs b/rust/tvm-ffi/src/derive.rs
index 7f89db37..38a837ea 100644
--- a/rust/tvm-ffi/src/derive.rs
+++ b/rust/tvm-ffi/src/derive.rs
@@ -28,7 +28,8 @@ pub use tvm_ffi_macros::Object;
/// [`ObjectRefAnyCompatible`](crate::ObjectRefAnyCompatible) (conversions to
/// and from [`Any`](crate::Any), in-place downcasts, typed function
/// arguments), the upcast to [`ObjectRef`](crate::object::ObjectRef),
-/// `Deref<Target = FooObj>` for field reads, `Debug`, and a private
+/// `Deref<Target = FooObj>` for field reads, `Debug`, `Display` (the runtime
+/// `repr`, as [`AnyView`](crate::AnyView) formats it), and a private
/// `fn from_object(object: FooObj) -> Self` for the type's constructors.
///
/// # Options
@@ -41,6 +42,7 @@ pub use tvm_ffi_macros::Object;
/// `unsafe_impl_object_ref_upcast!`.
/// * `skip_deref` - No `Deref`, for a reference whose object is internal.
/// * `skip_debug` - `Debug` is written by hand.
+/// * `skip_display` - `Display` is written by hand.
/// * `skip_any_compatible` - `ObjectRefAnyCompatible` is written by hand,
/// for a typed reference with a stricter check or fallback conversions.
/// * `crate = path` - Where the generated code finds tvm-ffi, as for
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index 25936be2..48f6cf58 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -66,7 +66,7 @@ pub struct ErrorObj {
/// An `Error` is `Send + Sync`, so a `Result` can cross threads.
#[repr(C)]
#[derive(Clone, ObjectRef)]
-#[object_ref(skip_deref, skip_debug)]
+#[object_ref(skip_deref, skip_debug, skip_display)]
pub struct Error {
data: ObjectArc<ErrorObj>,
}
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index 91d8f3e6..37f9fd0c 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -897,13 +897,9 @@ pub(crate) fn into_root_object_ref<S:
ObjectRefCore>(value: S) -> ObjectRef {
/// The registered type key of `type_index`, or `<type_index N>` when it has
none.
pub(crate) fn type_key_of_index(type_index: i32) -> String {
- unsafe {
- let info = TVMFFIGetTypeInfo(type_index);
- if info.is_null() {
- format!("<type_index {type_index}>")
- } else {
- (*info).type_key.as_str().to_string()
- }
+ match crate::reflection::type_key(type_index) {
+ Some(key) => key.to_string(),
+ None => format!("<type_index {type_index}>"),
}
}
diff --git a/rust/tvm-ffi/src/reflection.rs b/rust/tvm-ffi/src/reflection.rs
index 1483a577..10a95a9a 100644
--- a/rust/tvm-ffi/src/reflection.rs
+++ b/rust/tvm-ffi/src/reflection.rs
@@ -19,16 +19,21 @@
//! Object reflection: look up the methods, fields and type attributes of
//! registered types; register Rust object types with [`ObjectDef`], type
-//! attributes with [`TypeAttrDef`] and enum values with [`EnumDef`].
+//! attributes with [`TypeAttrDef`] and enum values with [`EnumDef`]; call the
+//! typed hooks that attributes hold with [`NativeFunction`].
mod access_path;
mod accessor;
pub(crate) mod enum_def;
+mod native_function;
mod registry;
mod unsafe_init;
pub use access_path::{AccessKind, AccessPath, AccessPathObj, AccessStep,
AccessStepObj};
-pub use accessor::{get_method, get_method_by_index, FieldGetter,
TypeAttrColumn};
+pub use accessor::{
+ field_default, get_method, get_method_by_index, type_key, FieldGetter,
TypeAttrColumn,
+};
pub use enum_def::{EnumAttrMap, EnumDef};
+pub use native_function::{NativeArgs, NativeFunction};
#[doc(hidden)]
pub use registry::register_object;
pub use registry::{FieldFlags, ObjectDef, SEqHashKind, ShallowCopy,
TypeAttrDef};
diff --git a/rust/tvm-ffi/src/reflection/accessor.rs
b/rust/tvm-ffi/src/reflection/accessor.rs
index fe72c130..2b1f111b 100644
--- a/rust/tvm-ffi/src/reflection/accessor.rs
+++ b/rust/tvm-ffi/src/reflection/accessor.rs
@@ -21,13 +21,15 @@
use std::ffi::c_void;
use std::ptr::NonNull;
+use std::sync::{PoisonError, RwLock};
use crate::tvm_ffi_sys::{
- TVMFFIAny, TVMFFIByteArray, TVMFFIFieldGetter, TVMFFIFieldInfo,
TVMFFIGetTypeAttrColumn,
- TVMFFIGetTypeInfo, TVMFFIObject, TVMFFITypeAttrColumn, TVMFFITypeIndex,
TVMFFITypeKeyToIndex,
+ TVMFFIAny, TVMFFIByteArray, TVMFFIFieldFlagBitMask, TVMFFIFieldGetter,
TVMFFIFieldInfo,
+ TVMFFIGetTypeAttrColumn, TVMFFIGetTypeInfo, TVMFFIObject,
TVMFFITypeAttrColumn,
+ TVMFFITypeIndex, TVMFFITypeInfo, TVMFFITypeKeyToIndex,
};
use crate::type_traits::AnyCompatible;
-use crate::{Any, AnyView, Error, Function, ObjectCore, Result, TYPE_ERROR};
+use crate::{Any, AnyView, Array, Error, Function, ObjectCore, Result,
TYPE_ERROR};
/// A registry-owned type-attribute column indexed by runtime type.
///
@@ -82,6 +84,87 @@ impl TypeAttrColumn {
}
}
+/// The type key registered for `type_index`, such as `"ffi.Array"`, or `None`
+/// if no type has that index.
+///
+/// ```
+/// use tvm_ffi::collections::array::ArrayObj;
+/// use tvm_ffi::{reflection, ObjectCore};
+///
+/// assert_eq!(reflection::type_key(ArrayObj::type_index()),
Some("ffi.Array"));
+/// assert_eq!(reflection::type_key(-1), None);
+/// ```
+pub fn type_key(type_index: i32) -> Option<&'static str> {
+ if !is_registered(type_index) {
+ return None;
+ }
+ // SAFETY: the index is registered, and type information lives as long as
+ // the process.
+ let info: &'static TVMFFITypeInfo = unsafe {
&*TVMFFIGetTypeInfo(type_index) };
+ Some(info.type_key.as_str())
+}
+
+/// The default value `T` registers for its field `name`, or `None` if `T`
+/// registers no such field, the field has no default, or its default comes
+/// from a factory.
+///
+/// Only fields `T` registers itself are searched, not those of its ancestors.
+pub fn field_default<T: ObjectCore>(name: &str) -> Option<Any> {
+ // SAFETY: `ObjectCore` types are registered, and type information lives
+ // as long as the process.
+ let info: &'static TVMFFITypeInfo = unsafe {
&*TVMFFIGetTypeInfo(T::type_index()) };
+ if info.fields.is_null() {
+ return None;
+ }
+ // SAFETY: a registered type lists `num_fields` initialized fields.
+ let fields = unsafe { std::slice::from_raw_parts(info.fields,
info.num_fields as usize) };
+ let field = fields.iter().find(|field| field.name.as_str() == name)?;
+ let has_default =
TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskHasDefault as i64;
+ let from_factory =
TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskDefaultFromFactory as i64;
+ if field.flags & has_default == 0 || field.flags & from_factory != 0 {
+ return None;
+ }
+ // SAFETY: the registry keeps the default alive; the view only borrows it
+ // for the copy.
+ Some(Any::from(unsafe {
+ AnyView::from_raw_ffi_any(field.default_value_or_factory)
+ }))
+}
+
+/// Whether a type is registered with index `type_index`. The registry aborts
+/// the process on a lookup of any other index, so the registered indices are
+/// listed once and again whenever an index is missing, as types register over
+/// time.
+fn is_registered(type_index: i32) -> bool {
+ static REGISTERED: RwLock<Vec<bool>> = RwLock::new(Vec::new());
+ let Ok(index) = usize::try_from(type_index) else {
+ return false;
+ };
+ let read = REGISTERED.read().unwrap_or_else(PoisonError::into_inner);
+ if read.get(index).copied().unwrap_or(false) {
+ return true;
+ }
+ drop(read);
+ let keys = crate::cached_global_func!("ffi.GetRegisteredTypeKeys")
+ .call_tuple(())
+ .and_then(Array::<crate::String>::try_from);
+ let mut registered =
REGISTERED.write().unwrap_or_else(PoisonError::into_inner);
+ for key in keys.iter().flat_map(|keys| keys.iter()) {
+ let mut key_index = 0;
+ // SAFETY: the byte array borrows `key` for the call.
+ let found = unsafe {
+ TVMFFITypeKeyToIndex(&TVMFFIByteArray::from_str(key.as_str()),
&mut key_index) == 0
+ };
+ if let (true, Ok(key_index)) = (found, usize::try_from(key_index)) {
+ if registered.len() <= key_index {
+ registered.resize(key_index + 1, false);
+ }
+ registered[key_index] = true;
+ }
+ }
+ registered.get(index).copied().unwrap_or(false)
+}
+
/// Look up a reflected method of a type by type key and method name.
///
/// Same as [`get_method_by_index`], but resolves `type_key` to a type index
diff --git a/rust/tvm-ffi/src/reflection/enum_def.rs
b/rust/tvm-ffi/src/reflection/enum_def.rs
index 9b39d056..d67049ab 100644
--- a/rust/tvm-ffi/src/reflection/enum_def.rs
+++ b/rust/tvm-ffi/src/reflection/enum_def.rs
@@ -85,8 +85,10 @@ impl<E: EnumRef> EnumDef<E> {
/// # Errors
///
/// Returns a `ValueError` if `E` already has a value named `name`, or a
- /// `TypeError` if `E` is not an enum type or is an integer enum
- /// (`ffi.IntEnum`), whose values name their own indices.
+ /// `TypeError` if `E` is not an enum type, is an integer enum
+ /// (`ffi.IntEnum`), whose values name their own indices, or registers an
+ /// object size other than that of [`EnumObj`]: an enum type whose objects
+ /// have fields of their own.
pub fn new(name: &str) -> Result<Self> {
const {
assert!(
@@ -104,6 +106,24 @@ impl<E: EnumRef> EnumDef<E> {
"",
));
}
+ // The assert above sees only the Rust mirror, which may be opaque: a
+ // registered object size must be exactly that of the `EnumObj`
+ // allocated below. A type registered without one (such as by
+ // `#[derive(Object)]`) is laid out as its Rust struct.
+ // SAFETY: the type is registered, and its information is static.
+ let metadata = unsafe { (*TVMFFIGetTypeInfo(type_index)).metadata };
+ // SAFETY: registered metadata is static.
+ let total_size = unsafe { metadata.as_ref() }.map_or(0, |metadata|
metadata.total_size);
+ if total_size != 0 && total_size as usize != size_of::<EnumObj>() {
+ return Err(Error::new(
+ TYPE_ERROR,
+ &format!(
+ "`{type_key}` objects have fields beyond `ffi.Enum`, so
Rust cannot \
+ register its values"
+ ),
+ "",
+ ));
+ }
let state = ensure_state::<E>()?;
let index = state.entries.len() as i64;
let key = String::from(name);
@@ -118,7 +138,7 @@ impl<E: EnumRef> EnumDef<E> {
));
}
let object = ObjectArc::into_raw(ObjectArc::new(EnumObj::new(index,
name)));
- // SAFETY: `E`'s object is an `EnumObj` with `E`'s type index (asserted
+ // SAFETY: `E`'s object is an `EnumObj` with `E`'s type index (checked
// above), and the object is not shared yet. The deleter stays that of
// `EnumObj`, which drops the same fields.
let value = unsafe {
diff --git a/rust/tvm-ffi/src/reflection/native_function.rs
b/rust/tvm-ffi/src/reflection/native_function.rs
new file mode 100644
index 00000000..baca00ba
--- /dev/null
+++ b/rust/tvm-ffi/src/reflection/native_function.rs
@@ -0,0 +1,359 @@
+/*
+ * 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.
+ */
+//! Typed hooks that registries hold: native functions with a packed fallback.
+
+use std::ffi::c_void;
+use std::marker::PhantomData;
+
+use crate::object::type_key_of_index;
+use crate::tvm_ffi_sys::{TVMFFIAny, TVMFFITypeIndex};
+use crate::type_traits::DowncastRefFromObject;
+use crate::{Any, AnyView, Error, Function, Result, TYPE_ERROR};
+
+/// A typed hook that a type attribute, an enum attribute or another registry
+/// slot holds: a native function, called directly through the C ABI, or a
+/// packed [`Function`] registered in its place.
+///
+/// `Args` is the tuple of argument types and `R` the result type, as in
+/// `Fn(Args) -> R`: a hook that checks a pair is a
+/// `NativeFunction<(&MyPairObj,)>`, and one that measures it a
+/// `NativeFunction<(&MyPairObj,), i64>`. An argument is a borrowed object
+/// (`&MyPairObj`), a borrowed managed reference (`&Array<T>`) or `&Any`, and a
+/// native function receives its address, which C++ declares as
+/// `const MyPairObj*`, `const Array<T>&` or `const Any&`.
+///
+/// A native function carries no signature at run time, so the slot declares
+/// it. Reading a hook with [`from_any`](Self::from_any) is `unsafe`, the
+/// caller vouching for the signature, and then calling it is safe. Building a
+/// hook from a Rust function with `into()` is safe; store it, through
+/// `Any::from`, only in a slot whose callers use exactly this signature.
+///
+/// # Examples
+///
+/// ```
+/// use tvm_ffi::derive::Object;
+/// use tvm_ffi::reflection::{NativeFunction, TypeAttrColumn, TypeAttrDef};
+/// use tvm_ffi::{Object, ObjectArc, ObjectCore, Result};
+///
+/// #[repr(C)]
+/// #[derive(Object)]
+/// #[type_key = "my_ext.MyPair"]
+/// #[type_register]
+/// pub struct MyPairObj {
+/// base_: Object,
+/// first: i64,
+/// second: i64,
+/// }
+///
+/// fn sum(pair: &MyPairObj) -> Result<i64> {
+/// Ok(pair.first + pair.second)
+/// }
+///
+/// let hook: NativeFunction<(&MyPairObj,), i64> = sum.into();
+/// TypeAttrDef::<MyPairObj>::new().attr("my_ext.sum", hook)?;
+///
+/// let stored =
TypeAttrColumn::new("my_ext.sum").unwrap().get(MyPairObj::type_index()).unwrap();
+/// // SAFETY: `my_ext.sum` holds `i64(const MyPairObj*)` hooks.
+/// let sum: NativeFunction<(&MyPairObj,), i64> = unsafe {
NativeFunction::from_any(stored)? };
+/// let pair = ObjectArc::new(MyPairObj { base_: Object::new(), first: 1,
second: 2 });
+/// assert_eq!(sum.call((&*pair,))?, 3);
+/// # Ok::<(), tvm_ffi::Error>(())
+/// ```
+pub struct NativeFunction<Args, R = ()> {
+ // A non-null opaque pointer to the native function, or a `Function`.
+ data: Any,
+ // Covariant in `Args`: a hook that takes `&'a T` takes any shorter borrow.
+ signature: PhantomData<fn() -> (Args, R)>,
+}
+
+impl<Args: NativeArgs, R: TryFrom<Any>> NativeFunction<Args, R> {
+ /// Call the hook with `args`.
+ ///
+ /// # Arguments
+ ///
+ /// * `args` - The arguments, as a tuple such as `(&array,)`.
+ ///
+ /// # Returns
+ ///
+ /// The hook's result.
+ ///
+ /// # Errors
+ ///
+ /// Returns the error the hook raises, or a `TypeError` when a packed
+ /// function returns a value that does not convert to `R`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if an object argument is not managed, such as an object built
+ /// on the stack: the hook may retain it.
+ ///
+ /// See also [`NativeFunction`].
+ pub fn call(&self, args: Args) -> Result<R> {
+ let raw = self.data.as_raw_ffi_any();
+ let result = if raw.type_index == TVMFFITypeIndex::kTVMFFIOpaquePtr as
i32 {
+ // SAFETY: `from_any`'s caller vouches that the native function
+ // takes `Args` and returns `R`.
+ unsafe { args.call_native(raw.data_union.v_ptr) }
+ } else {
+ let function = self.data.downcast_ref::<Function>();
+ args.call_packed(function.expect("a hook is a native or packed
function"))?
+ };
+ // A native function returns its error as its result.
+ if result.type_index() == TVMFFITypeIndex::kTVMFFIError as i32 {
+ return Err(result.try_into()?);
+ }
+ R::try_from(result)
+ .map_err(|_| Error::new(TYPE_ERROR, "Packed hook result type
mismatch", ""))
+ }
+}
+
+impl<Args, R> Clone for NativeFunction<Args, R> {
+ fn clone(&self) -> Self {
+ Self {
+ data: self.data.clone(),
+ signature: PhantomData,
+ }
+ }
+}
+
+impl<Args, R> NativeFunction<Args, R> {
+ /// Read the hook that `value` holds: a non-null opaque pointer to a native
+ /// function, or a packed [`Function`].
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The hook, as its registry slot holds it.
+ ///
+ /// # Returns
+ ///
+ /// The hook, typed with `Args` and `R`.
+ ///
+ /// # Errors
+ ///
+ /// Returns a `TypeError` if `value` is neither a non-null opaque pointer
+ /// nor a [`Function`].
+ ///
+ /// # Safety
+ ///
+ /// A native function in `value` must take `Args` and return `R`, as the
+ /// code that registers it in the slot declares. A packed [`Function`] is
+ /// called through checked conversions and has no such requirement.
+ ///
+ /// See also [`NativeFunction`].
+ pub unsafe fn from_any(value: Any) -> Result<Self> {
+ let raw = value.as_raw_ffi_any();
+ // SAFETY: `v_ptr` is the payload of an opaque pointer.
+ let native = raw.type_index == TVMFFITypeIndex::kTVMFFIOpaquePtr as i32
+ && !unsafe { raw.data_union.v_ptr }.is_null();
+ if !native && raw.type_index != TVMFFITypeIndex::kTVMFFIFunction as
i32 {
+ let message = format!(
+ "Cannot convert from type `{}` to `NativeFunction`",
+ type_key_of_index(raw.type_index)
+ );
+ return Err(Error::new(TYPE_ERROR, &message, ""));
+ }
+ Ok(Self {
+ data: value,
+ signature: PhantomData,
+ })
+ }
+}
+
+/// Stores the hook in a registry slot, such as with
+/// [`TypeAttrDef::attr`](super::TypeAttrDef::attr): the opaque pointer of a
+/// native function, or the packed [`Function`]. Store it only in a slot whose
+/// callers call it with exactly `Args` and `R`, which native callers do not
+/// check.
+impl<Args, R> From<NativeFunction<Args, R>> for Any {
+ fn from(hook: NativeFunction<Args, R>) -> Self {
+ hook.data
+ }
+}
+
+/// The argument tuples a [`NativeFunction`] takes: up to eight borrowed
+/// objects, managed references or [`Any`] values.
+///
+/// This trait is sealed.
+pub trait NativeArgs: sealed::NativeArgs {}
+
+impl<T: sealed::NativeArgs> NativeArgs for T {}
+
+mod sealed {
+ use super::*;
+
+ pub trait NativeArgs {
+ /// Call the native function `function` with these arguments.
+ ///
+ /// # Safety
+ ///
+ /// `function` is an `extern "C" fn(Args...) -> TVMFFIAny`.
+ unsafe fn call_native(self, function: *mut c_void) -> Any;
+
+ fn call_packed(self, function: &Function) -> Result<Any>;
+ }
+
+ /// An argument passed to a native function as itself: a pointer.
+ pub trait NativeArg: Copy {
+ /// The pointee, which a native function receives the address of.
+ type Target: 'static;
+
+ /// The argument as a packed function receives it.
+ fn to_any_view(&self) -> AnyView<'_>;
+ }
+
+ impl<T: DowncastRefFromObject + 'static> NativeArg for &T {
+ type Target = T;
+
+ #[inline(always)]
+ fn to_any_view(&self) -> AnyView<'_> {
+ // Panics on an unmanaged object.
+ AnyView::from_object(self.as_object())
+ }
+ }
+
+ impl NativeArg for &Any {
+ type Target = Any;
+
+ #[inline(always)]
+ fn to_any_view(&self) -> AnyView<'_> {
+ AnyView::from(*self)
+ }
+ }
+
+ /// The only value of the zero-sized `F`, for the adapter, which cannot
+ /// receive it.
+ ///
+ /// # Safety
+ ///
+ /// A value of `F` exists. `F` is `Copy`, so it has no drop and may be
+ /// duplicated: a zero-sized value that must stay unique, such as a token,
+ /// is not `Copy`.
+ #[inline(always)]
+ unsafe fn conjure<F: Copy>() -> F {
+ const {
+ assert!(
+ size_of::<F>() == 0,
+ "a native hook is built from a function or a closure that
captures nothing"
+ )
+ };
+ // SAFETY: a zero-sized type has no bytes, so any value is initialized,
+ // and duplicating it is a copy (see above).
+ unsafe { std::mem::zeroed() }
+ }
+
+ /// The result of a native function as its caller owns it: the value, or
+ /// the error.
+ #[inline(always)]
+ fn into_raw<R: Into<Any>>(result: Result<R>) -> TVMFFIAny {
+ Any::into_raw_ffi_any(match result {
+ Ok(value) => value.into(),
+ Err(error) => Any::from(error),
+ })
+ }
+
+ macro_rules! impl_native_args {
+ ($($arg:ident $index:tt),*) => {
+ impl<$($arg: NativeArg),*> NativeArgs for ($($arg,)*) {
+ #[inline(always)]
+ unsafe fn call_native(self, function: *mut c_void) -> Any {
+ // The function may retain an object argument, which must
be managed.
+ $(let _ = self.$index.to_any_view();)*
+ // SAFETY: the caller passes a function of this signature,
+ // which returns an owned value.
+ unsafe {
+ let function = std::mem::transmute::<
+ *mut c_void,
+ unsafe extern "C" fn($($arg),*) -> TVMFFIAny,
+ >(function);
+ Any::from_raw_ffi_any(function($(self.$index),*))
+ }
+ }
+
+ #[inline(always)]
+ fn call_packed(self, function: &Function) -> Result<Any> {
+ function.call_packed(&[$(self.$index.to_any_view()),*])
+ }
+ }
+ };
+ }
+
+ macro_rules! impl_native_fn {
+ ($($arg:ident $raw:ident),*) => {
+ /// Build a native hook from a function item or a closure that
+ /// captures nothing, with the C ABI adapter that native callers
+ /// call.
+ ///
+ /// The function takes the arguments as `call` passes them and
+ /// returns `Result<R>`; its error is the error the hook raises. A
+ /// hook reports failure through its `Result` and must not panic: a
+ /// panic aborts the process. A closure that captures a value does
+ /// not compile.
+ ///
+ /// See also [`NativeFunction`].
+ // The function takes borrows of any lifetime, so it cannot keep an
+ // argument past the call.
+ impl<F, $($arg: NativeArg,)* R: Into<Any>> From<F> for
NativeFunction<($($arg,)*), R>
+ where
+ F: for<'a> Fn($(&'a $arg::Target),*) -> Result<R> + Copy +
'static,
+ {
+ fn from(_function: F) -> Self {
+ extern "C" fn adapter<F, $($arg: NativeArg,)* R:
Into<Any>>(
+ $($raw: *const $arg::Target),*
+ ) -> TVMFFIAny
+ where
+ F: for<'a> Fn($(&'a $arg::Target),*) -> Result<R> +
Copy + 'static,
+ {
+ // SAFETY: `from` received a value of `F`.
+ let function = unsafe { conjure::<F>() };
+ // SAFETY: a native caller passes the address of a live
+ // argument for the duration of the call, or null.
+ $(let Some($raw) = (unsafe { $raw.as_ref() }) else {
+ let message = "A native hook argument is null";
+ return into_raw::<R>(Err(Error::new(TYPE_ERROR,
message, "")));
+ };)*
+ // A panic aborts here rather than unwind into the
caller.
+ into_raw(function($($raw),*))
+ }
+ let adapter = adapter::<F, $($arg,)* R> as *mut c_void;
+ Self { data: Any::from(adapter), signature: PhantomData }
+ }
+ }
+ };
+ }
+
+ impl_native_fn!();
+ impl_native_fn!(A0 a0);
+ impl_native_fn!(A0 a0, A1 a1);
+ impl_native_fn!(A0 a0, A1 a1, A2 a2);
+ impl_native_fn!(A0 a0, A1 a1, A2 a2, A3 a3);
+ impl_native_fn!(A0 a0, A1 a1, A2 a2, A3 a3, A4 a4);
+ impl_native_fn!(A0 a0, A1 a1, A2 a2, A3 a3, A4 a4, A5 a5);
+ impl_native_fn!(A0 a0, A1 a1, A2 a2, A3 a3, A4 a4, A5 a5, A6 a6);
+ impl_native_fn!(A0 a0, A1 a1, A2 a2, A3 a3, A4 a4, A5 a5, A6 a6, A7 a7);
+
+ impl_native_args!();
+ impl_native_args!(A0 0);
+ impl_native_args!(A0 0, A1 1);
+ impl_native_args!(A0 0, A1 1, A2 2);
+ impl_native_args!(A0 0, A1 1, A2 2, A3 3);
+ impl_native_args!(A0 0, A1 1, A2 2, A3 3, A4 4);
+ impl_native_args!(A0 0, A1 1, A2 2, A3 3, A4 4, A5 5);
+ impl_native_args!(A0 0, A1 1, A2 2, A3 3, A4 4, A5 5, A6 6);
+ impl_native_args!(A0 0, A1 1, A2 2, A3 3, A4 4, A5 5, A6 6, A7 7);
+}
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index 30ebf504..4495a103 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -440,6 +440,7 @@ pub unsafe fn downcast_ref_from_any_in_place<T:
RefFromAnyUnchecked>(
///
/// # Safety
///
+/// `Self` must be an object type or exactly one pointer to a live object.
/// `downcast_ref_from_object` must return `Some` only when the object at
/// `*place` is a `Self`, with a reference that stays valid while `place` is
/// borrowed.
@@ -451,10 +452,23 @@ pub unsafe trait DowncastRefFromObject {
///
/// `place` must hold a pointer to a live object.
unsafe fn downcast_ref_from_object(place: &*mut TVMFFIObject) ->
Option<&Self>;
+
+ /// The object that `self` is or refers to.
+ #[inline(always)]
+ fn as_object(&self) -> &crate::object::Object {
+ // SAFETY: a reference type is one pointer to a live object (trait
contract).
+ unsafe { &**std::ptr::from_ref(self).cast::<*const
crate::object::Object>() }
+ }
}
// An object type borrows the object itself.
unsafe impl<T: crate::object::ObjectCore> DowncastRefFromObject for T {
+ #[inline(always)]
+ fn as_object(&self) -> &crate::object::Object {
+ // SAFETY: an object type starts with its object header.
+ unsafe { &*std::ptr::from_ref(self).cast::<crate::object::Object>() }
+ }
+
#[inline(always)]
unsafe fn downcast_ref_from_object(place: &*mut TVMFFIObject) ->
Option<&Self> {
let object = *place;
diff --git a/rust/tvm-ffi/tests/test_cast.rs b/rust/tvm-ffi/tests/test_cast.rs
index 228f3198..379b8d18 100644
--- a/rust/tvm-ffi/tests/test_cast.rs
+++ b/rust/tvm-ffi/tests/test_cast.rs
@@ -123,6 +123,19 @@ fn test_is_instance_of() {
assert!(!is_instance_of::<Object>(TypeIndex::kTVMFFIInt as i32));
}
+#[test]
+fn test_field_default() {
+ assert_eq!(unsafe { tvm_ffi_sys::TVMFFITestingDummyTarget() }, 0);
+ let v_i64 = reflection::field_default::<TestBaseObj>("v_i64").unwrap();
+ assert_eq!(i64::try_from(v_i64).unwrap(), 10);
+ let v_str = reflection::field_default::<TestBaseObj>("v_str").unwrap();
+ assert_eq!(String::try_from(v_str).unwrap().as_str(), "hello");
+ // no default, unknown, and inherited fields
+ assert!(reflection::field_default::<TestDerivedObj>("v_map").is_none());
+ assert!(reflection::field_default::<TestBaseObj>("missing").is_none());
+ assert!(reflection::field_default::<TestDerivedObj>("v_i64").is_none());
+}
+
#[test]
fn test_upcast_downcast_roundtrip() {
let delete_counter = Arc::new(AtomicU32::new(0));
diff --git a/rust/tvm-ffi/tests/test_object_reflection.rs
b/rust/tvm-ffi/tests/test_object_reflection.rs
index 6071039a..a7dbf137 100644
--- a/rust/tvm-ffi/tests/test_object_reflection.rs
+++ b/rust/tvm-ffi/tests/test_object_reflection.rs
@@ -17,8 +17,8 @@
* under the License.
*/
use tvm_ffi::derive::{Object, ObjectRef};
-use tvm_ffi::reflection::TypeAttrColumn;
use tvm_ffi::reflection::{FieldFlags, FieldGetter, ObjectDef};
+use tvm_ffi::reflection::{NativeFunction, TypeAttrColumn};
use tvm_ffi::*;
use tvm_ffi_sys::{
TVMFFIFieldFlagBitMask, TVMFFIFieldInfo, TVMFFIFieldSetter,
TVMFFIGetTypeInfo,
@@ -581,3 +581,52 @@ impl DuplicateMethodObj {
fn test_duplicate_method_names_fail_the_registration() {
DuplicateMethodObj::type_index();
}
+
+#[test]
+fn test_native_function() -> Result<()> {
+ // A big integer object registers a native `__any_equal__` hook.
+ let big = |value: i128| Any::from(BigInt::from(value));
+ let column = TypeAttrColumn::new("__any_equal__").expect("the column is
registered");
+ let hook = column
+ .get(big(1 << 100).type_index())
+ .expect("the hook is registered");
+ // SAFETY: C++ registers `__any_equal__` as `bool(const Any&, const Any&)`.
+ let equal: NativeFunction<(&Any, &Any), bool> = unsafe {
NativeFunction::from_any(hook)? };
+ assert!(equal.call((&big(1 << 100), &big(1 << 100)))?);
+ assert!(!equal.call((&big(1 << 100), &big(1 << 101)))?);
+ let error = equal
+ .call((&big(1 << 100), &Any::from(String::from("1"))))
+ .unwrap_err();
+ assert_eq!(error.kind(), TYPE_ERROR);
+
+ // A packed function in place of a native hook.
+ let get_x = Function::from_typed(|p: Point| -> Result<i64> { Ok(p.data.x)
});
+ // SAFETY: a packed function is called through checked conversions.
+ let hook: NativeFunction<(&PointObj,), i64> =
+ unsafe { NativeFunction::from_any(get_x.clone().into())? };
+ let p = point(3, 0.5, "p");
+ assert_eq!(hook.call((&*p.data,))?, 3);
+ // SAFETY: as above.
+ let mismatch: NativeFunction<(&Point,), String> =
+ unsafe { NativeFunction::from_any(get_x.into())? };
+ let error = mismatch.call((&p,)).unwrap_err();
+ assert_eq!(error.message(), "Packed hook result type mismatch");
+
+ // A Rust closure as a native hook, whose error is the hook's.
+ let x_of = |p: &PointObj| -> Result<i64> {
+ match p.x {
+ 0 => Err(Error::new(VALUE_ERROR, "zero", "")),
+ x => Ok(x),
+ }
+ };
+ let stored = Any::from(NativeFunction::<(&PointObj,), i64>::from(x_of));
+ // SAFETY: the hook was built with this signature.
+ let x_of: NativeFunction<(&PointObj,), i64> = unsafe {
NativeFunction::from_any(stored)? };
+ assert_eq!(x_of.call((&*p.data,))?, 3);
+ let error = x_of.call((&*point(0, 0.5, "p").data,)).unwrap_err();
+ assert_eq!(error.kind(), VALUE_ERROR);
+
+ // SAFETY: an integer is rejected before any call.
+ assert!(unsafe { NativeFunction::<()>::from_any(Any::from(1_i64))
}.is_err());
+ Ok(())
+}