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 ded7531f [FEAT][Rust] Register fields and methods of object types 
defined in Rust (#834)
ded7531f is described below

commit ded7531f1bbc5b43da7929f155ac890bfe31d82a
Author: Gabriel Wu <[email protected]>
AuthorDate: Tue Oct 27 10:02:21 2026 +0800

    [FEAT][Rust] Register fields and methods of object types defined in Rust 
(#834)
    
    ## Summary
    
    Since this PR was opened, #859 and #869 landed the field side of Rust
    object reflection
    (`#[reflect]` fields, `rs_class`). The branch is now rebased on `main`
    after #832 and narrowed
    to what `main` still lacks: one commit, f4907aa.
    
    A `#[type_register]` type registers its `#[reflect]` fields, but not its
    methods, so Python saw
    a Rust object without methods; and every field registered an empty doc
    string. This PR
    registers both, as C++ `refl::ObjectDef<T>().def(...)` and `def_ro("x",
    &T::x, "doc")` do:
    
    ```rust
    use tvm_ffi::derive::{Object, ObjectRef};
    use tvm_ffi::reflection::ObjectDef;
    use tvm_ffi::{Object, ObjectArc, Result};
    
    #[repr(C)]
    #[derive(Object)]
    #[type_key = "my.Point"]
    #[type_register]
    #[type_reflection(PointObj::reflect_methods)]
    pub struct PointObj {
        base: Object,
        /// The x coordinate.
        #[reflect]
        pub x: i64,
        #[reflect(writable, doc = "The y coordinate.")]
        pub y: i64,
        #[reflect(name = "label")]
        pub name: tvm_ffi::String,
    }
    
    impl PointObj {
        fn reflect_methods(def: &mut ObjectDef<Self>) {
            def.def("norm1", |p: Point| -> Result<i64> { Ok(p.data.x.abs() + 
p.data.y.abs()) }, "The L1 norm.")
                .def_static("origin", || -> Result<Point> { Ok(origin()) }, 
"The origin.");
        }
    }
    ```
    ---------
    
    Co-authored-by: Yuchuan <[email protected]>
---
 docs/guides/rust_lang_guide.md               |  41 +++--
 rust/tvm-ffi-macros/src/lib.rs               |  46 +++---
 rust/tvm-ffi-macros/src/object_macros.rs     | 169 +++++++++++++++++----
 rust/tvm-ffi-macros/src/rs_class.rs          |   9 +-
 rust/tvm-ffi-sys/src/c_api.rs                |   5 +
 rust/tvm-ffi/src/lib.rs                      |   2 +-
 rust/tvm-ffi/src/reflection.rs               |   4 +-
 rust/tvm-ffi/src/reflection/registry.rs      | 141 +++++++++++++++---
 rust/tvm-ffi/tests/test_object_reflection.rs | 214 ++++++++++++++++++++++++++-
 rust/tvm-ffi/tests/test_rs_class.rs          |  43 +++++-
 10 files changed, 580 insertions(+), 94 deletions(-)

diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 8309bfd7..c706a4a9 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -233,18 +233,21 @@ first `type_index()`, which allocating an instance calls:
 
 ```rust
 use tvm_ffi::derive::{Object, ObjectRef};
-use tvm_ffi::{Object, ObjectArc};
+use tvm_ffi::reflection::ObjectDef;
+use tvm_ffi::{Object, ObjectArc, Result};
 
 #[repr(C)]
 #[derive(Object)]
 #[type_key = "my.Point"]
 #[type_register]
 #[type_s_eq_hash_kind = "TreeNode"]
+#[type_reflection(PointObj::reflect_methods)]
 pub struct PointObj {
     base: Object,
+    /// The x coordinate.
     #[reflect]
     pub x: i64,
-    #[reflect(writable, seq_hash_ignore)]
+    #[reflect(writable, seq_hash_ignore, doc = "A label.")]
     pub label: tvm_ffi::String,
 }
 
@@ -253,21 +256,39 @@ pub struct PointObj {
 pub struct Point {
     data: ObjectArc<PointObj>,
 }
+
+impl PointObj {
+    fn reflect_methods(def: &mut ObjectDef<Self>) {
+        def.def("x_abs", |p: Point| -> Result<i64> { Ok(p.data.x.abs()) }, 
"The |x|.")
+            .def_static("zero", || -> Result<i64> { Ok(0) }, "");
+    }
+}
 ```
 
-Each reflected field gets a C ABI getter and setter and its type schema, so
-C++ structural equality and hashing, `ffi.ReprPrint`, `ffi.DeepCopy` and
-Python see it. The flags in `#[reflect(...)]` name
-`tvm_ffi::FieldFlags` constants. Without `#[type_s_eq_hash_kind]`, a type
-keeps its nearest reflected ancestor's kind. The derive also implements
-`ShallowCopy` and registers it as `__ffi_shallow_copy__`, which requires the
-parent to implement `ShallowCopy` and the other fields to be `Clone`.
+Each reflected field gets a C ABI getter and setter, its doc string and its
+type schema, so C++ structural equality and hashing, `ffi.ReprPrint`,
+`ffi.DeepCopy` and Python see it. The flags in `#[reflect(...)]` name
+`tvm_ffi::FieldFlags` constants; `name = "..."` registers the field under
+another name, and `doc = "..."` sets its doc string, otherwise the field's
+`///` doc comment. Without `#[type_s_eq_hash_kind]`, a type keeps its
+nearest reflected ancestor's kind. The derive also implements `ShallowCopy`
+and registers it as `__ffi_shallow_copy__`, which requires the parent to
+implement `ShallowCopy` and the other fields to be `Clone`.
+
+`#[type_reflection(path)]` names a function that registers the type's
+methods with `def` (the object is the first parameter) and `def_static`, as
+C++ `refl::ObjectDef<T>().def(...)` does, each with its type schema and doc
+string, where `Function::from_type_method` and Python find them. It runs
+while the type registers, so it must not call `type_index()` of the type or
+of another Rust type that is not registered yet. Method names, like
+reflected field names, must be unique in the type.
 
 ### Defining Object Types from Their Fields
 
 `#[rs_class]` defines such a type from its object struct, like Python's
 `py_class`. The first field, `base`, is the parent's object; every other field
-is reflected, and `#[reflect(...)]` sets its flags.
+is reflected, and `#[reflect(...)]` sets its flags, name and doc string.
+`reflection = path` after the type key is `#[type_reflection(path)]`.
 `def_object_ref!(TypeName, ParentType, ObjectName)` defines the reference, as
 C++ `TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE` does:
 
diff --git a/rust/tvm-ffi-macros/src/lib.rs b/rust/tvm-ffi-macros/src/lib.rs
index 5af23be1..9297f36a 100644
--- a/rust/tvm-ffi-macros/src/lib.rs
+++ b/rust/tvm-ffi-macros/src/lib.rs
@@ -69,17 +69,22 @@ pub fn match_any(input: TokenStream) -> TokenStream {
 ///
 /// With `#[type_register]`, fields marked `#[reflect]` are registered with the
 /// type table when the type is, as C++ `refl::ObjectDef<T>().def_ro(...)`
-/// does: a getter, a setter and the type schema per field, the metadata, and
-/// the `__ffi_shallow_copy__` and `__ffi_type_final__` type attributes. C++
-/// structural equality and hashing, `repr`, deep copy and Python then see
-/// the fields. `#[reflect(writable, seq_hash_ignore, ...)]` adds the
-/// [`FieldFlags`] of those names. `#[type_s_eq_hash_kind = "TreeNode"]`
-/// names the [`SEqHashKind`]; without it the type keeps its nearest reflected
-/// ancestor's, as a C++ subclass inherits `_type_s_eq_hash_kind`. A type with
-/// either attribute also gets `ShallowCopy`, which shallow-copies the parent
-/// and clones every other field, so the parent must implement `ShallowCopy`
-/// (`Object` and reflected Rust types do) and the fields must be `Clone`; a
-/// reflected field's type must implement `AnyCompatible`.
+/// does: a getter, a setter, the doc string and the type schema per field,
+/// the metadata, and the `__ffi_shallow_copy__` and `__ffi_type_final__`
+/// type attributes. C++ structural equality and hashing, `repr`, deep copy
+/// and Python then see the fields. `#[reflect(writable, seq_hash_ignore,
+/// ...)]` adds the [`FieldFlags`] of those names, `name = "..."` registers
+/// the field under another name, and `doc = "..."` sets its doc string,
+/// otherwise the field's `///` doc comment. `#[type_s_eq_hash_kind =
+/// "TreeNode"]` names the [`SEqHashKind`]; without it the type keeps its
+/// nearest reflected ancestor's, as a C++ subclass inherits
+/// `_type_s_eq_hash_kind`. `#[type_reflection(path)]` names a function
+/// `fn(&mut ObjectDef<Self>)` that registers the type's methods, as C++
+/// `refl::ObjectDef<T>().def(...)` does. A type with any of these attributes
+/// also gets `ShallowCopy`, which shallow-copies the parent and clones every
+/// other field, so the parent must implement `ShallowCopy` (`Object` and
+/// reflected Rust types do) and the fields must be `Clone`; a reflected
+/// field's type must implement `AnyCompatible`.
 ///
 /// [`FieldFlags`]: ../tvm_ffi/reflection/struct.FieldFlags.html
 /// [`SEqHashKind`]: ../tvm_ffi/reflection/enum.SEqHashKind.html
@@ -100,6 +105,7 @@ pub fn match_any(input: TokenStream) -> TokenStream {
         type_child_slots,
         type_child_slots_can_overflow,
         type_s_eq_hash_kind,
+        type_reflection,
         reflect
     )
 )]
@@ -142,7 +148,8 @@ pub fn derive_object_ref(input: TokenStream) -> TokenStream 
{
 /// must implement `ShallowCopy`, as `rs_class` types and non-final C++ types
 /// bound by `tvm-ffi-stubgen` do. Every other field is reflected;
 /// `#[reflect(writable, seq_hash_ignore, ...)]` gives a field the
-/// [`FieldFlags`] of those names. The macro adds to the struct:
+/// [`FieldFlags`] of those names, and `name` and `doc` as for
+/// `derive(Object)`. The macro adds to the struct:
 ///
 /// - `#[repr(C)]` and `Deref` to the parent's object, so `obj.field` reads the
 ///   fields of both.
@@ -157,18 +164,19 @@ pub fn derive_object_ref(input: TokenStream) -> 
TokenStream {
 ///
 /// After the type key:
 ///
-/// - `final`, `s_eq_hash_kind = "TreeNode"`, `child_slots = N` and
-///   `child_slots_can_overflow = bool` are `derive(Object)`'s `#[type_final]`,
-///   `#[type_s_eq_hash_kind]`, `#[type_child_slots]` and
-///   `#[type_child_slots_can_overflow]`.
+/// - `final`, `s_eq_hash_kind = "TreeNode"`, `child_slots = N`,
+///   `child_slots_can_overflow = bool` and `reflection = path` are
+///   `derive(Object)`'s `#[type_final]`, `#[type_s_eq_hash_kind]`,
+///   `#[type_child_slots]`, `#[type_child_slots_can_overflow]` and
+///   `#[type_reflection]`.
 /// - `crate = path` is where the generated code finds tvm-ffi
 ///   (`crate = ::my_lib::tvm_ffi`), for a crate that depends on it only 
through
 ///   a re-export; by default `tvm_ffi`.
 ///
 /// The type is a `derive(Object)` type with `#[type_register]`: it registers 
on
-/// its first `type_index()`, with the fields, `__ffi_shallow_copy__` and
-/// `__ffi_type_final__`, and without `__ffi_init__`. A field's type must
-/// implement `AnyCompatible` and `Clone`.
+/// its first `type_index()`, with the fields, the methods,
+/// `__ffi_shallow_copy__` and `__ffi_type_final__`, and without
+/// `__ffi_init__`. A field's type must implement `AnyCompatible` and `Clone`.
 ///
 /// [`FieldFlags`]: ../tvm_ffi/reflection/struct.FieldFlags.html
 #[proc_macro_error]
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs 
b/rust/tvm-ffi-macros/src/object_macros.rs
index 9064cf81..49f9584d 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -105,7 +105,8 @@ pub(crate) fn expand_object(
     let reflection = Reflection::parse(derive_input, reflect_always);
     assert!(
         reflection.is_none() || (type_register && get_attr(derive_input, 
"type_index").is_none()),
-        "#[reflect] and #[type_s_eq_hash_kind] need #[type_register] and no 
#[type_index]"
+        "#[reflect], #[type_s_eq_hash_kind] and #[type_reflection] need 
#[type_register] and no \
+         #[type_index]"
     );
     let (register_reflection, shallow_copy_impl) = match &reflection {
         Some(reflection) => (
@@ -251,16 +252,100 @@ pub(crate) fn expand_object(
     }
 }
 
-/// The reflection a `#[type_register]` type declares: its `#[reflect]` fields
-/// and its `#[type_s_eq_hash_kind]`.
+/// The reflection a `#[type_register]` type declares: its `#[reflect]` fields,
+/// its `#[type_s_eq_hash_kind]` and its `#[type_reflection]` function.
 struct Reflection {
     /// The structural kind; `None` inherits the parent's.
     kind: Option<syn::Ident>,
+    /// The function `#[type_reflection(path)]` names, which registers methods.
+    methods: Option<syn::Path>,
     /// The parent's field name and every other field's name.
     base_id: syn::Ident,
     field_ids: Vec<syn::Ident>,
-    /// The reflected fields: name, type and flag names.
-    reflected: Vec<(syn::Ident, syn::Type, Vec<syn::Ident>)>,
+    /// The reflected fields.
+    reflected: Vec<ReflectedField>,
+}
+
+/// A `#[reflect]` field: its identifier, type, flag names, and the name and
+/// doc string registered for it.
+struct ReflectedField {
+    id: syn::Ident,
+    ty: syn::Type,
+    flags: Vec<syn::Ident>,
+    name: String,
+    doc: String,
+}
+
+const EXPECTED_REFLECT: &str =
+    "Expect #[reflect] or #[reflect(<flag>, ..., name = \"<name>\", doc = 
\"<doc>\")]";
+
+impl ReflectedField {
+    /// The field's reflection from its `#[reflect]` attribute, whose `name`
+    /// and `doc` default to the field's name and its `///` doc comment.
+    fn parse(field: &syn::Field, attr: &syn::Attribute) -> Self {
+        let id = field
+            .ident
+            .clone()
+            .expect("#[derive(Object)] needs named fields");
+        let mut flags = Vec::new();
+        let mut name = None;
+        let mut doc = None;
+        let nested = match attr.parse_meta() {
+            Ok(syn::Meta::Path(_)) => Vec::new(),
+            Ok(syn::Meta::List(list)) => list.nested.into_iter().collect(),
+            _ => panic!("{EXPECTED_REFLECT}"),
+        };
+        for item in nested {
+            match item {
+                syn::NestedMeta::Meta(syn::Meta::Path(path)) => {
+                    
flags.push(path.get_ident().cloned().expect(EXPECTED_REFLECT));
+                }
+                syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue {
+                    path,
+                    lit: syn::Lit::Str(value),
+                    ..
+                })) if path.is_ident("name") || path.is_ident("doc") => {
+                    let slot = if path.is_ident("name") {
+                        &mut name
+                    } else {
+                        &mut doc
+                    };
+                    assert!(
+                        slot.is_none(),
+                        "duplicate `{}` in #[reflect] of field `{id}`",
+                        path.get_ident().unwrap()
+                    );
+                    *slot = Some(value.value());
+                }
+                _ => panic!("{EXPECTED_REFLECT}"),
+            }
+        }
+        Self {
+            name: name.unwrap_or_else(|| id.unraw().to_string()),
+            doc: doc.unwrap_or_else(|| doc_comment(&field.attrs)),
+            id,
+            ty: field.ty.clone(),
+            flags,
+        }
+    }
+}
+
+/// The `///` doc comment of an item, as rustdoc reads it: the lines of its
+/// `#[doc = "..."]` attributes without their leading space, joined by 
newlines.
+fn doc_comment(attrs: &[syn::Attribute]) -> String {
+    let lines: Vec<String> = attrs
+        .iter()
+        .filter(|attr| attr.path.is_ident("doc"))
+        .filter_map(|attr| match attr.parse_meta() {
+            Ok(syn::Meta::NameValue(syn::MetaNameValue {
+                lit: syn::Lit::Str(line),
+                ..
+            })) => Some(line.value()),
+            _ => None,
+        })
+        .map(|line| line.strip_prefix(' ').unwrap_or(&line).to_string())
+        .collect();
+    lines.join("\n").trim().to_string()
 }
 
 impl Reflection {
@@ -271,6 +356,10 @@ impl Reflection {
             let kind = attr_to_str(attr);
             syn::Ident::new(&kind.value(), kind.span())
         });
+        let methods = get_attr(derive_input, "type_reflection").map(|attr| {
+            attr.parse_args::<syn::Path>()
+                .expect("Expect #[type_reflection(<path to fn(&mut 
ObjectDef<Self>)>)]")
+        });
         let syn::Data::Struct(s) = &derive_input.data else {
             return None;
         };
@@ -281,38 +370,30 @@ impl Reflection {
             "the parent field cannot be #[reflect]; the parent registers its 
own fields"
         );
         let mut field_ids = Vec::new();
-        let mut reflected = Vec::new();
+        let mut reflected: Vec<ReflectedField> = Vec::new();
         for field in fields {
             let id = field
                 .ident
                 .clone()
                 .expect("#[derive(Object)] needs named fields");
-            field_ids.push(id.clone());
-            let Some(attr) = field.attrs.iter().find(|a| 
a.path.is_ident("reflect")) else {
-                continue;
-            };
-            let flags = match attr.parse_meta() {
-                Ok(syn::Meta::Path(_)) => Vec::new(),
-                Ok(syn::Meta::List(list)) => list
-                    .nested
-                    .iter()
-                    .map(|flag| match flag {
-                        syn::NestedMeta::Meta(syn::Meta::Path(path)) => path
-                            .get_ident()
-                            .cloned()
-                            .expect("Expect #[reflect(<flag>, ...)] with flag 
names"),
-                        _ => panic!("Expect #[reflect(<flag>, ...)] with flag 
names"),
-                    })
-                    .collect(),
-                _ => panic!("Expect #[reflect] or #[reflect(<flag>, ...)]"),
-            };
-            reflected.push((id, field.ty.clone(), flags));
+            field_ids.push(id);
+            if let Some(attr) = field.attrs.iter().find(|a| 
a.path.is_ident("reflect")) {
+                let field = ReflectedField::parse(field, attr);
+                if let Some(other) = reflected.iter().find(|f| f.name == 
field.name) {
+                    panic!(
+                        "fields `{}` and `{}` of `{}` both reflect as `{}`",
+                        other.id, field.id, derive_input.ident, field.name
+                    );
+                }
+                reflected.push(field);
+            }
         }
-        if kind.is_none() && reflected.is_empty() && !always {
+        if kind.is_none() && methods.is_none() && reflected.is_empty() && 
!always {
             return None;
         }
         Some(Self {
             kind,
+            methods,
             base_id: base
                 .ident
                 .clone()
@@ -335,8 +416,14 @@ impl Reflection {
             Some(kind) => quote!(Some(#reflection::SEqHashKind::#kind)),
             None => quote!(None),
         };
-        let fields = self.reflected.iter().map(|(id, ty, flags)| {
-            let name = id.unraw().to_string();
+        let fields = self.reflected.iter().map(|field| {
+            let ReflectedField {
+                id,
+                ty,
+                flags,
+                name,
+                doc,
+            } = field;
             // `#[reflect(seq_hash_ignore)]` names 
`FieldFlags::SEQ_HASH_IGNORE`.
             let flags = flags.iter().map(|flag| {
                 format_ident!("{}", flag.to_string().to_uppercase(), span = 
flag.span())
@@ -344,17 +431,35 @@ impl Reflection {
             quote! {
                 .field::<#ty>(
                     #name,
+                    #doc,
                     ::core::mem::offset_of!(#struct_name, #id),
                     #reflection::FieldFlags::NONE #(| 
#reflection::FieldFlags::#flags)*,
                 )?
             }
         });
+        // The `#[type_reflection]` function registers the methods after the
+        // fields, as a C++ `ObjectDef` chains `def` after `def_ro`.
+        let methods = self.methods.as_ref().map(|path| {
+            // The registration is a `static` initializer, where `Self` does
+            // not resolve, so `Self::f` names the struct.
+            let mut path = path.clone();
+            if let Some(first) = path.segments.first_mut() {
+                if first.ident == "Self" {
+                    first.ident = struct_name.clone();
+                }
+            }
+            quote! {
+                let mut def = def;
+                #path(&mut def);
+            }
+        });
         quote! {
             let registered = (|| -> #tvm_ffi_crate::Result<()> {
-                #reflection::ObjectDef::<#struct_name>::registering(tindex, 
#kind)?
+                let def = 
#reflection::ObjectDef::<#struct_name>::registering(tindex, #kind)?
                     #(#fields)*
-                    .shallow_copy()?
-                    .finish()
+                    .shallow_copy()?;
+                #methods
+                def.finish()
             })();
             if let Err(error) = registered {
                 panic!("Failed to register the reflection of type key {}: {}", 
#type_key, error);
diff --git a/rust/tvm-ffi-macros/src/rs_class.rs 
b/rust/tvm-ffi-macros/src/rs_class.rs
index b9aa39ba..0e34d560 100644
--- a/rust/tvm-ffi-macros/src/rs_class.rs
+++ b/rust/tvm-ffi-macros/src/rs_class.rs
@@ -41,6 +41,7 @@ struct RsClassArgs {
     s_eq_hash_kind: Option<LitStr>,
     child_slots: Option<LitInt>,
     child_slots_can_overflow: Option<LitBool>,
+    reflection: Option<Path>,
     tvm_ffi_crate: Option<Path>,
 }
 
@@ -58,6 +59,7 @@ impl Parse for RsClassArgs {
             s_eq_hash_kind: None,
             child_slots: None,
             child_slots_can_overflow: None,
+            reflection: None,
             tvm_ffi_crate: None,
         };
         let mut seen = Vec::new();
@@ -80,12 +82,14 @@ impl Parse for RsClassArgs {
                 "s_eq_hash_kind" => args.s_eq_hash_kind = Some(input.parse()?),
                 "child_slots" => args.child_slots = Some(input.parse()?),
                 "child_slots_can_overflow" => args.child_slots_can_overflow = 
Some(input.parse()?),
+                "reflection" => args.reflection = Some(input.parse()?),
                 "crate" => args.tvm_ffi_crate = 
Some(Path::parse_mod_style(input)?),
                 _ => {
                     return Err(syn::Error::new(
                         name.span(),
                         "expected `final`, `s_eq_hash_kind = \"<kind>\"`, 
`child_slots = <n>`, \
-                         `child_slots_can_overflow = <bool>` or `crate = 
<path>`",
+                         `child_slots_can_overflow = <bool>`, `reflection = 
<path>` or \
+                         `crate = <path>`",
                     ));
                 }
             }
@@ -187,6 +191,9 @@ fn expand(args: &RsClassArgs, item: &DeriveInput) -> 
syn::Result<TokenStream2> {
     if let Some(overflow) = &args.child_slots_can_overflow {
         derive_attrs.push(quote!(#[type_child_slots_can_overflow = 
#overflow]));
     }
+    if let Some(reflection) = &args.reflection {
+        derive_attrs.push(quote!(#[type_reflection(#reflection)]));
+    }
     let reflected_fields = fields.iter().map(|field| {
         if field.attrs.iter().any(|a| a.path.is_ident("reflect")) {
             quote!(#field)
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index 86ab0a51..7b57d978 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -245,6 +245,11 @@ impl TVMFFIByteArray {
     /// # Returns
     /// * `&str` - The converted str view.
     pub fn as_str(&self) -> &str {
+        // An empty array may hold a null pointer, as the type table stores an
+        // empty doc string.
+        if self.data.is_null() {
+            return "";
+        }
         unsafe { 
std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.data, self.size)) 
}
     }
     /// Unsafe function to create a TVMFFIByteArray from a string
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index c6310957..71fe9d34 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -73,7 +73,7 @@ pub use crate::object::{
 };
 pub use crate::optional::{Optional, OptionalCompatible};
 pub use crate::reflection::{
-    get_type_attr, FieldFlags, FieldGetter, SEqHashKind, ShallowCopy, 
TypeAttrColumn,
+    get_type_attr, FieldFlags, FieldGetter, ObjectDef, SEqHashKind, 
ShallowCopy, TypeAttrColumn,
 };
 pub use crate::rvalue_ref::RValueRef;
 pub use crate::string::{Bytes, String};
diff --git a/rust/tvm-ffi/src/reflection.rs b/rust/tvm-ffi/src/reflection.rs
index 32e69ac1..5ababa16 100644
--- a/rust/tvm-ffi/src/reflection.rs
+++ b/rust/tvm-ffi/src/reflection.rs
@@ -31,9 +31,7 @@ use crate::tvm_ffi_sys::{
 use crate::{Any, AnyView, Error, ObjectCore, Result, TYPE_ERROR};
 
 mod registry;
-#[doc(hidden)]
-pub use registry::ObjectDef;
-pub use registry::{FieldFlags, SEqHashKind, ShallowCopy};
+pub use registry::{FieldFlags, ObjectDef, SEqHashKind, ShallowCopy};
 
 /// A registry-owned type-attribute column indexed by runtime type.
 ///
diff --git a/rust/tvm-ffi/src/reflection/registry.rs 
b/rust/tvm-ffi/src/reflection/registry.rs
index acbb0558..b24807da 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -21,14 +21,17 @@
 //!
 //! [`ObjectDef`] fills the same type table entries as C++ `refl::ObjectDef<T>`
 //! and Python `_register_py_class`: the type metadata, one record per field
-//! with C ABI getter and setter, and the `__ffi_shallow_copy__` and
-//! `__ffi_type_final__` type attributes. Through them the C++ structural
-//! equality, hashing and mutation, `repr`, deep copy and Python's
-//! generated classes reach the fields of a Rust type.
+//! with C ABI getter and setter, one record per method, and the
+//! `__ffi_shallow_copy__` and `__ffi_type_final__` type attributes. Through
+//! them the C++ structural equality, hashing and mutation, `repr`, deep copy
+//! and Python's generated classes reach the fields and methods of a Rust
+//! type.
 //!
-//! Only `#[derive(Object)]` calls [`ObjectDef`], for a `#[type_register]`
-//! type with `#[reflect]` fields; the derive guarantees that each registered
-//! field's type and offset match.
+//! Only `#[derive(Object)]` creates an [`ObjectDef`], for a `#[type_register]`
+//! type with `#[reflect]` fields or a `#[type_reflection]` function; the
+//! derive guarantees that each registered field's type and offset match. The
+//! `#[type_reflection]` function receives the builder to register methods
+//! with [`ObjectDef::def`] and [`ObjectDef::def_static`].
 
 use std::ffi::c_void;
 use std::marker::PhantomData;
@@ -36,14 +39,16 @@ use std::mem::{align_of, size_of};
 use std::ops::BitOr;
 use std::panic::{catch_unwind, AssertUnwindSafe};
 
+use crate::function_internal::AsPackedCallable;
 use crate::object::{Object, ObjectArc, ObjectCore, ObjectRef, ObjectRefCore};
 use crate::tvm_ffi_sys::{
     TVMFFIAny, TVMFFIByteArray, TVMFFIFieldFlagBitMask, TVMFFIFieldInfo, 
TVMFFIFieldSetter,
-    TVMFFIGetTypeInfo, TVMFFIObject, TVMFFISEqHashKind, TVMFFITypeIndex, 
TVMFFITypeMetadata,
-    TVMFFITypeRegisterAttr, TVMFFITypeRegisterField, 
TVMFFITypeRegisterMetadata,
+    TVMFFIGetTypeInfo, TVMFFIMethodInfo, TVMFFIObject, TVMFFISEqHashKind, 
TVMFFITypeIndex,
+    TVMFFITypeMetadata, TVMFFITypeRegisterAttr, TVMFFITypeRegisterField,
+    TVMFFITypeRegisterMetadata, TVMFFITypeRegisterMethod,
 };
 use crate::type_traits::{type_schema_metadata, AnyCompatible};
-use crate::{Any, Error, Function, Result, INTERNAL_ERROR, TYPE_ERROR};
+use crate::{Any, Error, Function, Result, INTERNAL_ERROR, TYPE_ERROR, 
VALUE_ERROR};
 
 /// A copy of an object's own fields under a fresh object header (the C++ copy
 /// constructor), which `__ffi_shallow_copy__` returns.
@@ -139,15 +144,36 @@ impl BitOr for FieldFlags {
     }
 }
 
-/// Registers the reflection of the object type `N` (C++ `refl::ObjectDef<N>`)
-/// for `#[derive(Object)]`: the metadata on construction, then each field in
-/// call order, then the type attributes.
+/// Registers the reflection of the object type `N`, as C++ 
`refl::ObjectDef<N>`
+/// does: the metadata, each `#[reflect]` field, the methods and the type
+/// attributes.
 ///
-/// When `N` already has metadata in the type table, every call is a no-op.
-#[doc(hidden)]
+/// `#[derive(Object)]` creates one while it registers a `#[type_register]`
+/// type and passes it to the function `#[type_reflection(path)]` names, which
+/// registers methods with [`ObjectDef::def`] and [`ObjectDef::def_static`]:
+///
+/// ```ignore
+/// #[type_reflection(PointObj::reflect_methods)]
+/// struct PointObj { /* ... */ }
+///
+/// impl PointObj {
+///     fn reflect_methods(def: &mut ObjectDef<Self>) {
+///         def.def("norm1", |p: Point| -> Result<i64> { Ok(p.x.abs() + 
p.y.abs()) }, "The L1 norm.")
+///             .def_static("origin", || -> Result<Point> { Ok(origin()) }, 
"");
+///     }
+/// }
+/// ```
+///
+/// That function runs under the registration lock, before `N::type_index()`
+/// returns, so it must not call `type_index()` of `N` or of another Rust type
+/// that is not registered yet. When `N` already has metadata in the type
+/// table, every call is a no-op.
 pub struct ObjectDef<N: ObjectCore> {
     type_index: i32,
     fresh: bool,
+    /// The first failed method registration, which `finish` reports.
+    error: Option<Error>,
+    method_names: Vec<String>,
     _object: PhantomData<N>,
 }
 
@@ -162,6 +188,7 @@ impl<N: ObjectCore> ObjectDef<N> {
     ///
     /// The caller holds `TYPE_REGISTRATION` for the builder's lifetime, and
     /// `type_index` is `N`'s.
+    #[doc(hidden)]
     pub unsafe fn registering(type_index: i32, kind: Option<SEqHashKind>) -> 
Result<Self> {
         let info = TVMFFIGetTypeInfo(type_index);
         crate::ensure!(
@@ -187,20 +214,25 @@ impl<N: ObjectCore> ObjectDef<N> {
         Ok(Self {
             type_index,
             fresh,
+            error: None,
+            method_names: Vec::new(),
             _object: PhantomData,
         })
     }
 
     /// Registers the field of type `F` at byte offset `offset` in `N` under
-    /// `name`, which must be unique in `N`.
+    /// `name`, which must be unique in `N`, with its doc string (an empty one
+    /// records none).
     ///
     /// # Safety
     ///
     /// `offset` is `std::mem::offset_of!` of a field of `N` whose type is `F`:
     /// the registered getter and setter read and write an `F` there.
+    #[doc(hidden)]
     pub unsafe fn field<F: AnyCompatible>(
         self,
         name: &str,
+        doc: &str,
         offset: usize,
         flags: FieldFlags,
     ) -> Result<Self> {
@@ -210,7 +242,7 @@ impl<N: ObjectCore> ObjectDef<N> {
         let metadata = type_schema_metadata(&F::type_schema());
         let info = TVMFFIFieldInfo {
             name: unsafe { TVMFFIByteArray::from_str(name) },
-            doc: unsafe { TVMFFIByteArray::from_str("") },
+            doc: unsafe { TVMFFIByteArray::from_str(doc) },
             metadata: unsafe { TVMFFIByteArray::from_str(&metadata) },
             flags: flags.bits(),
             size: size_of::<F>() as i64,
@@ -229,6 +261,7 @@ impl<N: ObjectCore> ObjectDef<N> {
     /// Registers `__ffi_shallow_copy__`, which reflected mutation (copy on
     /// write through the field setters) needs; C++ `ObjectDef` registers the
     /// copy constructor.
+    #[doc(hidden)]
     pub fn shallow_copy(self) -> Result<Self>
     where
         N: ShallowCopy,
@@ -245,9 +278,81 @@ impl<N: ObjectCore> ObjectDef<N> {
         Ok(self)
     }
 
+    /// Registers `func`, a typed function as for [`Function::from_typed`]
+    /// whose first parameter is the object, as the method `name` of `N`,
+    /// which must be unique in `N`, with its type schema and `doc`, as C++
+    /// `ObjectDef::def` does. A failed registration is reported when the
+    /// type's registration finishes.
+    pub fn def<F, I, O>(&mut self, name: &str, func: F, doc: &str) -> &mut Self
+    where
+        F: AsPackedCallable<I, O> + 'static,
+    {
+        self.method(name, func, doc, false)
+    }
+
+    /// Registers `func` as the static method `name` of `N`, as C++
+    /// `ObjectDef::def_static` does; otherwise as [`ObjectDef::def`].
+    pub fn def_static<F, I, O>(&mut self, name: &str, func: F, doc: &str) -> 
&mut Self
+    where
+        F: AsPackedCallable<I, O> + 'static,
+    {
+        self.method(name, func, doc, true)
+    }
+
+    fn method<F, I, O>(&mut self, name: &str, func: F, doc: &str, is_static: 
bool) -> &mut Self
+    where
+        F: AsPackedCallable<I, O> + 'static,
+    {
+        if !self.fresh || self.error.is_some() {
+            return self;
+        }
+        if self.method_names.iter().any(|n| n == name) {
+            self.error = Some(Error::new(
+                VALUE_ERROR,
+                &format!("duplicate method `{name}` of `{}`", N::TYPE_KEY),
+                "",
+            ));
+            return self;
+        }
+        self.method_names.push(name.to_string());
+        let metadata = type_schema_metadata(&F::type_schema());
+        let func = Function::from_typed(func);
+        let flags = if is_static {
+            TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskIsStaticMethod as 
i64
+        } else {
+            0
+        };
+        let mut method = TVMFFIAny::new();
+        // SAFETY: the strings outlive the call, which copies them, and the
+        // table takes its own reference to the function.
+        let registered = unsafe {
+            <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,
+                method,
+            };
+            crate::check_safe_call!(TVMFFITypeRegisterMethod(self.type_index, 
&info))
+        };
+        if let Err(error) = registered {
+            self.error = Some(Error::new(
+                error.kind(),
+                &format!("registering method `{name}`: {}", error.message()),
+                "",
+            ));
+        }
+        self
+    }
+
     /// Registers `__ffi_type_final__`, which no other registered metadata
-    /// exposes.
+    /// exposes, and reports the first failed method registration.
+    #[doc(hidden)]
     pub fn finish(self) -> Result<()> {
+        if let Some(error) = self.error {
+            return Err(error);
+        }
         if !self.fresh {
             return Ok(());
         }
diff --git a/rust/tvm-ffi/tests/test_object_reflection.rs 
b/rust/tvm-ffi/tests/test_object_reflection.rs
index 0a9ee0c0..b431d5b7 100644
--- a/rust/tvm-ffi/tests/test_object_reflection.rs
+++ b/rust/tvm-ffi/tests/test_object_reflection.rs
@@ -17,26 +17,32 @@
  * under the License.
  */
 use tvm_ffi::derive::{Object, ObjectRef};
-use tvm_ffi::reflection::FieldFlags;
+use tvm_ffi::reflection::{FieldFlags, ObjectDef};
 use tvm_ffi::*;
 use tvm_ffi_sys::{
     TVMFFIFieldFlagBitMask, TVMFFIFieldInfo, TVMFFIFieldSetter, 
TVMFFIGetTypeInfo,
-    TVMFFISEqHashKind, TVMFFITypeInfo,
+    TVMFFIMethodInfo, TVMFFISEqHashKind, TVMFFITypeInfo,
 };
 
-// Rust-defined types whose `#[reflect]` fields reach the type table, so that
-// the C++ structural engines, `repr` and deep copy see them.
+// Rust-defined types whose `#[reflect]` fields and `#[type_reflection]`
+// methods reach the type table, so that the C++ structural engines, `repr`,
+// deep copy and other languages see them.
 
 #[repr(C)]
 #[derive(Object)]
 #[type_key = "testing.rust.ReflectedPoint"]
 #[type_register]
 #[type_s_eq_hash_kind = "TreeNode"]
+#[type_reflection(PointObj::reflect_methods)]
 struct PointObj {
     base: Object,
+    /// The x coordinate.
+    ///
+    /// Signed.
     #[reflect]
     x: i64,
-    #[reflect(writable)]
+    /// Not the registered doc: `doc = ` wins.
+    #[reflect(writable, doc = "The y coordinate.")]
     y: f64,
     #[reflect(seq_hash_ignore, repr_off)]
     label: tvm_ffi::String,
@@ -44,6 +50,28 @@ struct PointObj {
     hits: i64,
 }
 
+impl PointObj {
+    fn reflect_methods(def: &mut ObjectDef<Self>) {
+        def.def(
+            "norm1",
+            |p: Point| -> Result<f64> { Ok(p.data.x.abs() as f64 + 
p.data.y.abs()) },
+            "The L1 norm.",
+        )
+        .def(
+            "shifted",
+            |p: Point, dx: i64| -> Result<Point> {
+                Ok(point(p.data.x + dx, p.data.y, p.data.label.as_str()))
+            },
+            "",
+        )
+        .def_static(
+            "origin",
+            || -> Result<Point> { Ok(point(0, 0.0, "origin")) },
+            "The origin.",
+        );
+    }
+}
+
 #[repr(C)]
 #[derive(ObjectRef, Clone)]
 struct Point {
@@ -59,7 +87,7 @@ struct Point3Obj {
     base: PointObj,
     #[reflect]
     z: i64,
-    #[reflect]
+    #[reflect(name = "labels")]
     tags: Array<tvm_ffi::String>,
     #[reflect]
     r#type: Option<Point>,
@@ -108,6 +136,21 @@ fn fields(info: &TVMFFITypeInfo) -> &[TVMFFIFieldInfo] {
     unsafe { std::slice::from_raw_parts(info.fields, info.num_fields as usize) 
}
 }
 
+fn methods(info: &TVMFFITypeInfo) -> &[TVMFFIMethodInfo] {
+    if info.num_methods == 0 {
+        return &[];
+    }
+    unsafe { std::slice::from_raw_parts(info.methods, info.num_methods as 
usize) }
+}
+
+/// The metadata that records `type_schema`, as C++ `Metadata::ToJSON` writes 
it.
+fn metadata_of(type_schema: &str) -> std::string::String {
+    format!(
+        r#"{{"type_schema":"{}"}}"#,
+        type_schema.replace('"', "\\\"")
+    )
+}
+
 fn call(name: &str, args: &[AnyView]) -> Any {
     Function::get_global(name)
         .unwrap()
@@ -124,6 +167,7 @@ fn register_types() {
         PointObj::type_index();
         Point3Obj::type_index();
         CounterObj::type_index();
+        TallyObj::type_index();
         no_imports::LeafObj::type_index();
     });
 }
@@ -168,7 +212,7 @@ fn test_reflected_fields_are_registered_with_the_type() {
     // The subtype registers only its own fields, and inherits the kind.
     let info = type_info::<Point3Obj>();
     let names: Vec<&str> = fields(info).iter().map(|f| 
f.name.as_str()).collect();
-    assert_eq!(names, ["z", "tags", "type"]);
+    assert_eq!(names, ["z", "labels", "type"]);
     assert_eq!(
         fields(info)[1].metadata.as_str(),
         
r#"{"type_schema":"{\"type\":\"ffi.Array\",\"args\":[{\"type\":\"ffi.String\"}]}"}"#
@@ -179,6 +223,132 @@ fn test_reflected_fields_are_registered_with_the_type() {
     );
 }
 
+#[test]
+fn test_field_docs_come_from_doc_comments_or_reflect() {
+    register_types();
+    let [x, y, label] = fields(type_info::<PointObj>()) else {
+        unreachable!()
+    };
+    assert_eq!(x.doc.as_str(), "The x coordinate.\n\nSigned.");
+    assert_eq!(y.doc.as_str(), "The y coordinate.");
+    assert_eq!(label.doc.as_str(), "");
+    // A renamed field keeps its Rust type and offset.
+    let labels = &fields(type_info::<Point3Obj>())[1];
+    assert_eq!(labels.name.as_str(), "labels");
+    assert_eq!(labels.offset, std::mem::offset_of!(Point3Obj, tags) as i64);
+    let p3 = point3(1, 2);
+    let getter = FieldGetter::new(Point3Obj::type_index(), "labels").unwrap();
+    let tags = getter.get::<_, Array<tvm_ffi::String>>(&*p3.data).unwrap();
+    assert_eq!(tags.len(), 2);
+}
+
+#[test]
+fn test_methods_are_registered_with_the_type() {
+    register_types();
+    let info = type_info::<PointObj>();
+    let names: Vec<&str> = methods(info).iter().map(|m| 
m.name.as_str()).collect();
+    assert_eq!(names, ["norm1", "shifted", "origin"]);
+    let [norm1, shifted, origin] = methods(info) else {
+        unreachable!()
+    };
+    assert_eq!(norm1.flags, 0);
+    assert_eq!(norm1.doc.as_str(), "The L1 norm.");
+    assert_eq!(shifted.doc.as_str(), "");
+    assert_eq!(
+        origin.flags,
+        TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitMaskIsStaticMethod as i64
+    );
+    assert_eq!(origin.doc.as_str(), "The origin.");
+    // Each method records its type schema, as C++ `ObjectDef::def` does.
+    let point = r#"{"type":"testing.rust.ReflectedPoint"}"#;
+    assert_eq!(
+        shifted.metadata.as_str(),
+        metadata_of(&format!(
+            
r#"{{"type":"ffi.Function","named_args":{{"return":[{point}],"params":[{point},{{"type":"int"}}]}}}}"#
+        ))
+    );
+    assert_eq!(
+        origin.metadata.as_str(),
+        metadata_of(&format!(
+            
r#"{{"type":"ffi.Function","named_args":{{"return":[{point}],"params":[]}}}}"#
+        ))
+    );
+    // The subtype's table holds only its own methods.
+    assert_eq!(methods(type_info::<Point3Obj>()).len(), 0);
+}
+
+#[test]
+fn test_methods_are_called_through_the_type_table() {
+    register_types();
+    let norm1 = Function::from_type_key_method("testing.rust.ReflectedPoint", 
"norm1").unwrap();
+    let norm = f64::try_from(norm1.call_tuple((point(3, -4.5, 
"p"),)).unwrap()).unwrap();
+    assert_eq!(norm, 7.5);
+    // An object of a subtype is an object of the type.
+    let norm = f64::try_from(norm1.call_tuple((point3(2, 
9),)).unwrap()).unwrap();
+    assert_eq!(norm, 2.5);
+    let shifted = Function::from_type_method(PointObj::type_index(), 
"shifted").unwrap();
+    let moved = Point::try_from(shifted.call_tuple((point(3, 1.0, "p"), 
4i64)).unwrap()).unwrap();
+    assert_eq!(
+        (moved.data.x, moved.data.y, moved.data.label.as_str()),
+        (7, 1.0, "p")
+    );
+    let origin = Function::from_type_key_method("testing.rust.ReflectedPoint", 
"origin").unwrap();
+    let origin = Point::try_from(origin.call_tuple(()).unwrap()).unwrap();
+    assert_eq!((origin.data.x, origin.data.label.as_str()), (0, "origin"));
+    // A wrong argument is a type error, not a panic.
+    let error = norm1.call_tuple((1i64,)).err().unwrap();
+    assert_eq!(error.kind(), TYPE_ERROR);
+}
+
+// A type with methods but no reflected fields registers its reflection too.
+
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "testing.rust.ReflectedTally"]
+#[type_register]
+#[type_final]
+#[type_reflection(Self::reflect_methods)]
+struct TallyObj {
+    base: Object,
+    count: i64,
+}
+
+#[repr(C)]
+#[derive(ObjectRef, Clone)]
+struct Tally {
+    data: ObjectArc<TallyObj>,
+}
+
+impl TallyObj {
+    fn reflect_methods(def: &mut ObjectDef<Self>) {
+        def.def("count", |t: Tally| -> Result<i64> { Ok(t.data.count) }, "");
+    }
+}
+
+#[test]
+fn test_types_with_only_methods_register_their_reflection() {
+    register_types();
+    let info = type_info::<TallyObj>();
+    assert_eq!(info.num_fields, 0);
+    assert!(!info.metadata.is_null());
+    assert_eq!(methods(info).len(), 1);
+    assert!(
+        bool::try_from(get_type_attr(TallyObj::type_index(), 
"__ffi_type_final__").unwrap())
+            .unwrap()
+    );
+    let count = Function::from_type_method(TallyObj::type_index(), 
"count").unwrap();
+    let tally = Tally {
+        data: ObjectArc::new(TallyObj {
+            base: Object::new(),
+            count: 11,
+        }),
+    };
+    assert_eq!(
+        i64::try_from(count.call_tuple((tally,)).unwrap()).unwrap(),
+        11
+    );
+}
+
 #[test]
 fn test_reflected_fields_read_and_write_through_the_c_abi() {
     register_types();
@@ -367,3 +537,33 @@ fn test_derives_need_nothing_in_scope() {
     let w = FieldGetter::new(no_imports::LeafObj::type_index(), "w").unwrap();
     assert_eq!(w.get::<_, i64>(&*leaf.data).unwrap(), 9);
 }
+
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "testing.rust.DuplicateMethod"]
+#[type_register]
+#[type_reflection(Self::reflect_methods)]
+struct DuplicateMethodObj {
+    base: Object,
+}
+
+impl DuplicateMethodObj {
+    fn reflect_methods(def: &mut ObjectDef<Self>) {
+        def.def(
+            "same",
+            |_: tvm_ffi::object::ObjectRef| -> Result<i64> { Ok(1) },
+            "",
+        )
+        .def(
+            "same",
+            |_: tvm_ffi::object::ObjectRef| -> Result<i64> { Ok(2) },
+            "",
+        );
+    }
+}
+
+#[test]
+#[should_panic(expected = "duplicate method `same` of 
`testing.rust.DuplicateMethod`")]
+fn test_duplicate_method_names_fail_the_registration() {
+    DuplicateMethodObj::type_index();
+}
diff --git a/rust/tvm-ffi/tests/test_rs_class.rs 
b/rust/tvm-ffi/tests/test_rs_class.rs
index 8a3f889b..2bde20cf 100644
--- a/rust/tvm-ffi/tests/test_rs_class.rs
+++ b/rust/tvm-ffi/tests/test_rs_class.rs
@@ -18,7 +18,7 @@
  */
 use tvm_ffi::derive::Object;
 use tvm_ffi::object::ObjectRef;
-use tvm_ffi::reflection::FieldFlags;
+use tvm_ffi::reflection::{FieldFlags, ObjectDef};
 use tvm_ffi::*;
 use tvm_ffi_sys::{TVMFFIFieldInfo, TVMFFIGetTypeInfo, TVMFFISEqHashKind, 
TVMFFITypeInfo};
 
@@ -26,16 +26,33 @@ use tvm_ffi_sys::{TVMFFIFieldInfo, TVMFFIGetTypeInfo, 
TVMFFISEqHashKind, TVMFFIT
 // structural engines, `repr` and deep copy see.
 
 /// A root type, under `ffi.Object`.
-#[rs_class("testing.rust.RsPoint", s_eq_hash_kind = "TreeNode")]
+#[rs_class(
+    "testing.rust.RsPoint",
+    s_eq_hash_kind = "TreeNode",
+    reflection = RsPointObj::reflect_methods
+)]
 pub struct RsPointObj {
     base: Object,
+    /// The x coordinate.
     pub x: i64,
-    #[reflect(writable, seq_hash_ignore)]
+    #[reflect(writable, seq_hash_ignore, doc = "A label.")]
     pub label: tvm_ffi::String,
 }
 
 def_object_ref!(pub RsPoint, ObjectRef, RsPointObj);
 
+impl RsPointObj {
+    fn reflect_methods(def: &mut ObjectDef<Self>) {
+        def.def(
+            "with_label",
+            |p: RsPoint, label: tvm_ffi::String| -> Result<RsPoint> {
+                Ok(point(p.x, label.as_str()))
+            },
+            "The point with another label.",
+        );
+    }
+}
+
 /// A subtype of a Rust-defined type, with a private field.
 #[rs_class("testing.rust.RsPoint3", final)]
 pub struct RsPoint3Obj {
@@ -214,10 +231,12 @@ fn test_fields_are_registered_with_their_flags() {
     };
     assert_eq!(x.flags, FieldFlags::NONE.bits());
     assert_eq!(x.offset, std::mem::offset_of!(RsPointObj, x) as i64);
+    assert_eq!(x.doc.as_str(), "The x coordinate.");
     assert_eq!(
         label.flags,
         (FieldFlags::WRITABLE | FieldFlags::SEQ_HASH_IGNORE).bits()
     );
+    assert_eq!(label.doc.as_str(), "A label.");
     let metadata = unsafe { &*type_info::<RsPointObj>().metadata };
     assert_eq!(
         metadata.total_size as usize,
@@ -243,6 +262,24 @@ fn test_fields_are_registered_with_their_flags() {
     );
 }
 
+#[test]
+fn test_reflection_registers_methods() {
+    register_types();
+    let info = type_info::<RsPointObj>();
+    assert_eq!(info.num_methods, 1);
+    let method = unsafe { &*info.methods };
+    assert_eq!(method.name.as_str(), "with_label");
+    assert_eq!(method.doc.as_str(), "The point with another label.");
+    let with_label = Function::from_type_key_method("testing.rust.RsPoint", 
"with_label").unwrap();
+    let relabeled = with_label
+        .call_tuple((point(4, "a"), tvm_ffi::String::from("b")))
+        .unwrap();
+    let relabeled = RsPoint::try_from(relabeled).unwrap();
+    assert_eq!((relabeled.x, relabeled.label.as_str()), (4, "b"));
+    // The subtype has no methods of its own.
+    assert_eq!(type_info::<RsPoint3Obj>().num_methods, 0);
+}
+
 #[test]
 fn test_constructors_deref_and_upcast() {
     register_types();

Reply via email to