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 a0e43582 [FEAT][Rust] Register derived object types on first use (#824)
a0e43582 is described below

commit a0e43582f4a369078647cf510d3d665a4e91083b
Author: Gabriel Wu <[email protected]>
AuthorDate: Tue Oct 6 23:20:32 2026 +0800

    [FEAT][Rust] Register derived object types on first use (#824)
    
    ## Why
    
    `#[derive(Object)]` without `#[type_index]` resolves its type key with
    `TVMFFITypeKeyToIndex` and panics when nothing has registered the key. A
    Rust crate therefore cannot define an object type of its own: someone
    else, usually C++, has to register the key first. The tests work around
    this. `test_match_any_ast_lookup.rs` declares
    `TVMFFITypeGetOrAllocIndex` itself and registers its types by hand
    before they are used.
    
    C++ does not have this gap. `TVM_FFI_DECLARE_OBJECT_INFO` calls
    `TVMFFITypeGetOrAllocIndex` with the parent's index, the type depth and
    the child slots. That returns the index the key already has, or
    allocates one under the parent.
    
    ## What
    
    - **Derive:** a type defined in Rust declares `#[type_register]`. Its
    `type_index()` calls `TVMFFITypeGetOrAllocIndex` on first use, passing
    the parent's `type_index()` (which registers the parent first if
    needed), `TYPE_DEPTH` and the child slots. A key that is already
    registered keeps its index.
    - **Bindings are unchanged:** a type without `#[type_register]` binds a
    key its defining library, usually C++, registers. As before, it looks
    the key up with `TVMFFITypeKeyToIndex` and panics if that library is not
    loaded yet. So a binding never registers a C++ type's key with the wrong
    child slots, whatever the load order.
    - **New attributes** `#[type_child_slots = N]` and
    `#[type_child_slots_can_overflow = bool]` mirror C++ `_type_child_slots`
    and `_type_child_slots_can_overflow`, with the same defaults of 0 and
    true. They need `#[type_register]`, and a `#[type_final]` type cannot
    reserve child slots.
    - **Concurrency:** C++ registers types while a library loads, and the
    type table relies on that (`src/ffi/object.cc`: updates happen during
    loading "or explicitly locked from the caller"). Rust registers on first
    use, possibly on any thread, so registrations from Rust hold one lock,
    `tvm_ffi::object::TYPE_REGISTRATION`; the parent is registered before it
    is taken. The docs state what stays the caller's duty, as for C++: not
    registering while another thread loads a library that registers types.
    Calling `type_index()` before starting such threads registers a type
    eagerly.
    - **tvm-ffi-sys** binds `TVMFFITypeGetOrAllocIndex`.
    - **Test cleanup:** `test_match_any_ast_lookup.rs` drops its own `extern
    "C"` declaration and registers its types with `#[type_register]`.
---
 rust/tvm-ffi-macros/src/lib.rs                  |  33 +++-
 rust/tvm-ffi-macros/src/object_macros.rs        | 101 ++++++++++++-
 rust/tvm-ffi-sys/src/c_api.rs                   |   8 +
 rust/tvm-ffi/src/object.rs                      |   6 +
 rust/tvm-ffi/tests/test_match_any_ast_lookup.rs |  35 +----
 rust/tvm-ffi/tests/test_object_registration.rs  | 190 ++++++++++++++++++++++++
 6 files changed, 341 insertions(+), 32 deletions(-)

diff --git a/rust/tvm-ffi-macros/src/lib.rs b/rust/tvm-ffi-macros/src/lib.rs
index 054836b8..07026e02 100644
--- a/rust/tvm-ffi-macros/src/lib.rs
+++ b/rust/tvm-ffi-macros/src/lib.rs
@@ -49,8 +49,39 @@ pub fn match_any(input: TokenStream) -> TokenStream {
     match_any::expand(input)
 }
 
+/// Derive `ObjectCore` for an object struct whose first field is its parent.
+///
+/// `#[type_key = "..."]` names the type, and `#[type_index(...)]` gives it a
+/// static index. Otherwise a type defined in Rust declares `#[type_register]`:
+/// it is registered under its parent on first use of `type_index()`, or takes
+/// the index its key already has, as C++ `TVM_FFI_DECLARE_OBJECT_INFO` does.
+/// Without `#[type_register]`, the type binds a key its defining library,
+/// usually C++, has registered, and `type_index()` panics if that library is
+/// not loaded yet, so a binding never registers a key with the wrong child
+/// slots.
+///
+/// `#[type_final]`, and with `#[type_register]` `#[type_child_slots = N]` and
+/// `#[type_child_slots_can_overflow = bool]`, mirror C++ `_type_final`,
+/// `_type_child_slots` (default 0) and `_type_child_slots_can_overflow`
+/// (default true).
+///
+/// Registrations from Rust hold one lock, so they do not race each other. The
+/// runtime's type table is not locked otherwise: as for C++, a type must not
+/// be registered while another thread loads a library that registers types.
+/// Calling `type_index()` before starting such threads registers a type
+/// eagerly.
 #[proc_macro_error]
-#[proc_macro_derive(Object, attributes(type_key, type_index, type_final))]
+#[proc_macro_derive(
+    Object,
+    attributes(
+        type_key,
+        type_index,
+        type_final,
+        type_register,
+        type_child_slots,
+        type_child_slots_can_overflow
+    )
+)]
 pub fn derive_object(input: TokenStream) -> TokenStream {
     object_macros::derive_object(input)
 }
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs 
b/rust/tvm-ffi-macros/src/object_macros.rs
index dace824b..807cd476 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -37,8 +37,61 @@ pub fn derive_object(input: proc_macro::TokenStream) -> 
TokenStream {
         None => false,
     };
 
