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();