This is an automated email from the ASF dual-hosted git repository.
Seven-Streams 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 ad6fc947 [FEAT][Rust] Record type schemas and doc strings of exported
functions (#832)
ad6fc947 is described below
commit ad6fc94765b39d2e27b360d0cd0a1cac23231fdf
Author: Gabriel Wu <[email protected]>
AuthorDate: Tue Oct 27 03:45:50 2026 +0800
[FEAT][Rust] Record type schemas and doc strings of exported functions
(#832)
- **Type schemas.** `AnyCompatible::type_schema()` mirrors C++
`TypeTraits<T>::TypeSchema`. Its default is `{"type":"<type_str>"}`,
which is right for object references and for most builtins, whose type
string is their type key. `Option`, `Array`, `Map`, `BigInt`,
`Unchanged` and `UnchangedOr` override it to write theirs as C++ does.
`RValueRef`, `Any` and `AnyView` have their schemas as parameters or
return types.
- **Function schemas.** `AsPackedCallable::type_schema()` gives a typed
function's schema,
`{"type":"ffi.Function","named_args":{"return":[...],"params":[...]}}`,
as C++ `FunctionInfo::TypeSchema` writes it. A callable whose signature
the crate cannot see, such as a downstream `AsPackedCallable` impl for a
packed function, keeps the default method, which returns the untyped
schema that `def_packed` records. Existing impls keep compiling.
- **Exports, behind a feature.** The new `export-metadata` cargo feature
is the counterpart of C++ `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`, and
like it is off by default. With the feature,
`tvm_ffi_dll_export_typed_func!` also exports
`__tvm_ffi__metadata_<name>`, and the new
`tvm_ffi_dll_export_typed_func_doc!(name, "doc")` exports
`__tvm_ffi__doc_<name>`, like `TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC`.
Without the feature, the export is unchanged and the doc macro exports
nothing, as in C++; it still checks that the doc string is a string
constant. A `#[cfg]` emitted by a `macro_rules!` expansion would test
the features of the calling crate, so the export macros call hidden
helper macros that tvm-ffi defines twice, once per state of its own
feature. As in C++, both getters return a string that the runtime
allocates (`TVMFFIStringFromByteArray`), so it outlives the library.
- **Globals, always.** `Function::register_global_typed(name, func,
doc)` registers a typed function with its schema and doc string, like
`GlobalDef().def`. `Function::register_global` now records the untyped
schema, like `def_packed`. Both register through
`TVMFFIFunctionSetGlobalFromMethodInfo`, which tvm-ffi-sys now binds.
- **Byte-identical JSON.** The metadata escapes the schema string with
the runtime's JSON writer (`ffi.json.Stringify`), which uses the same
`EscapeStringJSON` as the C++ macros. The Rust metadata therefore
matches the C++ metadata byte for byte, and Rust needs no JSON escaper
of its own.
- **Docs and CI.** The Rust guide documents typed globals, exports and
the feature. CI's Rust step also runs `cargo test -p tvm-ffi --features
export-metadata`, so the export tests run.
**API note:** a typed closure passed to `Function::from_typed` (and now
to the export macro) needs a return type with a schema. That means an
`AnyCompatible` type, `Any` or `AnyView`, which covers every return type
the crate converts today. Before, any type with `From<Out> for Any` was
accepted.
---------
Co-authored-by: Yuchuan <[email protected]>
---
.github/workflows/ci_test.yml | 1 +
docs/guides/rust_lang_guide.md | 41 ++++
rust/tvm-ffi-sys/src/c_api.rs | 4 +
rust/tvm-ffi/Cargo.toml | 6 +-
rust/tvm-ffi/src/any.rs | 2 +-
rust/tvm-ffi/src/big_int/mod.rs | 4 +
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/extra/unchanged.rs | 8 +
rust/tvm-ffi/src/function.rs | 59 +++++-
rust/tvm-ffi/src/function_internal.rs | 72 ++++++-
rust/tvm-ffi/src/macros.rs | 109 ++++++++++
rust/tvm-ffi/src/reflection/registry.rs | 154 +++++---------
rust/tvm-ffi/src/rvalue_ref.rs | 9 +
rust/tvm-ffi/src/type_traits.rs | 93 +++++++++
rust/tvm-ffi/tests/test_function_metadata.rs | 291 +++++++++++++++++++++++++++
17 files changed, 753 insertions(+), 121 deletions(-)
diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml
index 7a1b735f..32d994eb 100644
--- a/.github/workflows/ci_test.yml
+++ b/.github/workflows/ci_test.yml
@@ -208,6 +208,7 @@ jobs:
working-directory: rust
run: |
cargo test
+ cargo test -p tvm-ffi --features export-metadata
# Build the Rust stubgen example against the installed package. Its
post-build
# step refreshes the bindings in place, so a diff means the checked-in
file was stale.
diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 32515785..8309bfd7 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -157,6 +157,47 @@ let my_func = Function::from_packed(|args: &[AnyView]| ->
Result<Any> {
Ok(Any::default())
});
Function::register_global("my_custom_func", my_func)?;
+
+// Register a typed function with a doc string
+Function::register_global_typed(
+ "my_add",
+ |x: i64, y: i64| -> Result<i64> { Ok(x + y) },
+ "Add two integers.",
+)?;
+```
+
+As C++ `refl::GlobalDef().def` does, a typed function's metadata records its
+type schema, so `tvm_ffi.get_global_func_metadata` and the stub generator see
+its parameter and return types. `Function::register_global` records the schema
+of an untyped function, as `def_packed` does.
+
+### Exporting Functions from a Library
+
+A Rust `cdylib` exports a typed function under the `__tvm_ffi_<name>` symbol
+that `Module::load_from_file` and `tvm_ffi.load_module` look up:
+
+```rust
+use tvm_ffi::*;
+
+fn add(a: i64, b: i64) -> Result<i64> {
+ Ok(a + b)
+}
+
+tvm_ffi_dll_export_typed_func!(add, add);
+tvm_ffi_dll_export_typed_func_doc!(add, "Add two integers and return the
sum.");
+```
+
+With the `export-metadata` feature of the `tvm-ffi` crate, the counterpart of
+C++ `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`, the export also writes the
+function's type schema as `__tvm_ffi__metadata_<name>`, and
+`tvm_ffi_dll_export_typed_func_doc!` writes its doc string as
+`__tvm_ffi__doc_<name>`, which `Module.get_function_metadata` and
+`Module.get_function_doc` read. As in C++, it is off by default, and without
+it `tvm_ffi_dll_export_typed_func_doc!` exports nothing:
+
+```toml
+[dependencies]
+tvm-ffi = { version = "...", features = ["export-metadata"] }
```
### Reflected Type Methods
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index 86f5e431..86ab0a51 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -494,6 +494,10 @@ unsafe extern "C" {
f: TVMFFIObjectHandle,
can_override: i32,
) -> i32;
+ pub fn TVMFFIFunctionSetGlobalFromMethodInfo(
+ method_info: *const TVMFFIMethodInfo,
+ allow_override: i32,
+ ) -> i32;
pub fn TVMFFIFunctionCreate(
self_ptr: *mut c_void,
safe_call: TVMFFISafeCallType,
diff --git a/rust/tvm-ffi/Cargo.toml b/rust/tvm-ffi/Cargo.toml
index 1fbd1c8a..7adf105f 100644
--- a/rust/tvm-ffi/Cargo.toml
+++ b/rust/tvm-ffi/Cargo.toml
@@ -39,8 +39,12 @@ tvm-ffi-macros = { version = "0.1.0-alpha.0", path =
"../tvm-ffi-macros" }
# The tests call the `testing.*` global functions.
tvm-ffi-sys = { version = "0.1.0-alpha.0", path = "../tvm-ffi-sys", features =
["testing"] }
-# only needed for example building
[features]
+# Export the metadata and doc string of each function that
+# `tvm_ffi_dll_export_typed_func!` and `tvm_ffi_dll_export_typed_func_doc!`
+# export, as C++ does with TVM_FFI_DLL_EXPORT_INCLUDE_METADATA.
+export-metadata = []
+# only needed for example building
example = []
[[example]]
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 8b0d714c..1c5fc500 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -428,7 +428,7 @@ crate::impl_try_from_any_for_parametric!(Option<T>);
//------------------------------------------------------------
/// ArgTryFromAnyView: Helper for function argument passing
///-----------------------------------------------------------
-pub(crate) trait ArgTryFromAnyView: Sized {
+pub(crate) trait ArgTryFromAnyView: Sized + crate::type_traits::TypeSchema {
fn try_from_any_view(value: &AnyView, arg_index: usize) -> Result<Self,
Error>;
}
diff --git a/rust/tvm-ffi/src/big_int/mod.rs b/rust/tvm-ffi/src/big_int/mod.rs
index b0380d28..652dbd41 100644
--- a/rust/tvm-ffi/src/big_int/mod.rs
+++ b/rust/tvm-ffi/src/big_int/mod.rs
@@ -825,6 +825,10 @@ unsafe impl AnyCompatible for BigInt {
"BigInt".to_string()
}
+ fn type_schema() -> std::string::String {
+ crate::type_traits::type_schema(<BigIntObj as
crate::ObjectCore>::TYPE_KEY, &[])
+ }
+
unsafe fn copy_to_any_view(this: &Self, data: &mut TVMFFIAny) {
*data = this.data;
}
diff --git a/rust/tvm-ffi/src/collections/array.rs
b/rust/tvm-ffi/src/collections/array.rs
index 0a842bf8..5d6bb115 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -243,6 +243,13 @@ where
format!("Array<{}>", T::container_type_str())
}
+ fn type_schema() -> String {
+ crate::type_traits::type_schema(
+ <ArrayObj as crate::ObjectCore>::TYPE_KEY,
+ &[T::container_type_schema()],
+ )
+ }
+
unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
if data.type_index != TypeIndex::kTVMFFIArray as i32 {
return false;
diff --git a/rust/tvm-ffi/src/collections/list.rs
b/rust/tvm-ffi/src/collections/list.rs
index 68add977..2aabfacd 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -565,6 +565,13 @@ unsafe impl<T: ContainerElement> AnyCompatible for List<T>
{
format!("List<{}>", T::container_type_str())
}
+ fn type_schema() -> String {
+ crate::type_traits::type_schema(
+ <ListObj as crate::ObjectCore>::TYPE_KEY,
+ &[T::container_type_schema()],
+ )
+ }
+
unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
data.type_index == TypeIndex::kTVMFFIList as i32
&& seq_cells(data)
diff --git a/rust/tvm-ffi/src/collections/map.rs
b/rust/tvm-ffi/src/collections/map.rs
index 8a1a425d..c7da95eb 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -467,6 +467,13 @@ where
)
}
+ fn type_schema() -> String {
+ crate::type_traits::type_schema(
+ <MapObj as crate::ObjectCore>::TYPE_KEY,
+ &[K::container_type_schema(), V::container_type_schema()],
+ )
+ }
+
unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
// Mirrors C++ `CheckAnyStrict` (map_base.h): every entry must strictly
// match `K`/`V`. An FFI failure reads as "no match" — this fn has no
diff --git a/rust/tvm-ffi/src/extra/unchanged.rs
b/rust/tvm-ffi/src/extra/unchanged.rs
index cbf0f140..0ebdccad 100644
--- a/rust/tvm-ffi/src/extra/unchanged.rs
+++ b/rust/tvm-ffi/src/extra/unchanged.rs
@@ -63,6 +63,10 @@ unsafe impl AnyCompatible for Unchanged {
fn type_str() -> std::string::String {
"Unchanged".into()
}
+
+ fn type_schema() -> std::string::String {
+ crate::type_traits::type_schema("ffi.Unchanged", &[])
+ }
}
crate::impl_try_from_any!(Unchanged);
@@ -270,6 +274,10 @@ unsafe impl<T: ContainerElement> AnyCompatible for
UnchangedOr<T> {
fn type_str() -> std::string::String {
format!("UnchangedOr<{}>", T::container_type_str())
}
+
+ fn type_schema() -> std::string::String {
+ crate::type_traits::type_schema("UnchangedOr",
&[T::container_type_schema()])
+ }
}
impl<T: ContainerElement> TryFrom<Any> for UnchangedOr<T> {
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index 2c7b1827..f375ba7e 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -24,8 +24,8 @@ use crate::object::{Object, ObjectArc, ObjectCore};
use crate::type_traits::AnyCompatible;
use tvm_ffi_sys::{
TVMFFIAny, TVMFFIByteArray, TVMFFIFunctionCell, TVMFFIFunctionCreate,
TVMFFIFunctionGetGlobal,
- TVMFFIFunctionSetGlobal, TVMFFIGetTypeInfo, TVMFFIObjectHandle,
TVMFFISafeCallType,
- TVMFFITypeIndex, TVMFFITypeKeyToIndex,
+ TVMFFIFunctionSetGlobalFromMethodInfo, TVMFFIGetTypeInfo,
TVMFFIMethodInfo, TVMFFIObjectHandle,
+ TVMFFISafeCallType, TVMFFITypeIndex, TVMFFITypeKeyToIndex,
};
/// function object
@@ -298,6 +298,12 @@ impl Function {
}
/// Register a function as a global function
+ ///
+ /// The function's metadata records the type schema of an untyped
+ /// function, as C++ `refl::GlobalDef().def_packed` does. Use
+ /// [`Function::register_global_typed`] to record the parameter and return
+ /// types of a typed function, and a doc string.
+ ///
/// # Arguments
/// * `name` - The name of the function
/// * `func` - The function to register
@@ -305,15 +311,50 @@ impl Function {
/// # Returns
/// * `Result<()>` - The result of the registration
pub fn register_global(name: &str, func: Function) -> Result<()> {
+ let type_schema =
crate::type_traits::type_schema(FunctionObj::TYPE_KEY, &[]);
+ Self::register_global_with_info(name, &func, &type_schema, "")
+ }
+
+ /// Register a typed function as a global function, with a doc string
+ ///
+ /// As C++ `refl::GlobalDef().def` does, the function's metadata records
+ /// its type schema, which `tvm_ffi.get_global_func_metadata` and the stub
+ /// generator read. An empty `doc` records no doc string.
+ ///
+ /// # Arguments
+ /// * `name` - The name of the function
+ /// * `func` - The typed function to register, as for
[`Function::from_typed`]
+ /// * `doc` - The doc string of the function
+ ///
+ /// # Returns
+ /// * `Result<()>` - The result of the registration
+ pub fn register_global_typed<F, I, O>(name: &str, func: F, doc: &str) ->
Result<()>
+ where
+ F: AsPackedCallable<I, O> + 'static,
+ {
+ let type_schema = F::type_schema();
+ Self::register_global_with_info(name, &Self::from_typed(func),
&type_schema, doc)
+ }
+
+ fn register_global_with_info(
+ name: &str,
+ func: &Function,
+ type_schema: &str,
+ doc: &str,
+ ) -> Result<()> {
+ let metadata = crate::type_traits::type_schema_metadata(type_schema);
+ let mut method = TVMFFIAny::new();
unsafe {
- let name_arg = TVMFFIByteArray::from_str(name);
+ <Function as AnyCompatible>::copy_to_any_view(func, &mut method);
+ let info = TVMFFIMethodInfo {
+ name: TVMFFIByteArray::from_str(name),
+ doc: TVMFFIByteArray::from_str(doc),
+ metadata: TVMFFIByteArray::from_str(&metadata),
+ flags: 0,
+ method,
+ };
let can_override = 0;
- crate::check_safe_call!(TVMFFIFunctionSetGlobal(
- &name_arg,
- ObjectArc::as_raw(&func.data) as *mut FunctionObj as
TVMFFIObjectHandle,
- can_override
- ))?;
- Ok(())
+
crate::check_safe_call!(TVMFFIFunctionSetGlobalFromMethodInfo(&info,
can_override))
}
}
/// Construct a function from a packed function
diff --git a/rust/tvm-ffi/src/function_internal.rs
b/rust/tvm-ffi/src/function_internal.rs
index 33c9eb47..8f7e5c87 100644
--- a/rust/tvm-ffi/src/function_internal.rs
+++ b/rust/tvm-ffi/src/function_internal.rs
@@ -18,10 +18,12 @@
*/
use crate::any::{Any, AnyView, ArgTryFromAnyView};
use crate::error::{Error, Result, INTERNAL_ERROR};
-use crate::object::ObjectRefCore;
+use crate::function::FunctionObj;
+use crate::object::{ObjectCore, ObjectRefCore};
use crate::rvalue_ref::RValueRef;
use crate::string::{Bytes, String};
-use crate::type_traits::{AnyCompatible, ContainerElement};
+use crate::type_traits::{AnyCompatible, ContainerElement, TypeSchema};
+use tvm_ffi_sys::{TVMFFIAny, TVMFFIByteArray, TVMFFIStringFromByteArray};
//------------------------------------------------------------------------
// PackedCallable
@@ -45,6 +47,61 @@ pub fn panic_to_error(payload: Box<dyn std::any::Any +
Send>) -> Error {
pub trait AsPackedCallable<I, O> {
// Call the function in packed convention
fn call_packed(&self, packed_args: &[AnyView]) -> Result<Any>;
+
+ /// The type schema of the function, as C++ `FunctionInfo::TypeSchema`
+ /// writes it.
+ ///
+ /// A typed function lists its return and parameter types. The default,
+ /// for a callable whose signature is not known, is the schema of an
+ /// untyped function, as C++ `refl::GlobalDef().def_packed` records it.
+ fn type_schema() -> std::string::String
+ where
+ Self: Sized,
+ {
+ crate::type_traits::type_schema(FunctionObj::TYPE_KEY, &[])
+ }
+}
+
+/// The type schema of `func`, a value of a callable type.
+#[inline]
+pub fn type_schema_of<Fun, I, O>(_func: &Fun) -> std::string::String
+where
+ Fun: AsPackedCallable<I, O>,
+{
+ Fun::type_schema()
+}
+
+/// Writes `text` to `result` as a string allocated by the runtime, so that it
+/// outlives the library that wrote it, as the getters that
+/// `TVM_FFI_DLL_EXPORT_TYPED_FUNC` and `TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC`
+/// export do. Returns the safe call return code.
+///
+/// # Safety
+///
+/// `result` must be valid for writes of one `TVMFFIAny`.
+#[doc(hidden)]
+pub unsafe fn write_exported_str(text: Result<std::string::String>, result:
*mut TVMFFIAny) -> i32 {
+ match text {
+ Ok(text) => {
+ let text = TVMFFIByteArray::from_str(&text);
+ TVMFFIStringFromByteArray(&text, result)
+ }
+ Err(error) => {
+ Error::set_raised(&error);
+ -1
+ }
+ }
+}
+
+/// The metadata a library exports for a function `func` as
+/// `__tvm_ffi__metadata_<name>`, as `TVM_FFI_DLL_EXPORT_TYPED_FUNC` does.
+#[doc(hidden)]
+pub fn exported_metadata<Fun, I, O>(func: &Fun) -> Result<std::string::String>
+where
+ Fun: AsPackedCallable<I, O>,
+{
+ let type_schema = type_schema_of(func);
+ Ok(crate::type_traits::type_schema_metadata(&type_schema))
}
#[inline]
@@ -61,8 +118,19 @@ macro_rules! impl_as_packed_callable {
where
Fun: Fn($($t,)*) -> Result<Out> + 'static,
Any: From<Out>,
+ Out: TypeSchema,
$($t: ArgTryFromAnyView),*
{
+ fn type_schema() -> std::string::String {
+ let params: &[std::string::String] = &[$(<$t as
TypeSchema>::type_schema()),*];
+ format!(
+
r#"{{"type":"{}","named_args":{{"return":[{}],"params":[{}]}}}}"#,
+ FunctionObj::TYPE_KEY,
+ Out::type_schema(),
+ params.join(","),
+ )
+ }
+
fn call_packed(&self, packed_args: &[AnyView]) -> Result<Any>
{
crate::ensure!(
diff --git a/rust/tvm-ffi/src/macros.rs b/rust/tvm-ffi/src/macros.rs
index 5f32ecbf..8c88fba6 100644
--- a/rust/tvm-ffi/src/macros.rs
+++ b/rust/tvm-ffi/src/macros.rs
@@ -313,6 +313,17 @@ macro_rules! impl_arg_into_ref {
/// since unwinding into the caller would abort the process, but panicking
/// is discouraged.
///
+/// With the `export-metadata` feature, the counterpart of C++
+/// `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`, the macro also exports the
+/// function's metadata, `{"type_schema":...}`, as
`__tvm_ffi__metadata_<name>`,
+/// which `Module::GetFunctionMetadata` and the stub generator read. A
+/// function's schema lists its parameter and return types; a callable whose
+/// signature is not known, such as a custom [`AsPackedCallable`], has the
+/// schema of an untyped function. As in C++, the feature is off by default,
+/// so exports are minimal.
+///
+/// [`AsPackedCallable`]: crate::function_internal::AsPackedCallable
+///
/// # Arguments
/// * `$name` - The name of the function
/// * `$func` - The function to export
@@ -371,6 +382,104 @@ macro_rules! tvm_ffi_dll_export_typed_func {
}
}
}
+ $crate::__tvm_ffi_dll_export_metadata!($name, $func);
+ };
+}
+
+/// Macro to export the doc string of a function exported with
+/// [`tvm_ffi_dll_export_typed_func!`], as C++
`TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC`
+/// does
+///
+/// With the `export-metadata` feature, the doc string is exported as
+/// `__tvm_ffi__doc_<name>`, which `Module::GetFunctionDoc` and the stub
+/// generator read. Without it, as C++ without
+/// `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`, the macro exports nothing, but
+/// still checks that the doc string is a string constant. The macro never
+/// exports the function itself.
+///
+/// # Arguments
+/// * `$name` - The name the function is exported as
+/// * `$doc` - The doc string, a string literal
+///
+/// # Example
+/// ```rust
+/// use tvm_ffi::*;
+///
+/// fn add(a: i64, b: i64) -> Result<i64> { Ok(a + b) }
+///
+/// tvm_ffi_dll_export_typed_func!(add, add);
+/// tvm_ffi_dll_export_typed_func_doc!(add, "Add two integers and return the
sum.");
+/// ```
+#[macro_export]
+macro_rules! tvm_ffi_dll_export_typed_func_doc {
+ ($name:ident, $doc:expr) => {
+ $crate::__tvm_ffi_dll_export_doc!($name, $doc);
+ };
+}
+
+// The metadata and doc getters the export macros emit with the
+// `export-metadata` feature. A `#[cfg]` in a `macro_rules!` expansion would
+// test the features of the crate that invokes the macro, so the feature
+// selects which definition of these helpers this crate compiles instead.
+
+#[cfg(feature = "export-metadata")]
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __tvm_ffi_dll_export_metadata {
+ ($name:ident, $func:expr) => {
+ $crate::macros::paste::paste! {
+ #[no_mangle]
+ pub unsafe extern "C" fn [<__tvm_ffi__metadata_ $name>](
+ _handle: *mut std::ffi::c_void,
+ _args: *const $crate::tvm_ffi_sys::TVMFFIAny,
+ _num_args: i32,
+ result: *mut $crate::tvm_ffi_sys::TVMFFIAny,
+ ) -> i32 {
+ let metadata = match
::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
+ $crate::function_internal::exported_metadata(&$func)
+ })) {
+ Ok(metadata) => metadata,
+ Err(payload) =>
Err($crate::function_internal::panic_to_error(payload)),
+ };
+ $crate::function_internal::write_exported_str(metadata, result)
+ }
+ }
+ };
+}
+
+#[cfg(not(feature = "export-metadata"))]
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __tvm_ffi_dll_export_metadata {
+ ($name:ident, $func:expr) => {};
+}
+
+#[cfg(feature = "export-metadata")]
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __tvm_ffi_dll_export_doc {
+ ($name:ident, $doc:expr) => {
+ $crate::macros::paste::paste! {
+ #[no_mangle]
+ pub unsafe extern "C" fn [<__tvm_ffi__doc_ $name>](
+ _handle: *mut std::ffi::c_void,
+ _args: *const $crate::tvm_ffi_sys::TVMFFIAny,
+ _num_args: i32,
+ result: *mut $crate::tvm_ffi_sys::TVMFFIAny,
+ ) -> i32 {
+ const DOC: &str = $doc;
+
$crate::function_internal::write_exported_str(Ok(DOC.to_string()), result)
+ }
+ }
+ };
+}
+
+#[cfg(not(feature = "export-metadata"))]
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __tvm_ffi_dll_export_doc {
+ ($name:ident, $doc:expr) => {
+ const _: &str = $doc;
};
}
diff --git a/rust/tvm-ffi/src/reflection/registry.rs
b/rust/tvm-ffi/src/reflection/registry.rs
index e7e59669..acbb0558 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -42,7 +42,7 @@ use crate::tvm_ffi_sys::{
TVMFFIGetTypeInfo, TVMFFIObject, TVMFFISEqHashKind, TVMFFITypeIndex,
TVMFFITypeMetadata,
TVMFFITypeRegisterAttr, TVMFFITypeRegisterField,
TVMFFITypeRegisterMetadata,
};
-use crate::type_traits::AnyCompatible;
+use crate::type_traits::{type_schema_metadata, AnyCompatible};
use crate::{Any, Error, Function, Result, INTERNAL_ERROR, TYPE_ERROR};
/// A copy of an object's own fields under a fresh object header (the C++ copy
@@ -207,8 +207,7 @@ impl<N: ObjectCore> ObjectDef<N> {
if !self.fresh {
return Ok(self);
}
- let (static_type_index, schema) = field_type_info(&F::type_str());
- let metadata = format!(r#"{{"type_schema":{}}}"#,
json_string(&schema));
+ let metadata = type_schema_metadata(&F::type_schema());
let info = TVMFFIFieldInfo {
name: unsafe { TVMFFIByteArray::from_str(name) },
doc: unsafe { TVMFFIByteArray::from_str("") },
@@ -220,7 +219,7 @@ impl<N: ObjectCore> ObjectDef<N> {
getter: Some(field_getter::<F>),
setter: field_setter::<F> as TVMFFIFieldSetter as *mut c_void,
default_value_or_factory: TVMFFIAny::new(),
- field_static_type_index: static_type_index,
+ field_static_type_index: field_static_type_index(&F::type_str()),
};
// The table copies the strings before returning.
crate::check_safe_call!(unsafe {
TVMFFITypeRegisterField(self.type_index, &info) })?;
@@ -364,135 +363,74 @@ unsafe extern "C" fn field_setter<F: AnyCompatible>(
-1
}
-/// The static type index and the JSON type schema of a field, from the Rust
type's `type_str()`
-/// (the names `AnyCompatible` reports, such as `int`, `ffi.String`,
`Array<my.Node>`), as C++
-/// `TypeTraits<T>::field_static_type_index` and `TypeTraits<T>::TypeSchema()`
state them.
-fn field_type_info(type_str: &str) -> (i32, String) {
+/// The static type index of a field, from the Rust type's `type_str()` (the
names
+/// `AnyCompatible` reports, such as `int`, `ffi.String`, `Array<my.Node>`),
as C++
+/// `TypeTraits<T>::field_static_type_index` states it.
+fn field_static_type_index(type_str: &str) -> i32 {
use TVMFFITypeIndex::*;
/// C++ `TypeIndex::kTVMFFIAny`, the static index of a field holding any
value.
const ANY: i32 = -1;
- let (name, args) = split_generic(type_str);
- let args: Vec<String> = args.iter().map(|arg|
field_type_info(arg).1).collect();
- let schema = |key: &str| match args.is_empty() {
- true => format!(r#"{{"type":"{key}"}}"#),
- false => format!(r#"{{"type":"{key}","args":[{}]}}"#, args.join(",")),
- };
- match name {
- "None" => (kTVMFFINone as i32, schema("None")),
- "int" => (kTVMFFIInt as i32, schema("int")),
- "bool" => (kTVMFFIBool as i32, schema("bool")),
- "float" => (kTVMFFIFloat as i32, schema("float")),
- "void*" => (kTVMFFIOpaquePtr as i32, schema("void*")),
- "DataType" => (kTVMFFIDataType as i32, schema("DataType")),
- "Device" => (kTVMFFIDevice as i32, schema("Device")),
- "DLTensor*" => (kTVMFFIDLTensorPtr as i32, schema("DLTensor*")),
- "Any" => (ANY, schema("Any")),
+ let origin = type_str.split('<').next().unwrap_or(type_str).trim();
+ match origin {
+ "None" => kTVMFFINone as i32,
+ "int" => kTVMFFIInt as i32,
+ "bool" => kTVMFFIBool as i32,
+ "float" => kTVMFFIFloat as i32,
+ "void*" => kTVMFFIOpaquePtr as i32,
+ "DataType" => kTVMFFIDataType as i32,
+ "Device" => kTVMFFIDevice as i32,
+ "DLTensor*" => kTVMFFIDLTensorPtr as i32,
// C++ `String`, `Bytes` and `BigInt` fields are typed `Any`
statically (they have
// inline forms), as is `Optional<T>`.
- "ffi.String" | "ffi.Bytes" => (ANY, schema(name)),
- "BigInt" => (ANY, schema("ffi.BigInt")),
- "Optional" => (ANY, schema("Optional")),
- "ffi.Function" => (kTVMFFIFunction as i32, schema(name)),
- "ffi.Shape" => (kTVMFFIShape as i32, schema(name)),
- "Array" => (kTVMFFIArray as i32, schema("ffi.Array")),
- "List" => (kTVMFFIList as i32, schema("ffi.List")),
- // Any other object reference, `Map` and `Dict` included, is typed
`Object` statically.
- "Map" => (kTVMFFIStaticObjectBegin as i32, schema("ffi.Map")),
- "Dict" => (kTVMFFIStaticObjectBegin as i32, schema("ffi.Dict")),
- _ => (kTVMFFIStaticObjectBegin as i32, schema(name)),
- }
-}
-
-/// `Name<A, B<C>>` as (`Name`, [`A`, `B<C>`]); a plain name has no arguments.
-fn split_generic(type_str: &str) -> (&str, Vec<&str>) {
- let Some(open) = type_str.find('<') else {
- return (type_str.trim(), Vec::new());
- };
- let Some(inner) = type_str[open + 1..].strip_suffix('>') else {
- return (type_str.trim(), Vec::new());
- };
- let mut args = Vec::new();
- let (mut depth, mut start) = (0usize, 0usize);
- for (index, byte) in inner.bytes().enumerate() {
- match byte {
- b'<' => depth += 1,
- b'>' => depth = depth.saturating_sub(1),
- b',' if depth == 0 => {
- args.push(inner[start..index].trim());
- start = index + 1;
- }
- _ => {}
- }
+ "ffi.String" | "ffi.Bytes" | "BigInt" | "Optional" => ANY,
+ "ffi.Function" => kTVMFFIFunction as i32,
+ "ffi.Shape" => kTVMFFIShape as i32,
+ "Array" => kTVMFFIArray as i32,
+ "List" => kTVMFFIList as i32,
+ // Any other object reference, `Map` included, is typed `Object`
statically.
+ _ => kTVMFFIStaticObjectBegin as i32,
}
- args.push(inner[start..].trim());
- (type_str[..open].trim(), args)
-}
-
-/// `text` as a JSON string literal.
-fn json_string(text: &str) -> String {
- let mut out = String::with_capacity(text.len() + 2);
- out.push('"');
- for ch in text.chars() {
- match ch {
- '"' => out.push_str("\\\""),
- '\\' => out.push_str("\\\\"),
- '\n' => out.push_str("\\n"),
- '\r' => out.push_str("\\r"),
- '\t' => out.push_str("\\t"),
- ch if (ch as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", ch
as u32)),
- ch => out.push(ch),
- }
- }
- out.push('"');
- out
}
#[cfg(test)]
mod tests {
- use super::{field_type_info, json_string, split_generic};
+ use super::field_static_type_index;
+ use crate::type_traits::{type_schema_metadata, AnyCompatible};
+ use crate::{Array, List, Map};
#[test]
- fn generic_names_split_at_top_level_commas() {
- assert_eq!(split_generic("int"), ("int", vec![]));
- assert_eq!(split_generic("Array<my.Node>"), ("Array",
vec!["my.Node"]));
- assert_eq!(
- split_generic("Map<ffi.String, Array<my.Node>>"),
- ("Map", vec!["ffi.String", "Array<my.Node>"])
- );
+ fn field_static_type_indices_follow_the_cpp_type_traits() {
+ assert_eq!(field_static_type_index("int"), 1);
+ assert_eq!(field_static_type_index("ffi.String"), -1);
+ assert_eq!(field_static_type_index("Optional<my.Node>"), -1);
+ assert_eq!(field_static_type_index("my.Node"), 64);
+ assert_eq!(field_static_type_index("Array<my.Node>"), 71);
+ assert_eq!(field_static_type_index("List<int>"), 75);
+ assert_eq!(field_static_type_index("Map<ffi.String, int>"), 64);
}
#[test]
fn field_schemas_follow_the_cpp_type_traits() {
- assert_eq!(field_type_info("int"), (1, r#"{"type":"int"}"#.into()));
assert_eq!(
- field_type_info("ffi.String"),
- (-1, r#"{"type":"ffi.String"}"#.into())
+ <Map<crate::String, i64>>::type_schema(),
+
r#"{"type":"ffi.Map","args":[{"type":"ffi.String"},{"type":"int"}]}"#
);
assert_eq!(
- field_type_info("my.Node"),
- (64, r#"{"type":"my.Node"}"#.into())
+ <List<i64>>::type_schema(),
+ r#"{"type":"ffi.List","args":[{"type":"int"}]}"#
);
assert_eq!(
- field_type_info("Array<my.Node>"),
- (
- 71,
- r#"{"type":"ffi.Array","args":[{"type":"my.Node"}]}"#.into()
- )
+ <Option<Array<crate::String>>>::type_schema(),
+
r#"{"type":"Optional","args":[{"type":"ffi.Array","args":[{"type":"ffi.String"}]}]}"#
);
assert_eq!(
- field_type_info("Map<ffi.String, int>"),
- (
- 64,
-
r#"{"type":"ffi.Map","args":[{"type":"ffi.String"},{"type":"int"}]}"#.into()
- )
+ type_schema_metadata(r#"{"type":"int"}"#),
+ r#"{"type_schema":"{\"type\":\"int\"}"}"#
);
+ // As C++ `EscapeStringJSON`, `/`, `\b` and DEL are escaped too.
assert_eq!(
- field_type_info("Optional<my.Node>"),
- (
- -1,
- r#"{"type":"Optional","args":[{"type":"my.Node"}]}"#.into()
- )
+ type_schema_metadata("a/b\u{8}\u{7f}"),
+ r#"{"type_schema":"a\/b\b\u007f"}"#
);
- assert_eq!(json_string(r#"{"type":"int"}"#),
r#""{\"type\":\"int\"}""#);
}
}
diff --git a/rust/tvm-ffi/src/rvalue_ref.rs b/rust/tvm-ffi/src/rvalue_ref.rs
index f9d691b2..62e98887 100644
--- a/rust/tvm-ffi/src/rvalue_ref.rs
+++ b/rust/tvm-ffi/src/rvalue_ref.rs
@@ -126,6 +126,15 @@ where
}
}
+impl<T> crate::type_traits::TypeSchema for RValueRef<T>
+where
+ T: ObjectRefCore + AnyCompatible,
+{
+ fn type_schema() -> std::string::String {
+ crate::type_traits::type_schema("ObjectRValueRef", &[T::type_schema()])
+ }
+}
+
impl<T> ArgTryFromAnyView for RValueRef<T>
where
T: ObjectRefCore + AnyCompatible,
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index c298db9e..5f15a957 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -70,6 +70,16 @@ pub unsafe trait AnyCompatible: Sized {
/// the type string of the type
fn type_str() -> String;
+ /// The type schema of the type: the JSON object C++
+ /// `TypeTraits<T>::TypeSchema` writes, which the type schemas of functions
+ /// and fields embed.
+ ///
+ /// The default, `{"type":"<type_str>"}`, fits a type whose type string is
+ /// its type key, as for object references.
+ fn type_schema() -> String {
+ type_schema(&Self::type_str(), &[])
+ }
+
/// Borrow `self` into an owned [`Any`], increfing object-backed values.
///
/// The by-reference counterpart of `Any::from(value)`, for fields reached
@@ -83,6 +93,74 @@ pub unsafe trait AnyCompatible: Sized {
}
}
+/// Writes a type schema, `{"type":"<origin>"}` or, with type arguments,
+/// `{"type":"<origin>","args":[...]}`, as C++ `TypeTraits<T>::TypeSchema`
does.
+pub(crate) fn type_schema(origin: &str, args: &[String]) -> String {
+ if args.is_empty() {
+ format!(r#"{{"type":"{origin}"}}"#)
+ } else {
+ format!(r#"{{"type":"{origin}","args":[{}]}}"#, args.join(","))
+ }
+}
+
+/// The metadata C++ records for a function or field of type schema
+/// `type_schema`: `{"type_schema":<type_schema as a JSON string>}`.
+pub(crate) fn type_schema_metadata(type_schema: &str) -> String {
+ format!(r#"{{"type_schema":{}}}"#, escape_string_json(type_schema))
+}
+
+/// `text` as a JSON string literal, escaped as C++ `EscapeStringJSON`
+/// (`string.h`) escapes it, so that metadata matches the C++ one byte for
byte.
+fn escape_string_json(text: &str) -> String {
+ let mut out = String::with_capacity(text.len() + 2);
+ out.push('"');
+ for ch in text.chars() {
+ match ch {
+ '"' => out.push_str("\\\""),
+ '\\' => out.push_str("\\\\"),
+ '/' => out.push_str("\\/"),
+ '\u{8}' => out.push_str("\\b"),
+ '\u{c}' => out.push_str("\\f"),
+ '\n' => out.push_str("\\n"),
+ '\r' => out.push_str("\\r"),
+ '\t' => out.push_str("\\t"),
+ ch if (ch as u32) < 0x20 || ch == '\u{7f}' => {
+ out.push_str(&format!("\\u{:04x}", ch as u32))
+ }
+ ch => out.push(ch),
+ }
+ }
+ out.push('"');
+ out
+}
+
+/// The type schema of a function parameter or return type.
+///
+/// This is [`AnyCompatible::type_schema`] for a type that converts through
+/// [`Any`], and `{"type":"Any"}` for `Any` and `AnyView` themselves, as C++
+/// `details::TypeSchema<T>` writes them.
+pub(crate) trait TypeSchema {
+ fn type_schema() -> String;
+}
+
+impl<T: AnyCompatible> TypeSchema for T {
+ fn type_schema() -> String {
+ <T as AnyCompatible>::type_schema()
+ }
+}
+
+impl TypeSchema for Any {
+ fn type_schema() -> String {
+ type_schema("Any", &[])
+ }
+}
+
+impl TypeSchema for AnyView<'_> {
+ fn type_schema() -> String {
+ type_schema("Any", &[])
+ }
+}
+
/// Marker for a value that can be stored in an FFI container.
///
/// This is an implementation detail of [`crate::Array`] and [`crate::Map`].
@@ -112,6 +190,7 @@ mod container_element_ops {
unsafe fn container_try_cast_from_any_view(data: &TVMFFIAny) ->
Result<Self, ()>;
fn container_get_mismatch_type_info(data: &TVMFFIAny) -> String;
fn container_type_str() -> String;
+ fn container_type_schema() -> String;
}
impl<T: AnyCompatible> Ops for T {
@@ -154,6 +233,11 @@ mod container_element_ops {
fn container_type_str() -> String {
<T as AnyCompatible>::type_str()
}
+
+ #[inline]
+ fn container_type_schema() -> String {
+ <T as AnyCompatible>::type_schema()
+ }
}
impl Ops for Any {
@@ -198,6 +282,11 @@ mod container_element_ops {
fn container_type_str() -> String {
"Any".to_string()
}
+
+ #[inline]
+ fn container_type_schema() -> String {
+ <Any as super::TypeSchema>::type_schema()
+ }
}
}
@@ -297,6 +386,10 @@ unsafe impl<T: AnyCompatible> AnyCompatible for Option<T> {
"Optional<".to_string() + T::type_str().as_str() + ">"
}
+ fn type_schema() -> String {
+ type_schema("Optional", &[<T as AnyCompatible>::type_schema()])
+ }
+
unsafe fn copy_to_any_view(src: &Self, data: &mut TVMFFIAny) {
if let Some(ref value) = src {
T::copy_to_any_view(value, data);
diff --git a/rust/tvm-ffi/tests/test_function_metadata.rs
b/rust/tvm-ffi/tests/test_function_metadata.rs
new file mode 100644
index 00000000..e2370340
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_function_metadata.rs
@@ -0,0 +1,291 @@
+/*
+ * 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.
+ */
+//! The metadata and doc strings that Rust records for exported and global
+//! functions match the ones C++ records for the same signatures in
+//! `libtvm_ffi_testing`, byte for byte. Exported functions have them with the
+//! `export-metadata` feature, as C++ ones with
+//! `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`; global functions always do.
+
+use tvm_ffi::function_internal::AsPackedCallable;
+#[cfg(feature = "export-metadata")]
+use tvm_ffi::tvm_ffi_sys::TVMFFIAny;
+use tvm_ffi::*;
+
+// Exported by libtvm_ffi_testing with `TVM_FFI_DLL_EXPORT_TYPED_FUNC` and
+// `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`, for `int64_t(int64_t)`.
+#[cfg(feature = "export-metadata")]
+extern "C" {
+ fn __tvm_ffi__metadata_testing_dll_schema_id_int(
+ handle: *mut std::ffi::c_void,
+ args: *const TVMFFIAny,
+ num_args: i32,
+ result: *mut TVMFFIAny,
+ ) -> i32;
+}
+
+/// Calls an exported metadata or doc getter.
+#[cfg(feature = "export-metadata")]
+fn call_getter(getter: tvm_ffi::tvm_ffi_sys::TVMFFISafeCallType) -> String {
+ // SAFETY: the getters take no arguments and do not use the handle.
+ let getter = unsafe { Function::from_extern_c(std::ptr::null_mut(),
getter, None) };
+ getter.call_packed(&[]).unwrap().try_into().unwrap()
+}
+
+/// The dummy call keeps `libtvm_ffi_testing`, which registers the C++
+/// functions compared with, linked.
+fn global_metadata(name: &str) -> String {
+ assert_eq!(unsafe { tvm_ffi_sys::TVMFFITestingDummyTarget() }, 0);
+ Function::get_global("ffi.GetGlobalFuncMetadata")
+ .unwrap()
+ .call_tuple_with_len::<1, _>((String::from(name),))
+ .unwrap()
+ .try_into()
+ .unwrap()
+}
+
+fn rust_schema_id_int(x: i64) -> Result<i64> {
+ Ok(x)
+}
+tvm_ffi_dll_export_typed_func!(rust_schema_id_int, rust_schema_id_int);
+
+#[cfg(feature = "export-metadata")]
+#[test]
+fn test_exported_metadata_matches_cpp() {
+ let rust = call_getter(__tvm_ffi__metadata_rust_schema_id_int);
+ let cpp = call_getter(__tvm_ffi__metadata_testing_dll_schema_id_int);
+ assert_eq!(rust.as_str(), cpp.as_str());
+}
+
+fn rust_add(a: i64, b: i64) -> Result<i64> {
+ Ok(a + b)
+}
+tvm_ffi_dll_export_typed_func!(rust_add, rust_add);
+tvm_ffi_dll_export_typed_func_doc!(
+ rust_add,
+ "Add two integers and return the sum.\n\n\"a\" and \"b\" are integers."
+);
+
+#[cfg(feature = "export-metadata")]
+#[test]
+fn test_exported_doc_is_verbatim() {
+ let doc = call_getter(__tvm_ffi__doc_rust_add);
+ assert_eq!(
+ doc.as_str(),
+ "Add two integers and return the sum.\n\n\"a\" and \"b\" are integers."
+ );
+}
+
+/// A callable whose signature is not known, as a crate may define to export a
+/// packed function.
+struct Packed(fn(&[AnyView]) -> Result<Any>);
+struct PackedArgs;
+
+impl AsPackedCallable<PackedArgs, Any> for Packed {
+ fn call_packed(&self, packed_args: &[AnyView]) -> Result<Any> {
+ (self.0)(packed_args)
+ }
+}
+
+fn rust_packed(_args: &[AnyView]) -> Result<Any> {
+ Ok(Any::new())
+}
+tvm_ffi_dll_export_typed_func!(rust_packed, Packed(rust_packed));
+
+#[cfg(feature = "export-metadata")]
+#[test]
+fn test_exported_packed_metadata_is_untyped() {
+ register_globals();
+ let rust = call_getter(__tvm_ffi__metadata_rust_packed);
+ assert_eq!(
+ rust.as_str(),
+ global_metadata("testing.schema_packed").as_str()
+ );
+}
+
+// Without `export-metadata`, the exports define no metadata or doc getters:
+// these symbols of the same names would otherwise be defined twice.
+#[cfg(not(feature = "export-metadata"))]
+mod without_export_metadata {
+ #[no_mangle]
+ pub extern "C" fn __tvm_ffi__metadata_rust_schema_id_int() {}
+ #[no_mangle]
+ pub extern "C" fn __tvm_ffi__metadata_rust_packed() {}
+ #[no_mangle]
+ pub extern "C" fn __tvm_ffi__doc_rust_add() {}
+}
+
+#[cfg(not(feature = "export-metadata"))]
+#[test]
+fn test_exports_without_metadata() {
+ // The functions themselves are exported.
+ // SAFETY: rust_add takes two arguments and does not use the handle.
+ let add = unsafe { Function::from_extern_c(std::ptr::null_mut(),
__tvm_ffi_rust_add, None) };
+ let sum = add.call_tuple_with_len::<2, _>((1i64, 2i64)).unwrap();
+ assert_eq!(i64::try_from(sum).unwrap(), 3);
+ without_export_metadata::__tvm_ffi__metadata_rust_schema_id_int();
+ without_export_metadata::__tvm_ffi__metadata_rust_packed();
+ without_export_metadata::__tvm_ffi__doc_rust_add();
+}
+
+/// Registers `func` as `testing.rust.<name>` and records `name`.
+fn register_typed<F, I, O>(names: &mut Vec<&'static str>, name: &'static str,
func: F)
+where
+ F: AsPackedCallable<I, O> + 'static,
+{
+ Function::register_global_typed(&format!("testing.rust.{name}"), func,
"").unwrap();
+ names.push(name);
+}
+
+// The runtime does not guard its global function table against readers on
+// other threads, so each test registers every global here before it reads
+// the table. Returns the names of the typed globals that mirror a C++
+// `testing.<name>` global.
+fn register_globals() -> &'static [&'static str] {
+ static NAMES: std::sync::OnceLock<Vec<&'static str>> =
std::sync::OnceLock::new();
+ NAMES.get_or_init(|| {
+ let mut names = Vec::new();
+ let n = &mut names;
+ register_typed(n, "schema_id_float", |x: f64| -> Result<f64> { Ok(x)
});
+ register_typed(n, "schema_id_bool", |x: bool| -> Result<bool> { Ok(x)
});
+ register_typed(n, "schema_id_device", |x: DLDevice| ->
Result<DLDevice> {
+ Ok(x)
+ });
+ register_typed(
+ n,
+ "schema_id_dtype",
+ |x: DLDataType| -> Result<DLDataType> { Ok(x) },
+ );
+ register_typed(n, "schema_id_string", |x: String| -> Result<String> {
+ Ok(x)
+ });
+ register_typed(n, "schema_id_bytes", |x: Bytes| -> Result<Bytes> {
Ok(x) });
+ register_typed(n, "schema_id_func", |x: Function| -> Result<Function> {
+ Ok(x)
+ });
+ register_typed(
+ n,
+ "schema_id_object",
+ |x: object::ObjectRef| -> Result<object::ObjectRef> { Ok(x) },
+ );
+ register_typed(n, "schema_id_tensor", |x: Tensor| -> Result<Tensor> {
+ Ok(x)
+ });
+ register_typed(
+ n,
+ "schema_tensor_view_input",
+ |_: TensorView| -> Result<()> { Ok(()) },
+ );
+ register_typed(
+ n,
+ "schema_id_opt_int",
+ |x: Option<i64>| -> Result<Option<i64>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_opt_str",
+ |x: Option<String>| -> Result<Option<String>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_opt_obj",
+ |x: Option<object::ObjectRef>| ->
Result<Option<object::ObjectRef>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_arr_int",
+ |x: Array<i64>| -> Result<Array<i64>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_arr_str",
+ |x: Array<String>| -> Result<Array<String>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_arr_obj",
+ |x: Array<object::ObjectRef>| -> Result<Array<object::ObjectRef>>
{ Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_map_str_int",
+ |x: Map<String, i64>| -> Result<Map<String, i64>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_map_str_str",
+ |x: Map<String, String>| -> Result<Map<String, String>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_id_map_str_obj",
+ |x: Map<String, object::ObjectRef>| -> Result<Map<String,
object::ObjectRef>> { Ok(x) },
+ );
+ register_typed(
+ n,
+ "schema_arr_map_opt",
+ |_: Array<Option<i64>>,
+ mp: Map<String, Array<i64>>,
+ _: Option<String>|
+ -> Result<Map<String, Array<i64>>> { Ok(mp) },
+ );
+ register_typed(n, "schema_no_args", || -> Result<i64> { Ok(1) });
+ register_typed(n, "schema_no_return", |_: i64| -> Result<()> { Ok(())
});
+ register_typed(n, "schema_no_args_no_return", || -> Result<()> {
Ok(()) });
+
+ let packed = Function::from_packed(|_: &[AnyView]| -> Result<Any> {
Ok(Any::new()) });
+ Function::register_global("testing.rust.schema_packed",
packed).unwrap();
+ Function::register_global_typed(
+ "testing.rust.add",
+ |a: i64, b: i64| -> Result<i64> { Ok(a + b) },
+ "Add two integers.",
+ )
+ .unwrap();
+ names
+ })
+}
+
+#[test]
+fn test_global_metadata_matches_cpp() {
+ let names = register_globals();
+ assert_eq!(names.len(), 23);
+ for name in names {
+ assert_eq!(
+ global_metadata(&format!("testing.rust.{name}")).as_str(),
+ global_metadata(&format!("testing.{name}")).as_str(),
+ "{name}"
+ );
+ }
+}
+
+#[test]
+fn test_global_untyped_metadata_matches_cpp() {
+ register_globals();
+ assert_eq!(
+ global_metadata("testing.rust.schema_packed").as_str(),
+ global_metadata("testing.schema_packed").as_str()
+ );
+}
+
+#[test]
+fn test_global_typed_registration_calls_the_function() {
+ register_globals();
+ let add = Function::get_global("testing.rust.add").unwrap();
+ let sum = add.call_tuple_with_len::<2, _>((1i64, 2i64)).unwrap();
+ assert_eq!(i64::try_from(sum).unwrap(), 3);
+}