-    // type index can be optional
-    // for now we make it required for static index
+    // Whether this type is defined here, so that it registers its key, as C++
+    // `TVM_FFI_DECLARE_OBJECT_INFO` does. Without it, the type binds a key
+    // that its defining library, usually C++, has registered.
+    let type_register = match get_attr(&derive_input, "type_register") {
+        Some(attr) if matches!(attr.parse_meta(), Ok(syn::Meta::Path(_))) => 
true,
+        Some(_) => panic!("Expect #[type_register] attribute"),
+        None => false,
+    };
+
+    // Reserved child slots and whether children may exceed them, as C++
+    // `_type_child_slots` and `_type_child_slots_can_overflow` declare them.
+    let child_slots = match get_attr(&derive_input, 
"type_child_slots").map(|a| a.parse_meta()) {
+        Some(Ok(syn::Meta::NameValue(syn::MetaNameValue {
+            lit: syn::Lit::Int(n),
+            ..
+        }))) => n
+            .base10_parse::<i32>()
+            .expect("Expect #[type_child_slots = <non-negative integer>]"),
+        Some(_) => panic!("Expect #[type_child_slots = <non-negative 
integer>]"),
+        None => 0,
+    };
+    let child_slots_can_overflow =
+        match get_attr(&derive_input, "type_child_slots_can_overflow").map(|a| 
a.parse_meta()) {
+            Some(Ok(syn::Meta::NameValue(syn::MetaNameValue {
+                lit: syn::Lit::Bool(b),
+                ..
+            }))) => b.value,
+            Some(_) => panic!("Expect #[type_child_slots_can_overflow = 
<bool>]"),
+            None => true,
+        };
+    assert!(
+        child_slots >= 0,
+        "Expect #[type_child_slots = <non-negative integer>]"
+    );
+    assert!(
+        !(type_final && child_slots > 0),
+        "a final object type cannot reserve child slots"
+    );
+    assert!(
+        type_register
+            || (get_attr(&derive_input, "type_child_slots").is_none()
+                && get_attr(&derive_input, 
"type_child_slots_can_overflow").is_none()),
+        "#[type_child_slots] and #[type_child_slots_can_overflow] need 
#[type_register]"
+    );
+    // we expect base always to be the first field
+    let base_ty = match &derive_input.data {
+        syn::Data::Struct(s) => s.fields.iter().next().map(|f| f.ty.clone()),
+        _ => None,
+    }
+    .expect("First field must be `<base_name>: <ObjectCoreType>`");
+
+    // A type with a static index has it; one defined here registers its key
+    // under its parent on first use, or takes the index the key already has,
+    // as C++ `TVM_FFI_DECLARE_OBJECT_INFO` does; any other type binds the
+    // index its defining library registered.
     let type_index_tokens = match get_attr(&derive_input, 
"type_index").map(attr_to_expr) {
         Some(type_index) => {
             let type_index_expr =
@@ -50,6 +103,42 @@ pub fn derive_object(input: proc_macro::TokenStream) -> 
TokenStream {
                 }
             }
         }
