This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new 0aef1bf1 [FEAT][RUST] Define object types from a struct of their
fields with `rs_class` (#869)
0aef1bf1 is described below
commit 0aef1bf18151f0d0df78f4e9331629bc0c900b2b
Author: Linzhang Li <[email protected]>
AuthorDate: Thu Oct 22 20:48:38 2026 -0400
[FEAT][RUST] Define object types from a struct of their fields with
`rs_class` (#869)
Add `#[rs_class]` and `def_object_ref!`, which define a new FFI object
type in
Rust as C++ does: `#[rs_class]` goes on the object struct, whose first
field
`base` is the parent's object, and `def_object_ref!(TypeName,
ParentType,
ObjectName)` defines its reference, as C++
`TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE` does. The type is
registered in
the FFI type table the way a C++ type with `TVM_FFI_DECLARE_OBJECT_INFO`
and
`refl::ObjectDef` is:
- Its type key gets a type index under its parent's.
- Every field is registered with its getter, setter, type schema and
flags.
C++ and Python then handle it like a C++ type:
- C++ code that takes the parent type accepts it.
- C++ structural equality and hashing, `ffi.ReprPrint` and
`ffi.DeepCopy`
walk its fields.
- A Python `c_class` for its key gets the fields from the registration.
```rust
use tvm_ffi::{def_object_ref, rs_class};
#[rs_class("my_ext.PairType", final)]
pub struct PairTypeNode {
base: TypeObj,
pub first: Type,
pub second: Type,
#[reflect(seq_hash_ignore)]
pub note: tvm_ffi::String,
}
def_object_ref!(pub PairType, Type, PairTypeNode);
impl PairType {
pub fn new(first: impl Into<Type>, second: impl Into<Type>) -> Self {
PairTypeNode::new(TypeObj::new(UNKNOWN_LOC), first, second,
"").into()
}
}
let pair = PairType::new(PrimType::int(32), PrimType::float(32));
let ty: Type = pair.into(); // an `ir.Type` that C++ sees as
`my_ext.PairType`
```
On the Rust side, the two macros write what a type defined with
`derive(Object)` / `derive(ObjectRef)` (#859) otherwise needs by hand:
- `#[rs_class]`: `#[type_register]` and `#[reflect]` on each field,
`#[repr(C)]`, `Deref` to the parent's object and `PairTypeNode::new`,
which
takes the parent's object, then the fields
- `def_object_ref!`: the reference struct, `Deref` to the object,
`From<PairTypeNode>` and the conversion into `Type`
`PairTypeNode::new` and the reference's `new` that `tvm-ffi-stubgen`
generates
take a field the same way: a scalar (`bool`, an integer or a float) as
its own
type, and any other field as `impl Into<T>`. A literal infers only from
an
exact type: under `impl Into<u64>`, `1` falls back to `i32`, which does
not
convert into `u64`. The generated `new` thereby also takes a `&str` for
a
`String` field, a `Span` for an `Option<Span>` one, and a `PrimType` for
a
`Type` one, and converts each before it calls the generated
`<Leaf>Obj::new`.
That one takes every field as its own type, as a hand-written parent's
`new`
(bound with `ty-map`) does, so it hands the inherited fields on to its
parent's as they are, and `.into()` at its call site still names a
target. A
subtype defined in Rust builds a generated parent's object with it.
Neither macro names a type the user did not write. `ParentType` is the
reference to the parent's object, `tvm_ffi::object::ObjectRef` for a
type under
`ffi.Object`, and the conversion into it is the existing
`impl_object_upcast!`.
A conversion into the reference to a further ancestor is another
`impl_object_upcast!`, since Rust's `From` does not chain.
---
docs/guides/rust_lang_guide.md | 46 +++
.../rust/src/generated/rust_stubgen/mod.rs | 4 +-
python/tvm_ffi/stub/rust_generator/codegen.py | 82 ++++-
python/tvm_ffi/stub/rust_generator/consts.py | 7 +
rust/tvm-ffi-macros/src/def_object_ref.rs | 134 ++++++++
rust/tvm-ffi-macros/src/lib.rs | 99 ++++++
rust/tvm-ffi-macros/src/object_macros.rs | 67 ++--
rust/tvm-ffi-macros/src/rs_class.rs | 255 ++++++++++++++++
rust/tvm-ffi/src/lib.rs | 2 +-
rust/tvm-ffi/tests/test_rs_class.rs | 338 +++++++++++++++++++++
tests/python/test_stubgen_rust.py | 130 +++++++-
11 files changed, 1116 insertions(+), 48 deletions(-)
diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 16911894..32515785 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -222,6 +222,52 @@ 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`.
+### 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.
+`def_object_ref!(TypeName, ParentType, ObjectName)` defines the reference, as
+C++ `TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE` does:
+
+```rust
+use tvm_ffi::object::ObjectRef;
+use tvm_ffi::{def_object_ref, rs_class, Object};
+
+#[rs_class("my.Point", s_eq_hash_kind = "TreeNode")]
+pub struct PointObj {
+ base: Object,
+ pub x: i64,
+ #[reflect(writable, seq_hash_ignore)]
+ pub label: tvm_ffi::String,
+}
+
+def_object_ref!(pub Point, ObjectRef, PointObj);
+
+#[rs_class("my.Point3", final)]
+pub struct Point3Obj {
+ base: PointObj,
+ pub z: i64,
+}
+
+def_object_ref!(pub Point3, Point, Point3Obj);
+
+let point3 = Point3::from(Point3Obj::new(PointObj::new(Object::new(), 1, "p"),
3));
+assert_eq!((point3.x, point3.z), (1, 3));
+let point: Point = point3.into();
+```
+
+`#[rs_class]` adds `#[repr(C)]`, `Deref` to the parent's object and
+`Point3Obj::new`, which takes the parent's object, then the fields: a scalar
+(`bool`, an integer or a float) as its own type, so that a literal infers as
it,
+and any other field from anything that converts into it, as the `new` of a type
+bound by `tvm-ffi-stubgen` does. The parent's object must implement
+`ShallowCopy`, as `rs_class` types and non-final C++ types bound by
+`tvm-ffi-stubgen` do. The reference dereferences to the object, and the object
+converts into it. The reference also converts into `ParentType`, the reference
+to its parent (`ObjectRef` for a type under `ffi.Object`);
+`impl_object_upcast!` converts it into the references to further ancestors.
+
### Converting Borrowed Values into `Any`
`Any::from(value)` takes ownership of `value`. Use it when you own the value.
diff --git a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
index de5110c2..d32c0829 100644
--- a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
+++ b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
@@ -103,8 +103,8 @@ impl IntPairObj {
impl IntPair {
/// Lossless complete-field allocation.
- pub fn from_fields(a: i64, b: i64, kind: PairKind) -> Self {
- let obj = IntPairObj::new(a, b, kind);
+ pub fn from_fields(a: i64, b: i64, kind: impl Into<PairKind>) -> Self {
+ let obj = IntPairObj::new(a, b, kind.into());
Self { base: ObjectArc::new(obj) }
}
}
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py
b/python/tvm_ffi/stub/rust_generator/codegen.py
index ebf568be..ec72d739 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -22,9 +22,14 @@ the verdict of :mod:`tvm_ffi.stub.layout`:
- *complete*: the struct mirrors every physical field at its real offset and
width, pinned by a ``const`` size/alignment assertion. ``<Leaf>Obj::new``
- (crate-private) and the wrapper's ``new`` take every field root to leaf;
- ``custom-new`` leaves the wrapper's ``new`` to hand-written code and names
- the generated one ``from_fields``.
+ and the wrapper's ``new`` take every field root to leaf: the former each as
+ its own type, the latter a scalar as its own type and anything else as
+ ``impl Into``. ``custom-new`` leaves the wrapper's ``new`` to hand-written
+ code and names the generated one ``from_fields``. A type that is not final
+ can be the parent of a type defined in Rust (``tvm_ffi::rs_class``): its
+ ``<Leaf>Obj::new`` is public, and its object implements ``ShallowCopy`` when
+ every ancestor's is generated too (a ``ty-map``'d one is hand-written and
+ may not). ``<Leaf>Obj::new`` of a final type is crate-private.
- *opaque*: the struct embeds only its parent, and each field is read through
the C ABI getter. Nothing allocates it.
@@ -94,6 +99,16 @@ def _call_lines(open_: str, items: list[str], close: str) ->
list[str]:
]
+def _param_type(rust_type: str) -> str:
+ """Type a parameter of the wrapper's allocator: a scalar as itself,
anything else as ``impl Into``."""
+ return rust_type if rust_type in C_RUST.RUST_EXACT_PARAMS else f"impl
Into<{rust_type}>"
+
+
+def _forward(ident: str, rust_type: str) -> str:
+ """Pass a parameter of the wrapper's allocator to ``<Leaf>Obj::new``,
which takes ``rust_type``."""
+ return ident if rust_type in C_RUST.RUST_EXACT_PARAMS else
f"{ident}.into()"
+
+
def _check_width(target: str, field: NamedTypeSchema, rust_type: str, width:
int) -> None:
"""Reject a directive whose scalar type does not match the reflected field
size."""
if field.size is not None and field.size != width:
@@ -482,10 +497,22 @@ class _ObjectRenderer:
return self.imports.record(override) if "::" in override else override
def _fn_lines(
- self, head: str, params: list[tuple[str, str]], call: tuple[str,
list[str]], init: list[str]
+ self,
+ head: str,
+ params: list[tuple[str, str]],
+ call: tuple[str, list[str]],
+ init: list[str],
+ *,
+ generic: bool = False,
) -> list[str]:
- """Render ``<head>(<params>) -> Self { let <call>; Self { <init> } }``
in an ``impl`` block."""
- plist = [f"{rust_ident(name)}: {rust_type}" for name, rust_type in
params]
+ """Render ``<head>(<params>) -> Self { let <call>; Self { <init> } }``
in an ``impl`` block.
+
+ ``generic`` takes a parameter that is not a scalar as ``impl Into``
(:func:`_param_type`).
+ """
+ plist = [
+ f"{rust_ident(name)}: {_param_type(rust_type) if generic else
rust_type}"
+ for name, rust_type in params
+ ]
binding, args = call
return [
*[f" {line}" for line in _allow_non_snake_case(rust_ident(n)
for n, _ in params)],
@@ -498,7 +525,13 @@ class _ObjectRenderer:
def _allocator_sections(self, base: str, has_parent: bool) ->
list[list[str]]:
"""``<Leaf>Obj::new`` and the wrapper's ``new``.
+ ``<Leaf>Obj::new`` takes every field as its own type, so the parent's,
generated
+ or hand-written, takes what it hands on. The wrapper's ``new`` takes a
scalar
+ field as its own type and any other as ``impl Into``
(:func:`_param_type`), as
+ ``#[rs_class]``'s ``new`` does, and converts it on the way to
``<Leaf>Obj::new``.
``custom-new`` names the wrapper's allocator ``from_fields`` instead.
+ ``<Leaf>Obj::new`` of a type that is not final is public: a subtype
defined in Rust
+ builds its parent's object with it.
"""
inherited: list[tuple[str, str]] = []
if has_parent:
@@ -510,13 +543,13 @@ class _ObjectRenderer:
f"{rust_ident(name)}.into()" if rust_type != parent_type else
rust_ident(name)
for (name, rust_type), (_, parent_type) in zip(params, inherited)
]
- own = [rust_ident(f.name) for f in sorted(self.info.fields, key=lambda
f: f.offset or 0)]
- forward = [rust_ident(name) for name, _ in params]
+ own = [rust_ident(name) for name, _ in params[len(inherited) :]]
+ forward = [_forward(rust_ident(name), rust_type) for name, rust_type
in params]
sections = [
[
f"impl {self.obj_struct} {{",
*self._fn_lines(
- "pub(crate) fn new",
+ "pub(crate) fn new" if self.info.is_final else "pub fn
new",
params,
(f"base = {base}::new", to_parent),
["base", *own],
@@ -534,12 +567,40 @@ class _ObjectRenderer:
params,
(f"obj = {self.obj_struct}::new", forward),
["base: ObjectArc::new(obj)"],
+ generic=True,
),
"}",
]
)
return sections
+ def _shallow_copy_lines(self) -> list[str] | None:
+ """``ShallowCopy``, which a subtype defined in Rust needs of its
parent's object.
+
+ The parent's shallow copy and a clone of every field, as
``derive(Object)``
+ implements it, for a type that is not final and whose ancestors below
+ ``ffi.Object`` are all generated: each of those implements it by this
rule,
+ while a ``ty-map``'d one is hand-written and may not.
+ """
+ if self.info.is_final:
+ return None
+ ancestors = [key for key in self.info.ancestors if key !=
C_RUST.RUST_ROOT_TYPE_KEY]
+ if not all(self._provider(key) == "generated" for key in ancestors):
+ return None
+ shallow_copy = self.imports.record("tvm_ffi::ShallowCopy")
+ own = [rust_ident(f.name) for f in sorted(self.info.fields, key=lambda
f: f.offset or 0)]
+ init = [
+ f"base: {shallow_copy}::shallow_copy(&self.base)",
+ *[f"{name}: Clone::clone(&self.{name})" for name in own],
+ ]
+ return [
+ f"impl {shallow_copy} for {self.obj_struct} {{",
+ " fn shallow_copy(&self) -> Self {",
+ *_call_lines(" Self { ", init, " }"),
+ " }",
+ "}",
+ ]
+
def body(self) -> list[str]:
"""Build the Rust source lines for the object."""
self.imports.reserved.update((self.leaf, self.obj_struct))
@@ -590,6 +651,9 @@ class _ObjectRenderer:
)
elif verdict.is_complete:
sections += self._allocator_sections(base, has_parent)
+ shallow_copy = self._shallow_copy_lines()
+ if shallow_copy:
+ sections.append(shallow_copy)
upcasts = self._upcast_lines()
if upcasts:
sections.append(upcasts)
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py
b/python/tvm_ffi/stub/rust_generator/consts.py
index 0ab2a11d..aea485d5 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -85,6 +85,13 @@ RUST_SCALAR_WIDTHS = {
"f64": 8,
}
+#: Parameters of the wrapper's allocator taken as their own type, as
``#[rs_class]``'s
+#: ``new`` takes them: a literal infers only from its exact type. Any other
parameter is
+#: ``impl Into<T>``; ``<Leaf>Obj::new`` takes every one as its own type.
+RUST_EXACT_PARAMS = frozenset(
+ {"bool", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64",
"usize", "f32", "f64"}
+)
+
#: Size of an object reference field; ``nullable`` may only wrap those.
RUST_POINTER_SIZE = 8
diff --git a/rust/tvm-ffi-macros/src/def_object_ref.rs
b/rust/tvm-ffi-macros/src/def_object_ref.rs
new file mode 100644
index 00000000..9d02966c
--- /dev/null
+++ b/rust/tvm-ffi-macros/src/def_object_ref.rs
@@ -0,0 +1,134 @@
+/*
+ * 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 proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{parse_macro_input, Attribute, DeriveInput, Ident, Path, Token, Type,
Visibility};
+
+use crate::object_macros::expand_object_ref;
+use crate::utils::get_tvm_ffi_crate;
+
+pub(crate) fn def_object_ref(input: TokenStream) -> TokenStream {
+ let args = parse_macro_input!(input as DefObjectRefArgs);
+ expand(&args)
+ .unwrap_or_else(|error| error.to_compile_error())
+ .into()
+}
+
+/// `def_object_ref!(#[attrs] pub TypeName, ParentType, ObjectName, crate =
path)`.
+struct DefObjectRefArgs {
+ attrs: Vec<Attribute>,
+ vis: Visibility,
+ type_name: Ident,
+ parent_type: Type,
+ object_name: Type,
+ tvm_ffi_crate: Option<Path>,
+}
+
+impl Parse for DefObjectRefArgs {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let attrs = input.call(Attribute::parse_outer)?;
+ let vis = input.parse()?;
+ let type_name = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let parent_type = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let object_name = input.parse()?;
+ let mut tvm_ffi_crate = None;
+ if input.parse::<Option<Token![,]>>()?.is_some() &&
input.peek(Token![crate]) {
+ input.parse::<Token![crate]>()?;
+ input.parse::<Token![=]>()?;
+ tvm_ffi_crate = Some(Path::parse_mod_style(input)?);
+ input.parse::<Option<Token![,]>>()?;
+ }
+ if !input.is_empty() {
+ return Err(input.error("expected `crate = <path>` after the object
type"));
+ }
+ Ok(DefObjectRefArgs {
+ attrs,
+ vis,
+ type_name,
+ parent_type,
+ object_name,
+ tvm_ffi_crate,
+ })
+ }
+}
+
+fn expand(args: &DefObjectRefArgs) -> syn::Result<TokenStream2> {
+ let tvm_ffi = match &args.tvm_ffi_crate {
+ Some(path) => quote!(#path),
+ None => get_tvm_ffi_crate(),
+ };
+ let DefObjectRefArgs {
+ attrs,
+ vis,
+ type_name,
+ parent_type,
+ object_name,
+ ..
+ } = args;
+ let ref_input: DeriveInput = syn::parse2(quote! {
+ #vis struct #type_name {
+ base: #tvm_ffi::object::ObjectArc<#object_name>,
+ }
+ })?;
+ let ref_impl = expand_object_ref(&ref_input, &tvm_ffi);
+ let doc = if attrs.iter().any(|a| a.path.is_ident("doc")) {
+ None
+ } else {
+ let doc = format!(
+ "The reference to [`{}`].",
+ quote!(#object_name).to_string().replace(' ', "")
+ );
+ Some(quote!(#[doc = #doc]))
+ };
+
+ Ok(quote! {
+ #doc
+ #(#attrs)*
+ #[repr(C)]
+ #[derive(::core::clone::Clone)]
+ #vis struct #type_name {
+ base: #tvm_ffi::object::ObjectArc<#object_name>,
+ }
+
+ #ref_impl
+
+ impl ::core::ops::Deref for #type_name {
+ type Target = #object_name;
+ #[inline]
+ fn deref(&self) -> &#object_name {
+ &self.base
+ }
+ }
+
+ impl ::core::convert::From<#object_name> for #type_name {
+ #[inline]
+ fn from(object: #object_name) -> Self {
+ Self {
+ base: #tvm_ffi::object::ObjectArc::new(object),
+ }
+ }
+ }
+
+ #tvm_ffi::impl_object_upcast!(#type_name => #parent_type);
+ })
+}
diff --git a/rust/tvm-ffi-macros/src/lib.rs b/rust/tvm-ffi-macros/src/lib.rs
index a38a7729..5af23be1 100644
--- a/rust/tvm-ffi-macros/src/lib.rs
+++ b/rust/tvm-ffi-macros/src/lib.rs
@@ -20,9 +20,11 @@
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
+mod def_object_ref;
mod dispatch;
mod match_any;
mod object_macros;
+mod rs_class;
mod utils;
/// Generate `walk`, `map`, `visit`, or `mutate` dispatch from an inherent
impl.
@@ -110,3 +112,100 @@ pub fn derive_object(input: TokenStream) -> TokenStream {
pub fn derive_object_ref(input: TokenStream) -> TokenStream {
object_macros::derive_object_ref(input)
}
+
+/// Define an object type in Rust from its object struct, as Python's
+/// `py_class` does: the type registers its key with the reflection of every
+/// field, so that C++ structural equality and hashing, `repr`, deep copy and
+/// Python see the fields.
+///
+/// ```ignore
+/// use tvm_ffi::{def_object_ref, rs_class};
+///
+/// /// A pair of values under a `Base`.
+/// #[rs_class("my_ext.Pair", final)]
+/// pub struct PairObj {
+/// base: BaseObj,
+/// pub first: tvm_ffi::String,
+/// pub second: tvm_ffi::String,
+/// #[reflect(seq_hash_ignore)]
+/// pub note: tvm_ffi::String,
+/// }
+///
+/// def_object_ref!(pub Pair, Base, PairObj);
+///
+/// let pair = Pair::from(PairObj::new(BaseObj::new(/* ... */), "a", "b", ""));
+/// let base = Base::from(pair);
+/// ```
+///
+/// The first field, `base`, is the parent's object, as the base class of a C++
+/// object is: `base: Object` for a type under `ffi.Object`. The parent's
object
+/// 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:
+///
+/// - `#[repr(C)]` and `Deref` to the parent's object, so `obj.field` reads the
+/// fields of both.
+/// - `PairObj::new(base, ...)`, which builds the object from the parent's
+/// object and the fields in order: a scalar (`bool`, an integer or a float)
+/// as its own type, so that a literal infers as it, and any other field from
+/// anything that converts into it, as the `new` of a type bound by
+/// `tvm-ffi-stubgen` takes them.
+///
+/// [`def_object_ref!`] defines the reference to it and its conversion into the
+/// reference to its parent.
+///
+/// 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]`.
+/// - `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`.
+///
+/// [`FieldFlags`]: ../tvm_ffi/reflection/struct.FieldFlags.html
+#[proc_macro_error]
+#[proc_macro_attribute]
+pub fn rs_class(args: TokenStream, item: TokenStream) -> TokenStream {
+ rs_class::rs_class(args, item)
+}
+
+/// Define the reference to an object type, as C++
+/// `TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE` does:
+/// `def_object_ref!(TypeName, ParentType, ObjectName)`.
+///
+/// ```ignore
+/// use tvm_ffi::def_object_ref;
+///
+/// def_object_ref!(
+/// /// A pair of values.
+/// pub Pair,
+/// Base,
+/// PairObj
+/// );
+/// ```
+///
+/// `Pair` holds a `PairObj` and is a `derive(ObjectRef)` type. It dereferences
+/// to `PairObj`, and a `PairObj` converts into it.
+///
+/// `ParentType` is the reference to the parent's object: `Base` here, and
+/// `tvm_ffi::object::ObjectRef` for a type under `ffi.Object`. `Pair` converts
+/// into it by `impl_object_upcast!`, so its object must be a registered base
of
+/// `PairObj`, as that macro requires. A conversion into the reference to a
+/// further ancestor takes its own `impl_object_upcast!`.
+///
+/// Attributes and visibility before the name go on the struct. `crate = path`
+/// after the object type is where the generated code finds tvm-ffi, as for
+/// [`rs_class`].
+#[proc_macro_error]
+#[proc_macro]
+pub fn def_object_ref(input: TokenStream) -> TokenStream {
+ def_object_ref::def_object_ref(input)
+}
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs
b/rust/tvm-ffi-macros/src/object_macros.rs
index f4d6324d..9064cf81 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -17,6 +17,7 @@
* under the License.
*/
use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::ext::IdentExt;
use syn::DeriveInput;
@@ -25,14 +26,26 @@ use crate::utils::*;
/// Derive Object trait for a struct to generate boilerplate code
pub fn derive_object(input: proc_macro::TokenStream) -> TokenStream {
- let tvm_ffi_crate = get_tvm_ffi_crate();
let derive_input = syn::parse_macro_input!(input as DeriveInput);
+ TokenStream::from(expand_object(&derive_input, &get_tvm_ffi_crate(),
false))
+}
+
+/// The `ObjectCore` impl of an object struct, and its `ShallowCopy` when it
+/// registers reflection, naming the tvm-ffi crate `tvm_ffi_crate`.
+///
+/// `reflect_always` registers the reflection of a type without `#[reflect]`
+/// fields or `#[type_s_eq_hash_kind]` too, as `#[rs_class]` does.
+pub(crate) fn expand_object(
+ derive_input: &DeriveInput,
+ tvm_ffi_crate: &TokenStream2,
+ reflect_always: bool,
+) -> TokenStream2 {
let struct_name = derive_input.ident.clone();
- let type_key = get_attr(&derive_input, "type_key")
+ let type_key = get_attr(derive_input, "type_key")
.map(attr_to_str)
.expect("Expect #[type_key = \"<my_type_key>\"] attribute");
- let type_final = match get_attr(&derive_input, "type_final") {
+ let type_final = match get_attr(derive_input, "type_final") {
Some(attr) if matches!(attr.parse_meta(), Ok(syn::Meta::Path(_))) =>
true,
Some(_) => panic!("Expect #[type_final] attribute"),
None => false,
@@ -41,7 +54,7 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
// 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") {
+ 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,
@@ -49,7 +62,7 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
// 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()) {
+ 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),
..
@@ -60,7 +73,7 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
None => 0,
};
let child_slots_can_overflow =
- match get_attr(&derive_input, "type_child_slots_can_overflow").map(|a|
a.parse_meta()) {
+ 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),
..
@@ -78,8 +91,8 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
);
assert!(
type_register
- || (get_attr(&derive_input, "type_child_slots").is_none()
- && get_attr(&derive_input,
"type_child_slots_can_overflow").is_none()),
+ || (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
@@ -89,15 +102,15 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
}
.expect("First field must be `<base_name>: <ObjectCoreType>`");
- let reflection = Reflection::parse(&derive_input);
+ let reflection = Reflection::parse(derive_input, reflect_always);
assert!(
- reflection.is_none() || (type_register && get_attr(&derive_input,
"type_index").is_none()),
+ 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]"
);
let (register_reflection, shallow_copy_impl) = match &reflection {
Some(reflection) => (
- reflection.register_tokens(&struct_name, &type_key,
&tvm_ffi_crate),
- reflection.shallow_copy_tokens(&struct_name, &base_ty,
&tvm_ffi_crate),
+ reflection.register_tokens(&struct_name, &type_key, tvm_ffi_crate),
+ reflection.shallow_copy_tokens(&struct_name, &base_ty,
tvm_ffi_crate),
),
None => (quote! {}, quote! {}),
};
@@ -106,7 +119,7 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
// 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) {
+ let type_index_tokens = match get_attr(derive_input,
"type_index").map(attr_to_expr) {
Some(type_index) => {
let type_index_expr =
type_index.expect("Expect
#[type_index(TypeIndex::<my_type_index>)] attribute");
@@ -213,14 +226,16 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
fn assert_impl<T:
#tvm_ffi_crate::object::ObjectCore>() {}
let _ = assert_impl::<#base_ty>;
};
- #base_ty::object_header_mut(&mut this.#base_id)
+ <#base_ty as
#tvm_ffi_crate::object::ObjectCore>::object_header_mut(
+ &mut this.#base_id
+ )
}
})
}),
_ => panic!("First field must be `<base_name>: <ObjectCoreType>`"),
};
- let expanded = quote! {
+ quote! {
#final_parent_check
unsafe impl #tvm_ffi_crate::object::ObjectCore for #struct_name {
@@ -233,8 +248,7 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
}
#shallow_copy_impl
- };
- TokenStream::from(expanded)
+ }
}
/// The reflection a `#[type_register]` type declares: its `#[reflect]` fields
@@ -250,8 +264,9 @@ struct Reflection {
}
impl Reflection {
- /// The type's reflection, or `None` when it declares none.
- fn parse(derive_input: &DeriveInput) -> Option<Self> {
+ /// The type's reflection, or `None` when it declares none and `always`
+ /// is false.
+ fn parse(derive_input: &DeriveInput, always: bool) -> Option<Self> {
let kind = get_attr(derive_input, "type_s_eq_hash_kind").map(|attr| {
let kind = attr_to_str(attr);
syn::Ident::new(&kind.value(), kind.span())
@@ -293,7 +308,7 @@ impl Reflection {
};
reflected.push((id, field.ty.clone(), flags));
}
- if kind.is_none() && reflected.is_empty() {
+ if kind.is_none() && reflected.is_empty() && !always {
return None;
}
Some(Self {
@@ -372,8 +387,16 @@ impl Reflection {
/// Derive ObjectRef trait for a struct to generate boilerplate code
pub fn derive_object_ref(input: proc_macro::TokenStream) -> TokenStream {
- let tvm_ffi_crate = get_tvm_ffi_crate();
let derive_input = syn::parse_macro_input!(input as DeriveInput);
+ TokenStream::from(expand_object_ref(&derive_input, &get_tvm_ffi_crate()))
+}
+
+/// The `ObjectRefCore` and `AnyCompatible` impls of a reference struct,
+/// naming the tvm-ffi crate `tvm_ffi_crate`.
+pub(crate) fn expand_object_ref(
+ derive_input: &DeriveInput,
+ tvm_ffi_crate: &TokenStream2,
+) -> TokenStream2 {
let struct_name = derive_input.ident.clone();
// The `ObjectArc<T>` slot is the first field; its name is the struct's
choice
@@ -530,5 +553,5 @@ pub fn derive_object_ref(input: proc_macro::TokenStream) ->
TokenStream {
#tvm_ffi_crate::impl_into_arg_holder_default!(#struct_name);
});
}
- TokenStream::from(expanded)
+ expanded
}
diff --git a/rust/tvm-ffi-macros/src/rs_class.rs
b/rust/tvm-ffi-macros/src/rs_class.rs
new file mode 100644
index 00000000..b9aa39ba
--- /dev/null
+++ b/rust/tvm-ffi-macros/src/rs_class.rs
@@ -0,0 +1,255 @@
+/*
+ * 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 proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::ext::IdentExt;
+use syn::parse::{Parse, ParseStream};
+use syn::{parse_macro_input, Data, DeriveInput, Fields, LitBool, LitInt,
LitStr, Path, Token};
+
+use crate::object_macros::expand_object;
+use crate::utils::get_tvm_ffi_crate;
+
+pub(crate) fn rs_class(args: TokenStream, item: TokenStream) -> TokenStream {
+ let args = parse_macro_input!(args as RsClassArgs);
+ let item = parse_macro_input!(item as DeriveInput);
+ expand(&args, &item)
+ .unwrap_or_else(|error| error.to_compile_error())
+ .into()
+}
+
+/// `#[rs_class("type.key", final, ...)]`.
+struct RsClassArgs {
+ type_key: LitStr,
+ type_final: bool,
+ s_eq_hash_kind: Option<LitStr>,
+ child_slots: Option<LitInt>,
+ child_slots_can_overflow: Option<LitBool>,
+ tvm_ffi_crate: Option<Path>,
+}
+
+impl Parse for RsClassArgs {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let type_key = input.parse::<LitStr>().map_err(|error| {
+ syn::Error::new(
+ error.span(),
+ "expected the type key first, as in
`#[rs_class(\"my.Point\")]`",
+ )
+ })?;
+ let mut args = RsClassArgs {
+ type_key,
+ type_final: false,
+ s_eq_hash_kind: None,
+ child_slots: None,
+ child_slots_can_overflow: None,
+ tvm_ffi_crate: None,
+ };
+ let mut seen = Vec::new();
+ while !input.is_empty() {
+ input.parse::<Token![,]>()?;
+ if input.is_empty() {
+ break;
+ }
+ // `final` and `crate` are keywords.
+ let name = input.call(syn::Ident::parse_any)?;
+ let key = name.to_string();
+ if seen.contains(&key) {
+ return Err(syn::Error::new(name.span(), format!("duplicate
`{key}`")));
+ }
+ if key != "final" {
+ input.parse::<Token![=]>()?;
+ }
+ match key.as_str() {
+ "final" => args.type_final = true,
+ "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()?),
+ "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>`",
+ ));
+ }
+ }
+ seen.push(key);
+ }
+ if args.type_final
+ && (args.child_slots.is_some() ||
args.child_slots_can_overflow.is_some())
+ {
+ return Err(syn::Error::new(
+ args.type_key.span(),
+ "a final type has no children to reserve slots for",
+ ));
+ }
+ Ok(args)
+ }
+}
+
+const EXPECTED_BASE: &str =
+ "the first field holds the parent's object: `base: Object` at the root,
else `base: ParentObj`";
+
+fn expand(args: &RsClassArgs, item: &DeriveInput) -> syn::Result<TokenStream2>
{
+ if !item.generics.params.is_empty() ||
item.generics.where_clause.is_some() {
+ return Err(syn::Error::new_spanned(
+ &item.generics,
+ "an `rs_class` cannot be generic",
+ ));
+ }
+ let Data::Struct(data) = &item.data else {
+ return Err(syn::Error::new(
+ item.ident.span(),
+ "`rs_class` expects a struct",
+ ));
+ };
+ let fields: Vec<&syn::Field> = match &data.fields {
+ Fields::Named(fields) => fields.named.iter().collect(),
+ Fields::Unit => Vec::new(),
+ Fields::Unnamed(fields) => {
+ return Err(syn::Error::new_spanned(
+ fields,
+ "`rs_class` expects named fields",
+ ));
+ }
+ };
+ let (base, fields) = match fields.split_first() {
+ Some((base, fields)) if base.ident.as_ref().is_some_and(|id|
id.unraw() == "base") => {
+ (*base, fields)
+ }
+ Some((field, _)) => return Err(syn::Error::new_spanned(field,
EXPECTED_BASE)),
+ None => return Err(syn::Error::new(item.ident.span(), EXPECTED_BASE)),
+ };
+ if let Some(attr) = base.attrs.iter().find(|a| a.path.is_ident("reflect"))
{
+ return Err(syn::Error::new_spanned(
+ attr,
+ "`base` is the parent's object, whose fields the parent reflects",
+ ));
+ }
+ let base_id = &base.ident;
+ let base_ty = &base.ty;
+ // `new` takes a scalar as its own type, so that a literal infers as it,
and any
+ // other field as `impl Into`.
+ let params = fields.iter().map(|field| {
+ let (name, ty) = (&field.ident, &field.ty);
+ if is_exact_param(ty) {
+ quote!(#name: #ty)
+ } else {
+ quote!(#name: impl ::core::convert::Into<#ty>)
+ }
+ });
+ let inits = fields.iter().map(|field| {
+ let name = &field.ident;
+ if is_exact_param(&field.ty) {
+ quote!(#name)
+ } else {
+ quote!(#name: ::core::convert::Into::into(#name))
+ }
+ });
+
+ let tvm_ffi = match &args.tvm_ffi_crate {
+ Some(path) => quote!(#path),
+ None => get_tvm_ffi_crate(),
+ };
+ let attrs = &item.attrs;
+ let vis = &item.vis;
+ let obj = &item.ident;
+ let key = &args.type_key;
+
+ // `derive(Object)` reads the type from these attributes; the struct
written out
+ // carries neither them nor `#[reflect]`, which no derive on it would take.
+ let mut derive_attrs = vec![quote!(#[type_key = #key]),
quote!(#[type_register])];
+ if args.type_final {
+ derive_attrs.push(quote!(#[type_final]));
+ }
+ if let Some(kind) = &args.s_eq_hash_kind {
+ derive_attrs.push(quote!(#[type_s_eq_hash_kind = #kind]));
+ }
+ if let Some(slots) = &args.child_slots {
+ derive_attrs.push(quote!(#[type_child_slots = #slots]));
+ }
+ if let Some(overflow) = &args.child_slots_can_overflow {
+ derive_attrs.push(quote!(#[type_child_slots_can_overflow =
#overflow]));
+ }
+ let reflected_fields = fields.iter().map(|field| {
+ if field.attrs.iter().any(|a| a.path.is_ident("reflect")) {
+ quote!(#field)
+ } else {
+ quote!(#[reflect] #field)
+ }
+ });
+ let object_input: DeriveInput = syn::parse2(quote! {
+ #(#derive_attrs)*
+ #vis struct #obj {
+ #base,
+ #(#reflected_fields,)*
+ }
+ })?;
+ let object_impl = expand_object(&object_input, &tvm_ffi, true);
+ let plain_fields = fields.iter().map(|field| {
+ let mut field = (*field).clone();
+ field.attrs.retain(|a| !a.path.is_ident("reflect"));
+ field
+ });
+
+ Ok(quote! {
+ #(#attrs)*
+ #[repr(C)]
+ #vis struct #obj {
+ #base,
+ #(#plain_fields,)*
+ }
+
+ #object_impl
+
+ impl ::core::ops::Deref for #obj {
+ type Target = #base_ty;
+ #[inline]
+ fn deref(&self) -> &#base_ty {
+ &self.#base_id
+ }
+ }
+
+ impl #obj {
+ /// The object from the parent's object, then the fields in order:
a
+ /// scalar as its own type, any other from anything that converts
into it.
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(#base_id: #base_ty, #(#params),*) -> Self {
+ Self { #base_id, #(#inits,)* }
+ }
+ }
+ })
+}
+
+/// The scalars `new` takes as their own type, as the allocator of a type
bound by
+/// tvm-ffi-stubgen does (`RUST_EXACT_PARAMS` there): a literal infers only
from
+/// its exact type.
+const EXACT_PARAMS: &[&str] = &[
+ "bool", "i8", "i16", "i32", "i64", "isize", "u8", "u16", "u32", "u64",
"usize", "f32", "f64",
+];
+
+fn is_exact_param(ty: &syn::Type) -> bool {
+ match ty {
+ syn::Type::Path(path) if path.qself.is_none() => path
+ .path
+ .get_ident()
+ .is_some_and(|ident| EXACT_PARAMS.iter().any(|name| ident ==
name)),
+ _ => false,
+ }
+}
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 7ed07315..c6310957 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -78,7 +78,7 @@ pub use crate::reflection::{
pub use crate::rvalue_ref::RValueRef;
pub use crate::string::{Bytes, String};
pub use crate::type_traits::AnyCompatible;
-pub use tvm_ffi_macros::{dispatch, match_any};
+pub use tvm_ffi_macros::{def_object_ref, dispatch, match_any, rs_class};
pub use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
pub use tvm_ffi_sys::{
diff --git a/rust/tvm-ffi/tests/test_rs_class.rs
b/rust/tvm-ffi/tests/test_rs_class.rs
new file mode 100644
index 00000000..5cb5583c
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_rs_class.rs
@@ -0,0 +1,338 @@
+/*
+ * 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;
+use tvm_ffi::object::ObjectRef;
+use tvm_ffi::reflection::FieldFlags;
+use tvm_ffi::*;
+use tvm_ffi_sys::{TVMFFIFieldInfo, TVMFFIGetTypeInfo, TVMFFISEqHashKind,
TVMFFITypeInfo};
+
+// Object types defined in Rust by `#[rs_class]`, whose fields the C++
+// structural engines, `repr` and deep copy see.
+
+/// A root type, under `ffi.Object`.
+#[rs_class("testing.rust.RsPoint", s_eq_hash_kind = "TreeNode")]
+pub struct RsPointObj {
+ base: Object,
+ pub x: i64,
+ #[reflect(writable, seq_hash_ignore)]
+ pub label: tvm_ffi::String,
+}
+
+def_object_ref!(pub RsPoint, ObjectRef, RsPointObj);
+
+/// A subtype of a Rust-defined type, with a private field.
+#[rs_class("testing.rust.RsPoint3", final)]
+pub struct RsPoint3Obj {
+ base: RsPointObj,
+ pub z: i64,
+ pub tags: Array<tvm_ffi::String>,
+ tag: i64,
+}
+
+def_object_ref!(
+ /// The reference with its own docs.
+ pub RsPoint3,
+ RsPoint,
+ RsPoint3Obj,
+);
+
+/// A subtype without fields, which still registers its reflection.
+#[rs_class("testing.rust.RsMarker", final)]
+pub struct RsMarkerObj {
+ base: RsPointObj,
+}
+
+def_object_ref!(pub RsMarker, RsPoint, RsMarkerObj);
+
+/// Scalar fields, whose literal arguments to `new` infer as the field types.
+#[rs_class("testing.rust.RsScalars", final)]
+pub struct RsScalarsObj {
+ base: Object,
+ pub big: i64,
+ pub count: u64,
+ pub port: u16,
+ pub ratio: f32,
+}
+
+def_object_ref!(pub RsScalars, ObjectRef, RsScalarsObj);
+
+// `testing.TestCxxClassBase` from libtvm_ffi_testing, bound as tvm-ffi-stubgen
+// binds a complete C++ type that can have children: with a public `new` and
+// `ShallowCopy`, so that a Rust type can derive from it.
+
+#[repr(C)]
+#[derive(Object)]
+#[type_key = "testing.TestCxxClassBase"]
+pub struct TestCxxClassBaseObj {
+ base: Object,
+ pub v_i64: i64,
+ pub v_i32: i32,
+}
+
+impl TestCxxClassBaseObj {
+ pub fn new(v_i64: i64, v_i32: i32) -> Self {
+ let base = Object::new();
+ Self { base, v_i64, v_i32 }
+ }
+}
+
+impl ShallowCopy for TestCxxClassBaseObj {
+ fn shallow_copy(&self) -> Self {
+ Self {
+ base: ShallowCopy::shallow_copy(&self.base),
+ v_i64: Clone::clone(&self.v_i64),
+ v_i32: Clone::clone(&self.v_i32),
+ }
+ }
+}
+
+// `def_object_ref!` takes any object type, not only an `rs_class` one.
+def_object_ref!(pub TestCxxClassBase, ObjectRef, TestCxxClassBaseObj);
+
+/// A subtype of a C++ type.
+#[rs_class("testing.rust.RsCxxChild", final, s_eq_hash_kind = "TreeNode")]
+pub struct RsCxxChildObj {
+ base: TestCxxClassBaseObj,
+ pub name: tvm_ffi::String,
+}
+
+def_object_ref!(pub RsCxxChild, TestCxxClassBase, RsCxxChildObj);
+
+/// A crate that reaches tvm-ffi only through a re-export names it with
`crate`.
+mod reexported {
+ // The bare `tvm_ffi` here is not the crate, as in a crate without the
dependency.
+ #[allow(dead_code)]
+ mod tvm_ffi {}
+
+ #[::tvm_ffi::rs_class("testing.rust.RsReexported", crate = ::tvm_ffi)]
+ pub struct ReexportedObj {
+ base: ::tvm_ffi::Object,
+ pub v: i64,
+ }
+
+ ::tvm_ffi::def_object_ref!(
+ pub Reexported,
+ ::tvm_ffi::object::ObjectRef,
+ ReexportedObj,
+ crate = ::tvm_ffi
+ );
+}
+
+fn point(x: i64, label: &str) -> RsPoint {
+ RsPointObj::new(Object::new(), x, label).into()
+}
+
+fn point3(x: i64, z: i64) -> RsPoint3 {
+ let tags = Array::new(vec!["a".into(), "b".into()]);
+ RsPoint3Obj::new(RsPointObj::new(Object::new(), x, "p3"), z, tags,
7).into()
+}
+
+/// The dummy call keeps libtvm_ffi_testing, which registers
`testing.TestCxxClassBase`, linked.
+fn cxx_child(v_i64: i64, v_i32: i32, name: &str) -> RsCxxChild {
+ assert_eq!(unsafe { tvm_ffi_sys::TVMFFITestingDummyTarget() }, 0);
+ RsCxxChildObj::new(TestCxxClassBaseObj::new(v_i64, v_i32), name).into()
+}
+
+fn type_info<N: ObjectCore>() -> &'static TVMFFITypeInfo {
+ unsafe { &*TVMFFIGetTypeInfo(N::type_index()) }
+}
+
+fn field_names<N: ObjectCore>() -> Vec<&'static str> {
+ fields(type_info::<N>())
+ .iter()
+ .map(|f| f.name.as_str())
+ .collect()
+}
+
+fn fields(info: &TVMFFITypeInfo) -> &[TVMFFIFieldInfo] {
+ if info.num_fields == 0 {
+ return &[]; // `fields` may be null
+ }
+ unsafe { std::slice::from_raw_parts(info.fields, info.num_fields as usize)
}
+}
+
+fn call(name: &str, args: &[AnyView]) -> Any {
+ Function::get_global(name)
+ .unwrap()
+ .call_packed(args)
+ .unwrap()
+}
+
+fn structural_equal<T: AnyCompatible + Clone>(a: &T, b: &T) -> bool {
+ let (a, b) = (Any::from(a.clone()), Any::from(b.clone()));
+ let args = [
+ AnyView::from(&a),
+ AnyView::from(&b),
+ AnyView::from(&false),
+ AnyView::from(&false),
+ ];
+ bool::try_from(call("ffi.StructuralEqual", &args)).unwrap()
+}
+
+fn deep_copy<T: AnyCompatible + Clone + TryFrom<Any, Error = Error>>(value:
&T) -> T {
+ let any = Any::from(value.clone());
+ T::try_from(call("ffi.DeepCopy", &[AnyView::from(&any)])).unwrap()
+}
+
+#[test]
+fn test_fields_are_registered_with_their_flags() {
+ assert_eq!(field_names::<RsPointObj>(), ["x", "label"]);
+ let [x, label] = fields(type_info::<RsPointObj>()) else {
+ unreachable!()
+ };
+ assert_eq!(x.flags, FieldFlags::NONE.bits());
+ assert_eq!(x.offset, std::mem::offset_of!(RsPointObj, x) as i64);
+ assert_eq!(
+ label.flags,
+ (FieldFlags::WRITABLE | FieldFlags::SEQ_HASH_IGNORE).bits()
+ );
+ let metadata = unsafe { &*type_info::<RsPointObj>().metadata };
+ assert_eq!(
+ metadata.total_size as usize,
+ std::mem::size_of::<RsPointObj>()
+ );
+ assert_eq!(
+ metadata.structural_eq_hash_kind,
+ TVMFFISEqHashKind::kTVMFFISEqHashKindTreeNode as i32
+ );
+
+ // A subtype registers its own fields, private ones too, and inherits the
kind.
+ assert_eq!(field_names::<RsPoint3Obj>(), ["z", "tags", "tag"]);
+ let kind = |info: &TVMFFITypeInfo| unsafe {
(*info.metadata).structural_eq_hash_kind };
+ assert_eq!(
+ kind(type_info::<RsPoint3Obj>()),
+ TVMFFISEqHashKind::kTVMFFISEqHashKindTreeNode as i32
+ );
+ // One without fields registers its metadata all the same.
+ assert!(field_names::<RsMarkerObj>().is_empty());
+ assert_eq!(
+ kind(type_info::<RsMarkerObj>()),
+ TVMFFISEqHashKind::kTVMFFISEqHashKindTreeNode as i32
+ );
+}
+
+#[test]
+fn test_constructors_deref_and_upcast() {
+ let p3 = point3(1, 3);
+ // Through `RsPoint3Obj` to `RsPointObj`.
+ assert_eq!((p3.x, p3.label.as_str()), (1, "p3"));
+ assert_eq!((p3.z, p3.tags.len(), p3.tag), (3, 2, 7));
+
+ let point3_obj: &RsPoint3Obj = &p3;
+ let point_obj: &RsPointObj = point3_obj;
+ assert_eq!(point_obj.x, 1);
+
+ let upcast: RsPoint = p3.clone().into();
+ assert!(upcast.same_as(&p3));
+ assert_eq!(upcast.x, 1);
+ let back = RsPoint3::try_from(Any::from(upcast)).unwrap();
+ assert!(back.same_as(&p3));
+ assert!(RsPoint3::try_from(Any::from(point(1, ""))).is_err());
+
+ let marker =
RsMarker::from(RsMarkerObj::new(RsPointObj::new(Object::new(), 4, "m")));
+ assert_eq!(marker.x, 4);
+ assert!(RsPoint::from(&marker).same_as(&marker));
+
+ // A root type converts into `ObjectRef`, its parent type.
+ let root = point(2, "r");
+ assert!(ObjectRef::from(&root).same_as(&root));
+}
+
+#[test]
+fn test_new_takes_literals_of_the_field_types() {
+ let scalars = RsScalars::from(RsScalarsObj::new(Object::new(),
5_000_000_000, 1, 2, 0.5));
+ assert_eq!(
+ (scalars.big, scalars.count, scalars.port, scalars.ratio),
+ (5_000_000_000, 1, 2, 0.5)
+ );
+ assert_eq!(
+ field_names::<RsScalarsObj>(),
+ ["big", "count", "port", "ratio"]
+ );
+}
+
+#[test]
+fn test_cpp_structural_equality_and_deep_copy_see_the_fields() {
+ // `label` is ignored.
+ assert!(structural_equal(&point(1, "a"), &point(1, "b")));
+ assert!(!structural_equal(&point(1, "a"), &point(2, "a")));
+ assert!(structural_equal(&point3(1, 2), &point3(1, 2)));
+ assert!(!structural_equal(&point3(1, 2), &point3(0, 2)));
+
+ let original = point3(5, 6);
+ let copy = deep_copy(&original);
+ assert!(!copy.same_as(&original));
+ assert_eq!((copy.x, copy.label.as_str()), (5, "p3"));
+ assert_eq!((copy.z, copy.tags.len(), copy.tag), (6, 2, 7));
+
+ let repr = tvm_ffi::String::try_from(call(
+ "ffi.ReprPrint",
+ &[AnyView::from(&Any::from(original))],
+ ))
+ .unwrap();
+ assert!(repr.as_str().contains("z=6"), "{}", repr.as_str());
+}
+
+#[test]
+fn test_shallow_copy_and_finality_are_type_attributes() {
+ let attr = |type_index, name| get_type_attr(type_index, name).unwrap();
+ let is_final = |type_index| bool::try_from(attr(type_index,
"__ffi_type_final__")).unwrap();
+ assert!(!is_final(RsPointObj::type_index()));
+ assert!(is_final(RsPoint3Obj::type_index()));
+ assert!(is_final(RsMarkerObj::type_index()));
+
+ let original =
RsMarker::from(RsMarkerObj::new(RsPointObj::new(Object::new(), 3, "m")));
+ let shallow_copy =
+ Function::try_from(attr(RsMarkerObj::type_index(),
"__ffi_shallow_copy__")).unwrap();
+ let copy =
RsMarker::try_from(shallow_copy.call_tuple((original.clone(),)).unwrap()).unwrap();
+ assert!(!copy.same_as(&original));
+ assert_eq!((copy.x, copy.label.as_str()), (3, "m"));
+
+ // C++ cannot construct the type from its fields.
+ assert!(Function::from_type_key_method("testing.rust.RsPoint",
"__ffi_init__").is_err());
+}
+
+#[test]
+fn test_subtype_of_a_cpp_type() {
+ let child = cxx_child(7, 8, "c");
+ assert_eq!((child.v_i64, child.v_i32, child.name.as_str()), (7, 8, "c"));
+ // The C++ fields are reflected under the subtype.
+ let v_i64 = FieldGetter::new(RsCxxChildObj::type_index(),
"v_i64").unwrap();
+ assert_eq!(v_i64.get::<_, i64>(&*child.base).unwrap(), 7);
+ assert_eq!(field_names::<RsCxxChildObj>(), ["name"]);
+
+ let base = TestCxxClassBase::from(child.clone());
+ assert_eq!(base.v_i32, 8);
+
+ assert!(structural_equal(&child, &cxx_child(7, 8, "c")));
+ assert!(!structural_equal(&child, &cxx_child(7, 9, "c")));
+ assert!(!structural_equal(&child, &cxx_child(7, 8, "d")));
+
+ let copy = deep_copy(&child);
+ assert!(!copy.same_as(&child));
+ assert_eq!((copy.v_i64, copy.v_i32, copy.name.as_str()), (7, 8, "c"));
+}
+
+#[test]
+fn test_crate_path_names_a_reexported_tvm_ffi() {
+ let value =
reexported::Reexported::from(reexported::ReexportedObj::new(Object::new(), 5));
+ assert_eq!(value.v, 5);
+ assert!(ObjectRef::from(&value).same_as(&value));
+ assert_eq!(field_names::<reexported::ReexportedObj>(), ["v"]);
+}
diff --git a/tests/python/test_stubgen_rust.py
b/tests/python/test_stubgen_rust.py
index 56d6a75f..5d4c9a97 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -730,7 +730,7 @@ impl Deref for Expr {
}
impl ExprObj {
- pub(crate) fn new(span: Option<Span>, ty: Type) -> Self {
+ pub fn new(span: Option<Span>, ty: Type) -> Self {
let base = Object::new();
Self { base, span, ty }
}
@@ -738,10 +738,20 @@ impl ExprObj {
impl Expr {
/// Lossless complete-field allocation.
- pub fn new(span: Option<Span>, ty: Type) -> Self {
- let obj = ExprObj::new(span, ty);
+ pub fn new(span: impl Into<Option<Span>>, ty: impl Into<Type>) -> Self {
+ let obj = ExprObj::new(span.into(), ty.into());
Self { base: ObjectArc::new(obj) }
}
+}
+
+impl ShallowCopy for ExprObj {
+ fn shallow_copy(&self) -> Self {
+ Self {
+ base: ShallowCopy::shallow_copy(&self.base),
+ span: Clone::clone(&self.span),
+ ty: Clone::clone(&self.ty),
+ }
+ }
}"""
ADD_EXPECTED = """\
@@ -790,8 +800,13 @@ impl AddObj {
impl Add {
/// Lossless complete-field allocation.
- pub fn new(span: Option<Span>, ty: PrimType, a: PrimExpr, b: PrimExpr) ->
Self {
- let obj = AddObj::new(span, ty, a, b);
+ pub fn new(
+ span: impl Into<Option<Span>>,
+ ty: impl Into<PrimType>,
+ a: impl Into<PrimExpr>,
+ b: impl Into<PrimExpr>,
+ ) -> Self {
+ let obj = AddObj::new(span.into(), ty.into(), a.into(), b.into());
Self { base: ObjectArc::new(obj) }
}
}
@@ -805,15 +820,33 @@ def test_render_complete_expr_golden() -> None:
RUST.add_directive(imports, "nullable", "ir.Expr.span", 1)
text, imports = _render(_expr(), imports)
assert text == EXPR_EXPECTED
- assert _uses(imports) == {"std::ops::Deref", "tvm_ffi::Object",
"tvm_ffi::ObjectArc"}
+ assert _uses(imports) == {
+ "std::ops::Deref",
+ "tvm_ffi::Object",
+ "tvm_ffi::ObjectArc",
+ "tvm_ffi::ShallowCopy",
+ }
+
+
+def test_render_shallow_copy_that_fits_is_one_line() -> None:
+ """A `ShallowCopy` within 100 columns is on one line, as rustfmt keeps
it."""
+ text, _ = _render(_info("ir.Stmt", (_field("span", "ir.Span", 24, 8),),
total_size=32))
+ assert (
+ "impl ShallowCopy for StmtObj {\n"
+ " fn shallow_copy(&self) -> Self {\n"
+ " Self { base: ShallowCopy::shallow_copy(&self.base), "
+ "span: Clone::clone(&self.span) }\n"
+ " }\n"
+ "}"
+ ) in text
def test_render_complete_add_golden() -> None:
"""`tirx.Add` as tvm-rust-ext hand-writes it, on top of a complete
`ir.Expr`.
`field` directives narrow `a` / `b` and the inherited allocator parameter
- `ty` (upcast with `.into()` on the way to `ExprObj::new`); `upcast` adds
- the `PrimExpr` view.
+ `ty` (upcast with `.into()` on the way to `ExprObj::new`, which takes
`Type`
+ as it is); `upcast` adds the `PrimExpr` view.
"""
_register(_expr())
imports = RUST.new_imports()
@@ -898,7 +931,7 @@ def test_render_complete_enum_field() -> None:
RUST.add_directive(imports, "enum", "demo.Pair.kind -> Kind(i32) { A=0,
B=1 }", 1)
text, imports = _render(info, imports)
assert " pub kind: Kind,\n" in text
- assert " pub fn new(a: i64, kind: Kind) -> Self {" in text
+ assert " pub fn new(a: i64, kind: impl Into<Kind>) -> Self {" in text
assert {"tvm_ffi::Result", "tvm_ffi::Error", "tvm_ffi::VALUE_ERROR"} <=
_uses(imports)
assert "tvm_ffi::FieldGetter" not in _uses(imports)
@@ -960,7 +993,10 @@ def test_complete_big_int_field_mirrors() -> None:
" pub count: i64,\n"
"}"
) in text
- assert " pub fn new(value: BigInt, maybe: Optional<BigInt>, count: i64)
-> Self {" in text
+ assert (
+ " pub fn new(value: impl Into<BigInt>, maybe: impl
Into<Optional<BigInt>>, count: i64)"
+ " -> Self {"
+ ) in text
assert "assert!(::core::mem::size_of::<BigObj>() == 64);" in text
assert {"tvm_ffi::BigInt", "tvm_ffi::Optional"} <= _uses(imports)
@@ -1071,8 +1107,13 @@ def test_custom_new_renames_the_wrapper_allocator() ->
None:
)
assert (
"impl Add {\n /// Lossless complete-field allocation.\n"
- " pub fn from_fields(span: Span, ty: Type, a: Expr, b: Expr) ->
Self {\n"
- " let obj = AddObj::new(span, ty, a, b);\n"
+ " pub fn from_fields(\n"
+ " span: impl Into<Span>,\n"
+ " ty: impl Into<Type>,\n"
+ " a: impl Into<Expr>,\n"
+ " b: impl Into<Expr>,\n"
+ " ) -> Self {\n"
+ " let obj = AddObj::new(span.into(), ty.into(), a.into(),
b.into());\n"
" Self { base: ObjectArc::new(obj) }\n"
" }\n}"
) in text
@@ -1243,7 +1284,7 @@ def test_registry_complete_chain_is_mirrored() -> None:
# The allocator flattens the chain; a signature over 100 columns wraps.
assert (
"impl TestCxxClassDerivedDerivedObj {\n"
- " pub(crate) fn new(\n"
+ " pub fn new(\n"
" v_i64: i64,\n"
" v_i32: i32,\n"
" v_f64: f64,\n"
@@ -1256,11 +1297,31 @@ def test_registry_complete_chain_is_mirrored() -> None:
" }\n"
"}"
) in dd
+ # The wrapper's `new` takes a field that is not a scalar from anything
that converts into it.
+ assert (
+ " v_str: impl Into<String>,\n"
+ " v_bool: bool,\n"
+ " ) -> Self {\n"
+ " let obj =\n"
+ " TestCxxClassDerivedDerivedObj::new(v_i64, v_i32, v_f64,
v_f32, v_str.into(), v_bool);\n"
+ ) in dd
+ # Not final, under generated ancestors: a Rust subtype can copy its
parent's object.
+ assert (
+ "impl ShallowCopy for TestCxxClassDerivedDerivedObj {\n"
+ " fn shallow_copy(&self) -> Self {\n"
+ " Self {\n"
+ " base: ShallowCopy::shallow_copy(&self.base),\n"
+ " v_str: Clone::clone(&self.v_str),\n"
+ " v_bool: Clone::clone(&self.v_bool),\n"
+ " }\n"
+ " }\n"
+ "}"
+ ) in dd
assert dd.endswith(
"tvm_ffi::impl_object_upcast!(TestCxxClassDerivedDerived =>
TestCxxClassBase, "
"TestCxxClassDerivedDerived => TestCxxClassDerived);"
)
- assert "tvm_ffi::String" in _uses(imports)
+ assert {"tvm_ffi::String", "tvm_ffi::ShallowCopy"} <= _uses(imports)
def test_registry_hidden_field_and_vptr_stay_opaque() -> None:
@@ -1515,6 +1576,44 @@ def test_ty_map_parent_redirects_base_deref_and_upcast()
-> None:
assert not any(path.startswith("super::") for path in _uses(imports))
+def test_allocator_hands_a_ty_map_parent_its_own_types() -> None:
+ """`<Leaf>Obj::new` takes each field as its own type, as a hand-written
parent's `new` does.
+
+ A narrowed inherited scalar upcasts with `.into()`, whose target the
parent's
+ `i32` names; only the wrapper's `new` takes `impl Into`.
+ """
+ _register(
+ _info(
+ "demo.Base",
+ (_field("name", "str", 24, 16), _field("kind", "int", 40, 4)),
+ total_size=48,
+ )
+ )
+ info = _info(
+ "demo.Child",
+ (_field("n", "int", 48, 8),),
+ parent="demo.Base",
+ total_size=56,
+ is_final=True,
+ )
+ ty_map = {**RUST.default_ty_map(), "demo.Base": "crate::hand::Base"}
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "field", "demo.Child.kind -> Kind", 1)
+ block = _object_block("demo.Child")
+ generate_rust_object(block, ty_map, imports, Options(), info,
_declared("demo.Child"))
+ text = "\n".join(block.lines[1:-1])
+ assert (
+ "impl ChildObj {\n"
+ " pub(crate) fn new(name: String, kind: Kind, n: i64) -> Self {\n"
+ " let base = BaseObj::new(name, kind.into());\n"
+ " Self { base, n }\n"
+ ) in text
+ assert (
+ " pub fn new(name: impl Into<String>, kind: impl Into<Kind>, n:
i64) -> Self {\n"
+ " let obj = ChildObj::new(name.into(), kind.into(), n);\n"
+ ) in text
+
+
def test_stage_3_checks_dependencies_across_the_run(tmp_path: Path) -> None:
root = _crate(tmp_path)
src = root / "mod.rs"
@@ -1542,6 +1641,9 @@ def
test_stage_3_checks_dependencies_across_the_run(tmp_path: Path) -> None:
text = "\n".join(line for block in info.code_blocks for line in
block.lines)
assert " base: TestCxxClassBaseObj," in text
assert "use crate::hand::TestCxxClassBaseObj;" in text
+ # Not final: public `new`; a hand-written parent may lack `ShallowCopy`,
so the type has none.
+ assert "impl TestCxxClassDerivedObj {\n pub fn new(" in text
+ assert "ShallowCopy" not in text
# ---------------------------------------------------------------------------