+        None if type_register => {
+            quote! {
+                #[inline]
+                fn type_index() -> i32 {
+                    static TYPE_INDEX: std::sync::LazyLock<i32> = 
std::sync::LazyLock::new(||
+                        unsafe {
+                            // The parent first, since registering it takes the
+                            // same lock.
+                            let parent =
+                                <#base_ty as 
#tvm_ffi_crate::object::ObjectCore>::type_index();
+                            let _registering = 
#tvm_ffi_crate::object::TYPE_REGISTRATION
+                                .lock()
+                                .unwrap_or_else(|e| e.into_inner());
+                            let type_key_arg =
+                                 
#tvm_ffi_crate::tvm_ffi_sys::TVMFFIByteArray::from_str(#type_key);
+                            let tindex = 
#tvm_ffi_crate::tvm_ffi_sys::TVMFFITypeGetOrAllocIndex(
+                                &type_key_arg,
+                                -1,
+                                <#struct_name as 
#tvm_ffi_crate::object::ObjectCore>::TYPE_DEPTH,
+                                #child_slots,
+                                #child_slots_can_overflow as i32,
+                                parent,
+                            );
+                            if tindex < 0 {
+                                panic!(
+                                    "Failed to get or allocate type index for 
type key: {}",
+                                    #type_key
+                                );
+                            }
+                            tindex
+                        }
+                    );
+                    *TYPE_INDEX
+                }
+            }
+        }
         None => {
             quote! {
                 #[inline]
@@ -59,11 +148,15 @@ pub fn derive_object(input: proc_macro::TokenStream) -> 
TokenStream {
                             let type_key_arg =
                                  
#tvm_ffi_crate::tvm_ffi_sys::TVMFFIByteArray::from_str(#type_key);
                             let mut tindex = 0;
-                            let ret =  
#tvm_ffi_crate::tvm_ffi_sys::TVMFFITypeKeyToIndex(
+                            let ret = 
#tvm_ffi_crate::tvm_ffi_sys::TVMFFITypeKeyToIndex(
                                 &type_key_arg, &mut tindex
                             );
                             if ret != 0 {
-                                panic!("Failed to get type index for type key: 
{}", #type_key);
+                                panic!(
+                                    "Type key {} is not registered: load the 
library that defines \
+                                     it first, or add #[type_register] if this 
crate defines it",
+                                    #type_key
+                                );
                             }
                             tindex
                         }
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index f7b7c334..7a0dc2f8 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -466,6 +466,14 @@ unsafe extern "C" {
     pub fn TVMFFISetCustomAllocator(allocator: *mut TVMFFICustomAllocator) -> 
i32;
 
     pub fn TVMFFITypeKeyToIndex(type_key: *const TVMFFIByteArray, out_tindex: 
*mut i32) -> i32;
+    pub fn TVMFFITypeGetOrAllocIndex(
+        type_key: *const TVMFFIByteArray,
+        static_type_index: i32,
+        type_depth: i32,
+        num_child_slots: i32,
+        child_slots_can_overflow: i32,
+        parent_type_index: i32,
+    ) -> i32;
     pub fn TVMFFITypeRegisterAttr(
         type_index: i32,
         attr_name: *const TVMFFIByteArray,
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index f05d1a09..93306bdc 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -25,6 +25,12 @@ pub use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 /// Object related ABI handling
 use tvm_ffi_sys::{TVMFFIAny, TVMFFIGetTypeInfo, TVMFFIObject, 
COMBINED_REF_COUNT_BOTH_ONE};
 
+/// Held while `#[derive(Object)]` with `#[type_register]` registers a type, so
+/// that registrations from Rust do not race each other: the runtime does not
+/// lock its type table and leaves that to callers.
+#[doc(hidden)]
+pub static TYPE_REGISTRATION: std::sync::Mutex<()> = std::sync::Mutex::new(());
+
 /// Object type is by default the TVMFFIObject
 #[repr(C)]
 pub struct Object {
diff --git a/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs 
b/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
index 7d69189c..ad7ce02a 100644
--- a/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
+++ b/rust/tvm-ffi/tests/test_match_any_ast_lookup.rs
@@ -20,38 +20,19 @@
 use tvm_ffi::derive::{Object, ObjectRef};
 use tvm_ffi::object::{Object as ObjectBase, ObjectArc, ObjectCore};
 use tvm_ffi::{match_any, Any, Array, Shape, TypeIndex};
-use tvm_ffi_sys::TVMFFIByteArray;
-
-unsafe extern "C" {
-    fn TVMFFITypeGetOrAllocIndex(
-        type_key: *const TVMFFIByteArray,
-        static_type_index: i32,
-        type_depth: i32,
-        num_child_slots: i32,
-        child_slots_can_overflow: i32,
-        parent_type_index: i32,
-    ) -> i32;
-}
 
 #[repr(C)]
 #[derive(Object)]
 #[type_key = "testing.match_any.Expr"]
+#[type_register]
+#[type_child_slots = 20]
+#[type_child_slots_can_overflow = false]
 struct ExprObj {
     base: ObjectBase,
 }
 
-fn register_type<T: ObjectCore>(num_child_slots: i32, parent_type_index: i32) 
-> i32 {
-    let type_key = unsafe { TVMFFIByteArray::from_str(T::TYPE_KEY) };
-    let type_index = unsafe {
-        TVMFFITypeGetOrAllocIndex(
-            &type_key,
-            -1,
-            T::TYPE_DEPTH,
-            num_child_slots,
-            0,
-            parent_type_index,
-        )
-    };
+fn register_type<T: ObjectCore>() -> i32 {
+    let type_index = T::type_index();
     assert!(type_index >= TypeIndex::kTVMFFIStaticObjectBegin as i32);
     type_index
 }
@@ -62,6 +43,7 @@ macro_rules! define_expr_leaves {
             #[repr(C)]
             #[derive(Object)]
             #[type_key = $type_key]
+            #[type_register]
             #[type_final]
             struct $object {
                 base: ExprObj,
@@ -87,10 +69,9 @@ macro_rules! define_expr_leaves {
         )+
 
         fn register_expr_types() {
-            let expr_type_index =
-                register_type::<ExprObj>(20, 
TypeIndex::kTVMFFIStaticObjectBegin as i32);
+            register_type::<ExprObj>();
             $(
-                register_type::<$object>(0, expr_type_index);
+                register_type::<$object>();
             )+
         }
     };
diff --git a/rust/tvm-ffi/tests/test_object_registration.rs 
b/rust/tvm-ffi/tests/test_object_registration.rs
new file mode 100644
index 00000000..2226c5ab
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_object_registration.rs
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+use tvm_ffi::derive::{Object, ObjectRef};
+use tvm_ffi::object::{is_instance_of, ObjectRef};
+use tvm_ffi::*;
+use tvm_ffi_sys::{TVMFFIByteArray, TVMFFIGetTypeInfo, TVMFFITypeKeyToIndex};
+
+// Type keys that nothing else registers: deriving `Object` with
+// `#[type_register]` registers them on first use, under their parents, as C++
+// `TVM_FFI_DECLARE_OBJECT_INFO` does.
+
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "testing.rust.RegisteredBase"]
+#[type_register]
+#[type_child_slots = 2]
+#[type_child_slots_can_overflow = false]
+struct RegisteredBaseObj {
+    base: Object,
+    value: i64,
+}
+
+#[repr(C)]
+#[derive(ObjectRef, Clone)]
+struct RegisteredBase {
+    data: ObjectArc<RegisteredBaseObj>,
+}
+
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "testing.rust.RegisteredLeaf"]
+#[type_register]
+#[type_final]
+struct RegisteredLeafObj {
+    base: RegisteredBaseObj,
+    extra: i64,
+}
+
+#[repr(C)]
+#[derive(ObjectRef, Clone)]
+struct RegisteredLeaf {
+    data: ObjectArc<RegisteredLeafObj>,
+}
+
+tvm_ffi::impl_object_upcast!(RegisteredLeaf => RegisteredBase);
+
+fn registered_index(type_key: &str) -> i32 {
+    let key = unsafe { TVMFFIByteArray::from_str(type_key) };
+    let mut index = -1;
+    assert_eq!(unsafe { TVMFFITypeKeyToIndex(&key, &mut index) }, 0);
+    index
+}
+
+#[test]
+fn test_derived_object_types_are_registered_on_first_use() {
+    // The leaf comes first: registering it registers its parent before it.
+    let leaf_index = RegisteredLeafObj::type_index();
+    let base_index = RegisteredBaseObj::type_index();
+    assert!(base_index >= TypeIndex::kTVMFFIDynObjectBegin as i32);
+    assert_eq!(registered_index("testing.rust.RegisteredBase"), base_index);
+    assert_eq!(registered_index("testing.rust.RegisteredLeaf"), leaf_index);
+    // The leaf takes the first of its parent's reserved child slots.
+    assert_eq!(leaf_index, base_index + 1);
+    // Registration happens once; the index is stable.
+    assert_eq!(RegisteredLeafObj::type_index(), leaf_index);
+
+    let info = unsafe { &*TVMFFIGetTypeInfo(leaf_index) };
+    assert_eq!(info.type_depth, RegisteredLeafObj::TYPE_DEPTH);
+    let parent = unsafe { &**info.type_acenstors.add(1) };
+    assert_eq!(parent.type_index, base_index);
+
+    assert!(is_instance_of::<RegisteredBaseObj>(leaf_index));
+    assert!(is_instance_of::<Object>(base_index));
+    assert!(!is_instance_of::<RegisteredLeafObj>(base_index));
+}
+
+#[test]
+fn test_registered_object_types_round_trip_through_any() {
+    let leaf = RegisteredLeaf {
+        data: ObjectArc::new(RegisteredLeafObj {
+            base: RegisteredBaseObj {
+                base: Object::new(),
+                value: 7,
+            },
+            extra: 8,
+        }),
+    };
+    let any = Any::from(leaf);
+    assert_eq!(any.type_index(), RegisteredLeafObj::type_index());
+    let base = RegisteredBase::try_from(any).unwrap();
+    assert_eq!(base.data.value, 7);
+    let obj: ObjectRef = base.try_cast().unwrap();
+    let leaf: RegisteredLeaf = obj.try_cast().unwrap();
+    assert_eq!(leaf.data.extra, 8);
+}
+
+// Types defined here, registered from several threads at once.
+macro_rules! concurrent_types {
+    ($($name:ident = $key:literal),*) => {
+        $(
+            #[repr(C)]
+            #[derive(Object)]
+            #[type_key = $key]
+            #[type_register]
+            struct $name {
+                base: Object,
+            }
+        )*
+    };
+}
+
+concurrent_types!(
+    Concurrent0 = "testing.rust.Concurrent0",
+    Concurrent1 = "testing.rust.Concurrent1",
+    Concurrent2 = "testing.rust.Concurrent2",
+    Concurrent3 = "testing.rust.Concurrent3",
+    Concurrent4 = "testing.rust.Concurrent4",
+    Concurrent5 = "testing.rust.Concurrent5",
+    Concurrent6 = "testing.rust.Concurrent6",
+    Concurrent7 = "testing.rust.Concurrent7"
+);
+
+#[test]
+fn test_registration_from_threads() {
+    let registrations: [fn() -> i32; 8] = [
+        <Concurrent0 as ObjectCore>::type_index,
+        <Concurrent1 as ObjectCore>::type_index,
+        <Concurrent2 as ObjectCore>::type_index,
+        <Concurrent3 as ObjectCore>::type_index,
+        <Concurrent4 as ObjectCore>::type_index,
+        <Concurrent5 as ObjectCore>::type_index,
+        <Concurrent6 as ObjectCore>::type_index,
+        <Concurrent7 as ObjectCore>::type_index,
+    ];
+    let barrier = std::sync::Barrier::new(registrations.len());
+    let indices: Vec<i32> = std::thread::scope(|scope| {
+        let handles: Vec<_> = registrations
+            .iter()
+            .map(|register| {
+                let barrier = &barrier;
+                scope.spawn(move || {
+                    barrier.wait();
+                    register()
+                })
+            })
+            .collect();
+        handles.into_iter().map(|h| h.join().unwrap()).collect()
+    });
+    for (i, &index) in indices.iter().enumerate() {
+        assert_eq!(
+            index,
+            registered_index(&format!("testing.rust.Concurrent{i}"))
+        );
+        assert!(
+            !indices[..i].contains(&index),
+            "type indices must be distinct"
+        );
+    }
+}
+
+// A type that binds a key its defining library registers, without
+// `#[type_register]`, does not register it.
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "testing.rust.NeverRegistered"]
+struct UnregisteredBindingObj {
+    base: Object,
+}
+
+#[test]
+#[should_panic(expected = "is not registered: load the library that defines it 
first")]
+fn test_binding_does_not_register() {
+    let _ = <UnregisteredBindingObj as ObjectCore>::type_index();
+}

Reply via email to