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 e9fdc58f [FEAT][Rust] Replace rs_class placeholders with an UnsafeInit
creator (#886)
e9fdc58f is described below
commit e9fdc58fa4bf2e60ed701dacac48a01e41a3dc1f
Author: Linzhang Li <[email protected]>
AuthorDate: Wed Oct 28 22:23:44 2026 -0400
[FEAT][Rust] Replace rs_class placeholders with an UnsafeInit creator (#886)
## Summary
apache/tvm-ffi#884 made a creator of an `rs_class` type start the object
from `tvm_ffi::Placeholder`
values: a Rust reference is never null, so a Rust object cannot start
empty as a C++ one does. That
needs a placeholder for the parent's object and for every field type,
builds a whole placeholder
object for each reference field, and still leaves each downstream
project to register `__ffi_new__`
and call `ffi._RegisterFFIInit` itself.
This PR removes `Placeholder` and the `placeholder` flag, and implements
C++ `UnsafeInit` in Rust
instead, without calling into C++ for it. Every `#[rs_class]` type now
registers a creator, as
`refl::ObjectDef` registers `ObjectCreatorUnsafeInit<T>`. The creator
allocates the object zeroed
and leaves its fields undefined for reflection to set. From it the type
registers `__ffi_new__` and,
unless the type defines its own, the `__ffi_init__` that
`ffi._RegisterFFIInit` generates. A type
needs no per-type code:
```rust
use tvm_ffi::object::ObjectRef;
use tvm_ffi::{def_object_ref, rs_class, Object};
#[rs_class("my.Point")]
pub struct PointObj {
base: Object,
pub x: i64,
pub label: tvm_ffi::String,
}
def_object_ref!(pub Point, ObjectRef, PointObj);
```
```python
@tvm_ffi.register_object("my.Point")
class Point(tvm_ffi.Object):
pass
point = Point(1, "p") # the generated `__ffi_init__`
json = tvm_ffi.serialization.to_json_graph_str(point)
back = tvm_ffi.serialization.from_json_graph_str(json) # the creator
```
---
docs/guides/rust_lang_guide.md | 29 ++--
python/tvm_ffi/stub/rust_generator/codegen.py | 45 +++++-
rust/tvm-ffi-macros/src/lib.rs | 20 +--
rust/tvm-ffi-macros/src/object_macros.rs | 35 +++--
rust/tvm-ffi-macros/src/rs_class.rs | 39 +++--
rust/tvm-ffi/src/big_int/mod.rs | 6 +
rust/tvm-ffi/src/collections/array.rs | 5 +
rust/tvm-ffi/src/collections/list.rs | 5 +
rust/tvm-ffi/src/collections/map.rs | 5 +
rust/tvm-ffi/src/device.rs | 6 +
rust/tvm-ffi/src/lib.rs | 4 +-
rust/tvm-ffi/src/object.rs | 12 ++
rust/tvm-ffi/src/reflection.rs | 6 +-
rust/tvm-ffi/src/reflection/placeholder.rs | 90 -----------
rust/tvm-ffi/src/reflection/registry.rs | 85 ++++++++--
rust/tvm-ffi/src/reflection/unsafe_init.rs | 213 ++++++++++++++++++++++++++
rust/tvm-ffi/src/string.rs | 12 ++
rust/tvm-ffi/src/type_traits.rs | 36 +++++
rust/tvm-ffi/tests/test_rs_class.rs | 189 ++++++++++++++++++-----
tests/python/test_stubgen_rust.py | 53 +++++++
20 files changed, 700 insertions(+), 195 deletions(-)
diff --git a/docs/guides/rust_lang_guide.md b/docs/guides/rust_lang_guide.md
index 3dadfe5e..b6d5431e 100644
--- a/docs/guides/rust_lang_guide.md
+++ b/docs/guides/rust_lang_guide.md
@@ -323,16 +323,25 @@ let point: Point = point3.into();
`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. With `placeholder` after the type key, `#[rs_class]` also
-implements `Placeholder` for the object, from the placeholders of the parent's
-object and the fields: a valid object for a creator to make before reflection
-sets its fields, as the creator of a C++ type makes an empty one. 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.
+bound by `tvm-ffi-stubgen` does. 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.
+
+The type also registers a creator, as C++ `refl::ObjectDef` registers one from
+the `UnsafeInit` constructor, and from it `__ffi_new__` and `__ffi_init__`
+(unless `reflection = path` defines `__ffi_init__`), so that a JSON graph reads
+the type back and Python constructs it from its fields. The creator makes the
+object with every field undefined, as C++ `UnsafeInit` leaves a reference null,
+and reflection then sets the fields. A Rust reference is never null, so an
+undefined field holds all-zero bytes, which are no value of its type
+(`AnyCompatible::is_undefined`): the field setter overwrites them without
+dropping, the field getter reads them as None and the object's deleter skips
+them. An `init_off` field stays all-zero after `__ffi_init__`, so its type must
+take all-zero bytes as a value, as a scalar or `Option<T>` does; a reference or
+a string does not, and fails the type's registration. The parent's object must
+implement `ShallowCopy` and `UnsafeInit`, as `rs_class` types and non-final C++
+types bound by `tvm-ffi-stubgen` do.
### Converting Borrowed Values into `Any`
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py
b/python/tvm_ffi/stub/rust_generator/codegen.py
index ec72d739..0d2629df 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -27,9 +27,9 @@ the verdict of :mod:`tvm_ffi.stub.layout`:
``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.
+ ``<Leaf>Obj::new`` is public, and its object implements ``ShallowCopy`` and
+ ``UnsafeInit`` 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.
@@ -601,6 +601,44 @@ class _ObjectRenderer:
"}",
]
+ def _unsafe_init_lines(self) -> list[str]:
+ """``UnsafeInit``, which a subtype defined in Rust needs of its
parent's object.
+
+ It drops the defined fields of an object made with every field
undefined
+ (all-zero), the parent's first, as the creator of the subtype makes
it: a
+ ``TVMFFIAny`` cell (``Any``, an ``Optional`` that is not a pointer)
drops as
+ it is, since all-zero bytes are None; an enum newtype has nothing to
drop;
+ anything else goes through ``drop_if_defined``, which skips an
undefined field.
+ Emitted wherever ``ShallowCopy`` is.
+ """
+ unsafe_init = self.imports.record("tvm_ffi::UnsafeInit")
+ addr = self.imports.record("std::ptr::addr_of_mut")
+ directives = self.imports.directives
+ drops = [f"{unsafe_init}::drop_defined_fields({addr}!((*this).base));"]
+ for field in sorted(self.info.fields, key=lambda f: f.offset or 0):
+ target = f"{self.type_key}.{field.name}"
+ if target in directives.enums:
+ continue
+ mirror = self._field_mirror(self.type_key, field, self.imports)
+ assert mirror is not None # the verdict already ran the
renderability check
+ any_cell = target not in directives.field_types and (
+ field.origin == "Any"
+ or (field.origin == "Optional" and not
mirror.startswith("Option<"))
+ )
+ drop = self.imports.record(
+ "std::ptr::drop_in_place" if any_cell else
"tvm_ffi::reflection::drop_if_defined"
+ )
+ drops.append(f"{drop}({addr}!((*this).{rust_ident(field.name)}));")
+ return [
+ f"unsafe impl {unsafe_init} for {self.obj_struct} {{",
+ " unsafe fn drop_defined_fields(this: *mut Self) {",
+ " unsafe {",
+ *[f" {line}" for line in drops],
+ " }",
+ " }",
+ "}",
+ ]
+
def body(self) -> list[str]:
"""Build the Rust source lines for the object."""
self.imports.reserved.update((self.leaf, self.obj_struct))
@@ -654,6 +692,7 @@ class _ObjectRenderer:
shallow_copy = self._shallow_copy_lines()
if shallow_copy:
sections.append(shallow_copy)
+ sections.append(self._unsafe_init_lines())
upcasts = self._upcast_lines()
if upcasts:
sections.append(upcasts)
diff --git a/rust/tvm-ffi-macros/src/lib.rs b/rust/tvm-ffi-macros/src/lib.rs
index 998670a6..7e43b8db 100644
--- a/rust/tvm-ffi-macros/src/lib.rs
+++ b/rust/tvm-ffi-macros/src/lib.rs
@@ -145,9 +145,9 @@ pub fn derive_object_ref(input: TokenStream) -> TokenStream
{
///
/// 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
+/// must implement `ShallowCopy` and [`UnsafeInit`], 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, and `name` and `doc` as for
/// `derive(Object)`. The macro adds to the struct:
///
@@ -169,20 +169,22 @@ pub fn derive_object_ref(input: TokenStream) ->
TokenStream {
/// `derive(Object)`'s `#[type_final]`, `#[type_s_eq_hash_kind]`,
/// `#[type_child_slots]`, `#[type_child_slots_can_overflow]` and
/// `#[type_reflection]`.
-/// - `placeholder` also implements [`Placeholder`] for the object, from the
-/// placeholders of the parent's object and the fields, which must implement
-/// it too: the object a creator makes before reflection sets its fields.
/// - `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, the methods,
-/// `__ffi_shallow_copy__` and `__ffi_type_final__`, and without
-/// `__ffi_init__`. A field's type must implement `AnyCompatible` and `Clone`.
+/// `__ffi_shallow_copy__` and `__ffi_type_final__`, and a creator that makes
+/// the object with every field undefined for reflection to set, as C++ makes
one
+/// with `UnsafeInit`, from which it registers `__ffi_new__` and, unless the
+/// methods define it, `__ffi_init__`; the macro implements [`UnsafeInit`] for
+/// the object. A field's type must implement `AnyCompatible` and `Clone`; an
+/// `init_off` field's type must take all-zero bytes as a value, as
+/// `__ffi_init__` leaves the field so.
///
/// [`FieldFlags`]: ../tvm_ffi/reflection/struct.FieldFlags.html
-/// [`Placeholder`]: ../tvm_ffi/reflection/trait.Placeholder.html
+/// [`UnsafeInit`]: ../tvm_ffi/reflection/trait.UnsafeInit.html
#[proc_macro_error]
#[proc_macro_attribute]
pub fn rs_class(args: TokenStream, item: TokenStream) -> TokenStream {
diff --git a/rust/tvm-ffi-macros/src/object_macros.rs
b/rust/tvm-ffi-macros/src/object_macros.rs
index 49f9584d..408b1dcc 100644
--- a/rust/tvm-ffi-macros/src/object_macros.rs
+++ b/rust/tvm-ffi-macros/src/object_macros.rs
@@ -33,12 +33,13 @@ pub fn derive_object(input: proc_macro::TokenStream) ->
TokenStream {
/// 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.
+/// `rs_class` registers the reflection of a type without `#[reflect]` fields
+/// or `#[type_s_eq_hash_kind]` too, with the creator of its `UnsafeInit`, as
+/// `#[rs_class]` does.
pub(crate) fn expand_object(
derive_input: &DeriveInput,
tvm_ffi_crate: &TokenStream2,
- reflect_always: bool,
+ rs_class: bool,
) -> TokenStream2 {
let struct_name = derive_input.ident.clone();
@@ -102,7 +103,7 @@ pub(crate) fn expand_object(
}
.expect("First field must be `<base_name>: <ObjectCoreType>`");
- let reflection = Reflection::parse(derive_input, reflect_always);
+ let reflection = Reflection::parse(derive_input, rs_class);
assert!(
reflection.is_none() || (type_register && get_attr(derive_input,
"type_index").is_none()),
"#[reflect], #[type_s_eq_hash_kind] and #[type_reflection] need
#[type_register] and no \
@@ -264,6 +265,9 @@ struct Reflection {
field_ids: Vec<syn::Ident>,
/// The reflected fields.
reflected: Vec<ReflectedField>,
+ /// Whether the type registers the creator of its `UnsafeInit`, as an
+ /// `#[rs_class]` type does.
+ unsafe_init: bool,
}
/// A `#[reflect]` field: its identifier, type, flag names, and the name and
@@ -349,9 +353,9 @@ fn doc_comment(attrs: &[syn::Attribute]) -> String {
}
impl Reflection {
- /// The type's reflection, or `None` when it declares none and `always`
- /// is false.
- fn parse(derive_input: &DeriveInput, always: bool) -> Option<Self> {
+ /// The type's reflection, or `None` when it declares none and is not an
+ /// `rs_class`.
+ fn parse(derive_input: &DeriveInput, rs_class: 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())
@@ -388,7 +392,7 @@ impl Reflection {
reflected.push(field);
}
}
- if kind.is_none() && methods.is_none() && reflected.is_empty() &&
!always {
+ if kind.is_none() && methods.is_none() && reflected.is_empty() &&
!rs_class {
return None;
}
Some(Self {
@@ -400,6 +404,7 @@ impl Reflection {
.expect("First field must be `<base_name>: <ObjectCoreType>`"),
field_ids,
reflected,
+ unsafe_init: rs_class,
})
}
@@ -453,9 +458,16 @@ impl Reflection {
#path(&mut def);
}
});
+ let creator = if self.unsafe_init {
+ quote!(Some(#reflection::unsafe_init_creator::<#struct_name>))
+ } else {
+ quote!(None)
+ };
quote! {
let registered = (|| -> #tvm_ffi_crate::Result<()> {
- let def =
#reflection::ObjectDef::<#struct_name>::registering(tindex, #kind)?
+ let def = #reflection::ObjectDef::<#struct_name>::registering(
+ tindex, #kind, #creator,
+ )?
#(#fields)*
.shallow_copy()?;
#methods
@@ -558,6 +570,11 @@ pub(crate) fn expand_object_ref(
<ContainerType as
#tvm_ffi_crate::object::ObjectCore>::type_index()
}
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+
#tvm_ffi_crate::object::ObjectArc::is_null(::core::ptr::addr_of!((*field).#data_id))
+ }
+
fn type_str() -> std::string::String {
type ContainerType = <#struct_name as
#tvm_ffi_crate::object::ObjectRefCore>
::ContainerType;
diff --git a/rust/tvm-ffi-macros/src/rs_class.rs
b/rust/tvm-ffi-macros/src/rs_class.rs
index e10de7c7..b19a6ab9 100644
--- a/rust/tvm-ffi-macros/src/rs_class.rs
+++ b/rust/tvm-ffi-macros/src/rs_class.rs
@@ -38,7 +38,6 @@ pub(crate) fn rs_class(args: TokenStream, item: TokenStream)
-> TokenStream {
struct RsClassArgs {
type_key: LitStr,
type_final: bool,
- placeholder: bool,
s_eq_hash_kind: Option<LitStr>,
child_slots: Option<LitInt>,
child_slots_can_overflow: Option<LitBool>,
@@ -57,7 +56,6 @@ impl Parse for RsClassArgs {
let mut args = RsClassArgs {
type_key,
type_final: false,
- placeholder: false,
s_eq_hash_kind: None,
child_slots: None,
child_slots_can_overflow: None,
@@ -76,12 +74,11 @@ impl Parse for RsClassArgs {
if seen.contains(&key) {
return Err(syn::Error::new(name.span(), format!("duplicate
`{key}`")));
}
- if key != "final" && key != "placeholder" {
+ if key != "final" {
input.parse::<Token![=]>()?;
}
match key.as_str() {
"final" => args.type_final = true,
- "placeholder" => args.placeholder = 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()?),
@@ -90,9 +87,9 @@ impl Parse for RsClassArgs {
_ => {
return Err(syn::Error::new(
name.span(),
- "expected `final`, `placeholder`, `s_eq_hash_kind =
\"<kind>\"`, \
- `child_slots = <n>`, `child_slots_can_overflow =
<bool>`, \
- `reflection = <path>` or `crate = <path>`",
+ "expected `final`, `s_eq_hash_kind = \"<kind>\"`,
`child_slots = <n>`, \
+ `child_slots_can_overflow = <bool>`, `reflection =
<path>` or \
+ `crate = <path>`",
));
}
}
@@ -212,21 +209,23 @@ fn expand(args: &RsClassArgs, item: &DeriveInput) ->
syn::Result<TokenStream2> {
}
})?;
let object_impl = expand_object(&object_input, &tvm_ffi, true);
- // The object from the placeholders of the parent's object and the fields,
which
- // `new` takes as it takes any value of their types.
- let placeholder_impl = args.placeholder.then(|| {
- let field_tys = fields.iter().map(|field| &field.ty);
- quote! {
- impl #tvm_ffi::Placeholder for #obj {
- fn placeholder() -> Self {
- Self::new(
- <#base_ty as #tvm_ffi::Placeholder>::placeholder(),
- #(<#field_tys as
#tvm_ffi::Placeholder>::placeholder(),)*
- )
+ // The parent's fields first, then the others in order, as the drop glue
drops
+ // them; only through raw pointers, since some may be undefined.
+ let field_ids = fields.iter().map(|field| &field.ident);
+ let unsafe_init_impl = quote! {
+ unsafe impl #tvm_ffi::UnsafeInit for #obj {
+ unsafe fn drop_defined_fields(this: *mut Self) {
+ unsafe {
+ <#base_ty as #tvm_ffi::UnsafeInit>::drop_defined_fields(
+ ::core::ptr::addr_of_mut!((*this).#base_id),
+ );
+ #(#tvm_ffi::reflection::drop_if_defined(
+ ::core::ptr::addr_of_mut!((*this).#field_ids),
+ );)*
}
}
}
- });
+ };
let plain_fields = fields.iter().map(|field| {
let mut field = (*field).clone();
field.attrs.retain(|a| !a.path.is_ident("reflect"));
@@ -243,7 +242,7 @@ fn expand(args: &RsClassArgs, item: &DeriveInput) ->
syn::Result<TokenStream2> {
#object_impl
- #placeholder_impl
+ #unsafe_init_impl
impl ::core::ops::Deref for #obj {
type Target = #base_ty;
diff --git a/rust/tvm-ffi/src/big_int/mod.rs b/rust/tvm-ffi/src/big_int/mod.rs
index 652dbd41..3fe7acec 100644
--- a/rust/tvm-ffi/src/big_int/mod.rs
+++ b/rust/tvm-ffi/src/big_int/mod.rs
@@ -825,6 +825,12 @@ unsafe impl AnyCompatible for BigInt {
"BigInt".to_string()
}
+ // A big integer is an inline integer or a big integer object, never None.
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ (*std::ptr::addr_of!((*field).data.type_index)) ==
TypeIndex::kTVMFFINone as i32
+ }
+
fn type_schema() -> std::string::String {
crate::type_traits::type_schema(<BigIntObj as
crate::ObjectCore>::TYPE_KEY, &[])
}
diff --git a/rust/tvm-ffi/src/collections/array.rs
b/rust/tvm-ffi/src/collections/array.rs
index 5d6bb115..59f025ef 100644
--- a/rust/tvm-ffi/src/collections/array.rs
+++ b/rust/tvm-ffi/src/collections/array.rs
@@ -239,6 +239,11 @@ unsafe impl<T> AnyCompatible for Array<T>
where
T: ContainerElement + Clone,
{
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ ObjectArc::is_null(std::ptr::addr_of!((*field).data))
+ }
+
fn type_str() -> String {
format!("Array<{}>", T::container_type_str())
}
diff --git a/rust/tvm-ffi/src/collections/list.rs
b/rust/tvm-ffi/src/collections/list.rs
index 2aabfacd..63aad17d 100644
--- a/rust/tvm-ffi/src/collections/list.rs
+++ b/rust/tvm-ffi/src/collections/list.rs
@@ -561,6 +561,11 @@ impl<'a, T: ContainerElement> IntoIterator for &'a List<T>
{
// --- Any Type System Conversions ---
unsafe impl<T: ContainerElement> AnyCompatible for List<T> {
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ ObjectArc::is_null(std::ptr::addr_of!((*field).data))
+ }
+
fn type_str() -> String {
format!("List<{}>", T::container_type_str())
}
diff --git a/rust/tvm-ffi/src/collections/map.rs
b/rust/tvm-ffi/src/collections/map.rs
index c7da95eb..0f320a47 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -459,6 +459,11 @@ where
K: ContainerElement,
V: ContainerElement,
{
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ ObjectArc::is_null(std::ptr::addr_of!((*field).data))
+ }
+
fn type_str() -> String {
format!(
"Map<{}, {}>",
diff --git a/rust/tvm-ffi/src/device.rs b/rust/tvm-ffi/src/device.rs
index c69129ee..0885edb5 100644
--- a/rust/tvm-ffi/src/device.rs
+++ b/rust/tvm-ffi/src/device.rs
@@ -94,6 +94,12 @@ impl Drop for RestoreStream {
/// AnyCompatible for DLDevice
unsafe impl AnyCompatible for DLDevice {
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ // No device type is zero; read as an integer, since zero is no
`DLDeviceType`.
+ *std::ptr::addr_of!((*field).device_type).cast::<u32>() == 0
+ }
+
fn type_str() -> String {
// make it consistent with c++ representation
"Device".to_string()
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index b1d5d285..7d3a810a 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -74,8 +74,8 @@ pub use crate::object::{
};
pub use crate::optional::{Optional, OptionalCompatible};
pub use crate::reflection::{
- get_type_attr, FieldFlags, FieldGetter, ObjectDef, Placeholder,
SEqHashKind, ShallowCopy,
- TypeAttrColumn,
+ get_type_attr, FieldFlags, FieldGetter, ObjectDef, SEqHashKind,
ShallowCopy, TypeAttrColumn,
+ UnsafeInit,
};
pub use crate::rvalue_ref::RValueRef;
pub use crate::static_init::run_static_init_blocks;
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index 93306bdc..df5aae51 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -557,6 +557,18 @@ impl<T: ObjectCore> ObjectArc<T> {
}
}
+ /// Whether `this` holds a null pointer, as all-zero bytes do: no
+ /// `ObjectArc`, but the field of an object that a creator left undefined.
+ ///
+ /// # Safety
+ ///
+ /// `this` points to an `ObjectArc<T>` or to all-zero bytes.
+ #[doc(hidden)]
+ #[inline]
+ pub unsafe fn is_null(this: *const Self) -> bool {
+ (*std::ptr::addr_of!((*this).ptr).cast::<*const T>()).is_null()
+ }
+
/// Move a previously allocated object into the ObjectArc
///
/// # Arguments
diff --git a/rust/tvm-ffi/src/reflection.rs b/rust/tvm-ffi/src/reflection.rs
index 92abc4d1..b95aaf04 100644
--- a/rust/tvm-ffi/src/reflection.rs
+++ b/rust/tvm-ffi/src/reflection.rs
@@ -30,10 +30,12 @@ use crate::tvm_ffi_sys::{
};
use crate::{Any, AnyView, Error, ObjectCore, Result, TYPE_ERROR};
-mod placeholder;
mod registry;
-pub use placeholder::Placeholder;
+mod unsafe_init;
pub use registry::{FieldFlags, ObjectDef, SEqHashKind, ShallowCopy};
+#[doc(hidden)]
+pub use unsafe_init::{drop_if_defined, unsafe_init_creator};
+pub use unsafe_init::{unsafe_init, UnsafeInit};
/// A registry-owned type-attribute column indexed by runtime type.
///
diff --git a/rust/tvm-ffi/src/reflection/placeholder.rs
b/rust/tvm-ffi/src/reflection/placeholder.rs
deleted file mode 100644
index 9f9d7462..00000000
--- a/rust/tvm-ffi/src/reflection/placeholder.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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 crate::collections::list::List;
-use crate::object::Object;
-use crate::optional::{Optional, OptionalCompatible};
-use crate::type_traits::ContainerElement;
-use crate::{Any, Array, Bytes, Map, String};
-
-/// A valid value of an object or field type, which an object made before
-/// reflection sets its fields holds until then.
-///
-/// A C++ object can start empty, its references null, and the reflection then
-/// fills it (the creator of `refl::ObjectDef`); a Rust object cannot, so a
-/// creator of a Rust type (an `__ffi_new__` that a JSON graph or a generated
-/// `__ffi_init__` fills) makes the object from placeholders. `#[rs_class(...,
-/// placeholder)]` implements it for the object, from the placeholders of the
-/// parent's object and of the fields. tvm-ffi implements it for `Object`, the
-/// scalars, `String`, `Bytes`, `Any` and the containers; a field or parent
type
-/// of another crate implements it there.
-pub trait Placeholder {
- /// The value.
- fn placeholder() -> Self;
-}
-
-impl Placeholder for Object {
- fn placeholder() -> Self {
- Object::new()
- }
-}
-
-macro_rules! default_placeholder {
- ($($ty:ty),* $(,)?) => {
- $(impl Placeholder for $ty {
- fn placeholder() -> Self {
- Self::default()
- }
- })*
- };
-}
-
-default_placeholder!(
- bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64,
String, Bytes, Any,
-);
-
-impl<T: ContainerElement + Clone> Placeholder for Array<T> {
- fn placeholder() -> Self {
- Self::default()
- }
-}
-
-impl<T: ContainerElement> Placeholder for List<T> {
- fn placeholder() -> Self {
- Self::default()
- }
-}
-
-impl<K: ContainerElement, V: ContainerElement> Placeholder for Map<K, V> {
- fn placeholder() -> Self {
- Self::default()
- }
-}
-
-impl<T: OptionalCompatible> Placeholder for Optional<T> {
- fn placeholder() -> Self {
- Self::default()
- }
-}
-
-impl<T> Placeholder for Option<T> {
- fn placeholder() -> Self {
- None
- }
-}
diff --git a/rust/tvm-ffi/src/reflection/registry.rs
b/rust/tvm-ffi/src/reflection/registry.rs
index b24807da..8c70ef0d 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -22,10 +22,11 @@
//! [`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, 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.
+//! `__ffi_shallow_copy__` and `__ffi_type_final__` type attributes; for an
+//! `#[rs_class]` type also the creator, and from it the `__ffi_new__` and
+//! `__ffi_init__` type attributes. Through them the C++ structural equality,
+//! hashing and mutation, `repr`, deep copy, JSON graphs and Python's generated
+//! classes reach the fields and methods of a Rust type, and construct it.
//!
//! Only `#[derive(Object)]` creates an [`ObjectDef`], for a `#[type_register]`
//! type with `#[reflect]` fields or a `#[type_reflection]` function; the
@@ -43,9 +44,9 @@ use crate::function_internal::AsPackedCallable;
use crate::object::{Object, ObjectArc, ObjectCore, ObjectRef, ObjectRefCore};
use crate::tvm_ffi_sys::{
TVMFFIAny, TVMFFIByteArray, TVMFFIFieldFlagBitMask, TVMFFIFieldInfo,
TVMFFIFieldSetter,
- TVMFFIGetTypeInfo, TVMFFIMethodInfo, TVMFFIObject, TVMFFISEqHashKind,
TVMFFITypeIndex,
- TVMFFITypeMetadata, TVMFFITypeRegisterAttr, TVMFFITypeRegisterField,
- TVMFFITypeRegisterMetadata, TVMFFITypeRegisterMethod,
+ TVMFFIGetTypeInfo, TVMFFIMethodInfo, TVMFFIObject, TVMFFIObjectCreator,
TVMFFIObjectHandle,
+ 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,
VALUE_ERROR};
@@ -171,6 +172,9 @@ impl BitOr for FieldFlags {
pub struct ObjectDef<N: ObjectCore> {
type_index: i32,
fresh: bool,
+ /// The creator registered in the metadata, from which `finish` registers
+ /// `__ffi_new__` and `__ffi_init__`.
+ creator: Option<TVMFFIObjectCreator>,
/// The first failed method registration, which `finish` reports.
error: Option<Error>,
method_names: Vec<String>,
@@ -182,14 +186,21 @@ impl<N: ObjectCore> ObjectDef<N> {
/// derive registers `N`'s type index, before `N::type_index()` returns.
///
/// `kind` of `None` takes the nearest ancestor's kind, as a C++ subclass
- /// inherits `_type_s_eq_hash_kind`.
+ /// inherits `_type_s_eq_hash_kind`. `creator`, as C++ `ObjectDef` takes
+ /// the default or `UnsafeInit` constructor, makes an object of `N` for
+ /// reflection to fill (`unsafe_init_creator::<N>` for an `#[rs_class]`).
///
/// # Safety
///
/// The caller holds `TYPE_REGISTRATION` for the builder's lifetime, and
- /// `type_index` is `N`'s.
+ /// `type_index` is `N`'s. `creator` returns an owned handle of an object
of
+ /// `N` that the field setters can fill.
#[doc(hidden)]
- pub unsafe fn registering(type_index: i32, kind: Option<SEqHashKind>) ->
Result<Self> {
+ pub unsafe fn registering(
+ type_index: i32,
+ kind: Option<SEqHashKind>,
+ creator: Option<TVMFFIObjectCreator>,
+ ) -> Result<Self> {
let info = TVMFFIGetTypeInfo(type_index);
crate::ensure!(
!info.is_null(),
@@ -205,7 +216,7 @@ impl<N: ObjectCore> ObjectDef<N> {
};
let metadata = TVMFFITypeMetadata {
doc: TVMFFIByteArray::from_str(""),
- creator: None,
+ creator,
total_size: size_of::<N>() as i32,
structural_eq_hash_kind: kind,
};
@@ -214,6 +225,7 @@ impl<N: ObjectCore> ObjectDef<N> {
Ok(Self {
type_index,
fresh,
+ creator,
error: None,
method_names: Vec::new(),
_object: PhantomData,
@@ -239,6 +251,18 @@ impl<N: ObjectCore> ObjectDef<N> {
if !self.fresh {
return Ok(self);
}
+ // `__ffi_init__` leaves an `init_off` field as the creator made it,
all-zero.
+ if self.creator.is_some() && flags.bits() &
FieldFlags::INIT_OFF.bits() != 0 {
+ let zero = std::mem::MaybeUninit::<F>::zeroed();
+ crate::ensure!(
+ !unsafe { F::is_undefined(zero.as_ptr()) },
+ VALUE_ERROR,
+ "`init_off` field `{name}` of `{}` stays all-zero after
`__ffi_init__`, which \
+ is no value of `{}`",
+ N::TYPE_KEY,
+ F::type_str()
+ );
+ }
let metadata = type_schema_metadata(&F::type_schema());
let info = TVMFFIFieldInfo {
name: unsafe { TVMFFIByteArray::from_str(name) },
@@ -346,8 +370,10 @@ impl<N: ObjectCore> ObjectDef<N> {
self
}
- /// Registers `__ffi_type_final__`, which no other registered metadata
- /// exposes, and reports the first failed method registration.
+ /// Registers, as C++ `ObjectDef` does, `__ffi_new__` from the creator and
+ /// `__ffi_init__` from the fields unless the type defines its own, then
+ /// `__ffi_type_final__`, which no other registered metadata exposes; and
+ /// reports the first failed method registration.
#[doc(hidden)]
pub fn finish(self) -> Result<()> {
if let Some(error) = self.error {
@@ -356,6 +382,23 @@ impl<N: ObjectCore> ObjectDef<N> {
if !self.fresh {
return Ok(());
}
+ if let Some(creator) = self.creator {
+ let new = Function::from_typed(move || -> Result<ObjectRef> {
+ let mut handle: TVMFFIObjectHandle = std::ptr::null_mut();
+ crate::check_safe_call!(unsafe { creator(&mut handle) })?;
+ // SAFETY: the creator returns an owned handle, which
`ObjectRef` views.
+ Ok(unsafe {
ObjectRef::from_data(ObjectArc::from_raw(handle.cast::<Object>())) })
+ });
+ register_type_attr(self.type_index, "__ffi_new__",
Any::from(new))?;
+ // C++ `ObjectDef` looks `ffi._RegisterFFIInit` up the same way,
in case
+ // it is not registered yet.
+ let explicit_init = self.method_names.iter().any(|name| name ==
"__ffi_init__");
+ if !explicit_init {
+ if let Ok(register_init) =
Function::get_global("ffi._RegisterFFIInit") {
+ register_init.call_tuple((self.type_index,))?;
+ }
+ }
+ }
register_type_attr(
self.type_index,
"__ffi_type_final__",
@@ -419,7 +462,13 @@ unsafe extern "C" fn field_getter<F: AnyCompatible>(
result: *mut TVMFFIAny,
) -> i32 {
let read = catch_unwind(AssertUnwindSafe(|| {
- let value: Any = (*field.cast::<F>()).to_any();
+ let field = field.cast::<F>();
+ // An undefined field reads as None, as a null `ObjectRef` does in C++.
+ let value = if F::is_undefined(field) {
+ Any::new()
+ } else {
+ (*field).to_any()
+ };
std::ptr::write(result, Any::into_raw_ffi_any(value));
}));
match read {
@@ -441,7 +490,13 @@ unsafe extern "C" fn field_setter<F: AnyCompatible>(
let written = catch_unwind(AssertUnwindSafe(|| {
match F::try_cast_from_any_view(&*value) {
Ok(converted) => {
- *field.cast::<F>() = converted;
+ let field = field.cast::<F>();
+ // An undefined field holds no value to drop.
+ if F::is_undefined(field) {
+ field.write(converted);
+ } else {
+ *field = converted;
+ }
None
}
Err(()) => Some(Error::new(
diff --git a/rust/tvm-ffi/src/reflection/unsafe_init.rs
b/rust/tvm-ffi/src/reflection/unsafe_init.rs
new file mode 100644
index 00000000..4147db0a
--- /dev/null
+++ b/rust/tvm-ffi/src/reflection/unsafe_init.rs
@@ -0,0 +1,213 @@
+/*
+ * 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.
+ */
+
+//! Objects made with every field undefined, for reflection to set the fields,
+//! as C++ makes an object with `UnsafeInit`.
+//!
+//! A C++ object made with `UnsafeInit` holds undefined (null) references,
which
+//! the field setters overwrite and the destructor skips. A Rust reference is
+//! never null, so a Rust object cannot hold one; instead [`unsafe_init`]
+//! allocates the object zeroed, and a field holding all-zero bytes that are no
+//! value of its type ([`AnyCompatible::is_undefined`]) is undefined. The field
+//! setter writes an undefined field without dropping it, the field getter
reads
+//! it as None, and the object's deleter drops only the defined fields, all
+//! through raw pointers, so no undefined field is ever viewed as its type.
+
+use std::alloc::Layout;
+use std::ffi::c_void;
+use std::panic::{catch_unwind, AssertUnwindSafe};
+use std::sync::atomic::AtomicU64;
+
+use crate::object::{Object, ObjectArc, ObjectCore, ObjectRef, ObjectRefCore};
+use crate::tvm_ffi_sys::TVMFFIObjectDeleterFlagBitMask::{
+ kTVMFFIObjectDeleterFlagBitMaskStrong, kTVMFFIObjectDeleterFlagBitMaskWeak,
+};
+use crate::tvm_ffi_sys::{TVMFFIObject, TVMFFIObjectHandle,
COMBINED_REF_COUNT_BOTH_ONE};
+use crate::type_traits::AnyCompatible;
+use crate::{Error, INTERNAL_ERROR};
+
+/// An object type whose object can start with every field undefined, for
+/// reflection to set the fields: C++ `UnsafeInit`.
+///
+/// The creator that `#[rs_class]` registers for its type makes the object with
+/// [`unsafe_init`], which a JSON graph read back, `__ffi_init__` and
+/// `__ffi_new__` then fill. `#[rs_class]` implements it for its object, so the
+/// parent's object must implement it too: [`Object`] does, and so does the
+/// object of a non-final C++ type bound by `tvm-ffi-stubgen`.
+///
+/// `__ffi_init__` leaves an `init_off` field as the creator made it, so such a
+/// field's type must take all-zero bytes as a value, as a scalar, `Option<T>`
+/// or `Any` does; the type's registration fails otherwise.
+///
+/// # Safety
+///
+/// `drop_defined_fields` drops each defined field of the object and no
+/// undefined one (as [`drop_if_defined`] does), the parent's object's
included.
+pub unsafe trait UnsafeInit: ObjectCore {
+ /// Drops the defined fields of the object at `this`, the parent's object's
+ /// included, and leaves the undefined ones.
+ ///
+ /// # Safety
+ ///
+ /// `this` points to an object that [`unsafe_init`] made, which is not used
+ /// afterwards.
+ #[doc(hidden)]
+ unsafe fn drop_defined_fields(this: *mut Self);
+}
+
+// The header holds nothing to drop.
+unsafe impl UnsafeInit for Object {
+ unsafe fn drop_defined_fields(_this: *mut Self) {}
+}
+
+/// Drops the field at `field` unless it is undefined.
+///
+/// # Safety
+///
+/// `field` points to a field of an object, defined or undefined, which is not
+/// used afterwards.
+#[doc(hidden)]
+#[inline]
+pub unsafe fn drop_if_defined<F: AnyCompatible>(field: *mut F) {
+ unsafe {
+ if !F::is_undefined(field) {
+ std::ptr::drop_in_place(field);
+ }
+ }
+}
+
+/// An object of `T` with every field undefined, as C++
+/// `make_object<T>(UnsafeInit{})` makes one, for reflection to set the fields
+/// through their setters.
+///
+/// The [`ObjectRef`] views only the object's header, which is valid all along.
+///
+/// # Safety
+///
+/// Each field that all-zero bytes leave undefined
+/// ([`AnyCompatible::is_undefined`]) is set before the object is viewed as a
+/// `T` (by a cast of the reference) or the field is read as its type. Until
+/// then, only the reflection's field setters and getters reach the fields.
+pub unsafe fn unsafe_init<T: UnsafeInit>() -> ObjectRef {
+ let layout = Layout::new::<T>();
+ unsafe {
+ let ptr = std::alloc::alloc_zeroed(layout);
+ if ptr.is_null() {
+ std::alloc::handle_alloc_error(layout);
+ }
+ std::ptr::write(
+ ptr.cast::<TVMFFIObject>(),
+ TVMFFIObject {
+ combined_ref_count:
AtomicU64::new(COMBINED_REF_COUNT_BOTH_ONE),
+ type_index: T::type_index(),
+ __padding: 0,
+ deleter: Some(unsafe_init_deleter::<T>),
+ },
+ );
+ // SAFETY: `ObjectRef` is the view of any object.
+ ObjectRef::from_data(ObjectArc::from_raw(ptr.cast::<Object>()))
+ }
+}
+
+/// The deleter of an object that [`unsafe_init`] made, which may go before
+/// reflection sets every field, as when a JSON graph lacks one.
+unsafe extern "C" fn unsafe_init_deleter<T: UnsafeInit>(ptr: *mut c_void,
flags: i32) {
+ unsafe {
+ if flags & kTVMFFIObjectDeleterFlagBitMaskStrong as i32 != 0 {
+ T::drop_defined_fields(ptr.cast::<T>());
+ }
+ if flags & kTVMFFIObjectDeleterFlagBitMaskWeak as i32 != 0 {
+ std::alloc::dealloc(ptr.cast::<u8>(), Layout::new::<T>());
+ }
+ }
+}
+
+/// The creator of `T` (`TVMFFITypeMetadata::creator`) that `#[rs_class]`
+/// registers, as C++ `ObjectCreatorUnsafeInit<T>`: the owned handle of an
+/// object that [`unsafe_init`] made.
+///
+/// # Safety
+///
+/// `result` is valid for writes.
+#[doc(hidden)]
+pub unsafe extern "C" fn unsafe_init_creator<T: UnsafeInit>(
+ result: *mut TVMFFIObjectHandle,
+) -> i32 {
+ let made = catch_unwind(AssertUnwindSafe(|| unsafe {
+ let object = unsafe_init::<T>();
+ let handle = ObjectArc::into_raw(ObjectRef::into_data(object));
+ std::ptr::write(result, handle as TVMFFIObjectHandle);
+ }));
+ match made {
+ Ok(()) => 0,
+ Err(_) => {
+ let message = format!("panic while creating an object of `{}`",
T::TYPE_KEY);
+ Error::set_raised(&Error::new(INTERNAL_ERROR, &message, ""));
+ -1
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::mem::MaybeUninit;
+
+ use crate::tvm_ffi_sys::dlpack::{DLDevice, DLDeviceType};
+ use crate::type_traits::AnyCompatible;
+ use crate::{Array, BigInt, String};
+
+ fn zero_is_undefined<F: AnyCompatible>() -> bool {
+ let zero = MaybeUninit::<F>::zeroed();
+ unsafe { F::is_undefined(zero.as_ptr()) }
+ }
+
+ fn is_undefined<F: AnyCompatible>(value: F) -> bool {
+ unsafe { F::is_undefined(&value) }
+ }
+
+ #[test]
+ fn all_zero_bytes_are_undefined_where_they_are_no_value() {
+ assert!(zero_is_undefined::<Array<i64>>());
+ assert!(zero_is_undefined::<String>());
+ assert!(zero_is_undefined::<BigInt>());
+ assert!(zero_is_undefined::<DLDevice>());
+
+ assert!(!zero_is_undefined::<i64>());
+ assert!(!zero_is_undefined::<bool>());
+ // `None`, through the niche of a reference or a device, or through a
tag.
+ assert!(!zero_is_undefined::<Option<Array<i64>>>());
+ assert!(!zero_is_undefined::<Option<DLDevice>>());
+ assert!(!zero_is_undefined::<Option<String>>());
+ }
+
+ #[test]
+ fn values_are_defined() {
+ let cpu = DLDevice {
+ device_type: DLDeviceType::kDLCPU,
+ device_id: 0,
+ };
+ assert!(!is_undefined(Array::<i64>::new(vec![])));
+ assert!(!is_undefined(String::from("")));
+ assert!(!is_undefined(BigInt::from(0_i64)));
+ assert!(!is_undefined(cpu));
+ assert!(!is_undefined(Some(cpu)));
+ assert!(!is_undefined(None::<DLDevice>));
+ assert!(!is_undefined(None::<Array<i64>>));
+ }
+}
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index 94739a4c..d8b29d55 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -442,6 +442,12 @@ unsafe impl AnyCompatible for Bytes {
"ffi.Bytes".to_string()
}
+ // Bytes are small bytes or a bytes object, never None.
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ (*std::ptr::addr_of!((*field).data.type_index)) ==
TypeIndex::kTVMFFINone as i32
+ }
+
unsafe fn copy_to_any_view(this: &Self, data: &mut TVMFFIAny) {
*data = this.data;
}
@@ -490,6 +496,12 @@ unsafe impl AnyCompatible for String {
"ffi.String".to_string()
}
+ // A string is a small string or a string object, never None.
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ (*std::ptr::addr_of!((*field).data.type_index)) ==
TypeIndex::kTVMFFINone as i32
+ }
+
unsafe fn copy_to_any_view(this: &Self, data: &mut TVMFFIAny) {
*data = this.data;
}
diff --git a/rust/tvm-ffi/src/type_traits.rs b/rust/tvm-ffi/src/type_traits.rs
index 5f15a957..0e3abeaf 100644
--- a/rust/tvm-ffi/src/type_traits.rs
+++ b/rust/tvm-ffi/src/type_traits.rs
@@ -43,6 +43,30 @@ pub unsafe trait AnyCompatible: Sized {
unreachable!("MATCH_ANY_EXACT is false")
}
+ /// Whether `field`, which holds a value of this type or all-zero bytes,
+ /// holds all-zero bytes that are no value of this type, as a null object
+ /// reference, a string of type index None or a zero device type: a field
+ /// that a creator left undefined for reflection to set, as C++
`UnsafeInit`
+ /// leaves a reference undefined (null). Unlike a C++ undefined reference,
+ /// it is no value; a nullable reference is `Option<T>`, whose `None` is a
+ /// defined value.
+ ///
+ /// The field setter writes an undefined field without dropping it, the
+ /// field getter reads it as None, and the object's deleter skips it. The
default,
+ /// false, fits a type whose all-zero bytes are a value whose drop does
+ /// nothing, as a scalar's, `Any`'s or `Optional`'s.
+ ///
+ /// # Safety
+ ///
+ /// `field` points to a value of this type or to all-zero bytes. An
+ /// implementation reads only bytes that each value initializes and, when
+ /// `Option<Self>` is niche-optimized, that its `None` initializes too.
+ #[inline]
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ let _ = field;
+ false
+ }
+
/// the value to copy to TVMFFIAny
unsafe fn copy_to_any_view(src: &Self, data: &mut TVMFFIAny);
/// consume the value to move to Any
@@ -390,6 +414,18 @@ unsafe impl<T: AnyCompatible> AnyCompatible for Option<T> {
type_schema("Optional", &[<T as AnyCompatible>::type_schema()])
}
+ unsafe fn is_undefined(field: *const Self) -> bool {
+ // With a tag, all-zero bytes are `None`. A niche-optimized `Option<T>`
+ // keeps `T`'s bytes, where all-zero bytes are `None` if `None` looks
+ // undefined to `T` (a reference's null pointer, a device's zero type),
+ // and undefined if `None` sits elsewhere in the niche.
+ if std::mem::size_of::<Self>() != std::mem::size_of::<T>() {
+ return false;
+ }
+ let none = std::mem::MaybeUninit::new(None::<T>);
+ T::is_undefined(field.cast::<T>()) &&
!T::is_undefined(none.as_ptr().cast::<T>())
+ }
+
unsafe fn copy_to_any_view(src: &Self, data: &mut TVMFFIAny) {
if let Some(ref value) = src {
T::copy_to_any_view(value, data);
diff --git a/rust/tvm-ffi/tests/test_rs_class.rs
b/rust/tvm-ffi/tests/test_rs_class.rs
index d14a6597..cd38a413 100644
--- a/rust/tvm-ffi/tests/test_rs_class.rs
+++ b/rust/tvm-ffi/tests/test_rs_class.rs
@@ -17,8 +17,8 @@
* under the License.
*/
use tvm_ffi::derive::Object;
-use tvm_ffi::object::ObjectRef;
-use tvm_ffi::reflection::{FieldFlags, ObjectDef};
+use tvm_ffi::object::{ObjectArc, ObjectRef, ObjectRefCore};
+use tvm_ffi::reflection::{drop_if_defined, FieldFlags, ObjectDef};
use tvm_ffi::*;
use tvm_ffi_sys::{TVMFFIFieldInfo, TVMFFIGetTypeInfo, TVMFFISEqHashKind,
TVMFFITypeInfo};
@@ -90,8 +90,8 @@ pub struct RsScalarsObj {
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.
+// binds a complete C++ type that can have children: with a public `new`,
+// `ShallowCopy` and `UnsafeInit`, so that a Rust type can derive from it.
#[repr(C)]
#[derive(Object)]
@@ -119,6 +119,16 @@ impl ShallowCopy for TestCxxClassBaseObj {
}
}
+unsafe impl UnsafeInit for TestCxxClassBaseObj {
+ unsafe fn drop_defined_fields(this: *mut Self) {
+ unsafe {
+
UnsafeInit::drop_defined_fields(std::ptr::addr_of_mut!((*this).base));
+ drop_if_defined(std::ptr::addr_of_mut!((*this).v_i64));
+ drop_if_defined(std::ptr::addr_of_mut!((*this).v_i32));
+ }
+ }
+}
+
// `def_object_ref!` takes any object type, not only an `rs_class` one.
def_object_ref!(pub TestCxxClassBase, ObjectRef, TestCxxClassBaseObj);
@@ -131,31 +141,46 @@ pub struct RsCxxChildObj {
def_object_ref!(pub RsCxxChild, TestCxxClassBase, RsCxxChildObj);
-/// A root type with `placeholder`, whose fields have the field types tvm-ffi
-/// gives placeholders.
-#[rs_class("testing.rust.RsPlaceholder", s_eq_hash_kind = "TreeNode",
placeholder)]
-pub struct RsPlaceholderObj {
+/// A type that defines its own `__ffi_init__`, over which C++ generates none.
+#[rs_class("testing.rust.RsOwnInit", final, reflection =
RsOwnInitObj::reflect_methods)]
+pub struct RsOwnInitObj {
base: Object,
+ pub v: i64,
+}
+
+def_object_ref!(pub RsOwnInit, ObjectRef, RsOwnInitObj);
+
+impl RsOwnInitObj {
+ fn reflect_methods(def: &mut ObjectDef<Self>) {
+ def.def_static(
+ "__ffi_init__",
+ |v: i64| -> Result<RsOwnInit> {
Ok(RsOwnInitObj::new(Object::new(), 2 * v).into()) },
+ "Twice `v`.",
+ );
+ }
+}
+
+/// `init_off` fields, which `__ffi_init__` leaves all-zero: a value of these
types.
+#[rs_class("testing.rust.RsInitOff", final)]
+pub struct RsInitOffObj {
+ base: Object,
+ pub v: i64,
+ #[reflect(init_off)]
pub count: i64,
- pub ratio: f64,
- pub flag: bool,
- pub label: tvm_ffi::String,
- pub tags: Array<tvm_ffi::String>,
- pub table: Map<tvm_ffi::String, i64>,
+ #[reflect(init_off)]
pub point: Option<RsPoint>,
}
-def_object_ref!(pub RsPlaceholder, ObjectRef, RsPlaceholderObj);
+def_object_ref!(pub RsInitOff, ObjectRef, RsInitOffObj);
-/// A subtype with `placeholder`, whose placeholder starts from its parent's.
-#[rs_class("testing.rust.RsPlaceholderChild", final, placeholder)]
-pub struct RsPlaceholderChildObj {
- base: RsPlaceholderObj,
- pub z: i32,
+/// An `init_off` reference, which all-zero bytes leave undefined: its
registration fails.
+#[rs_class("testing.rust.RsInitOffReference", final)]
+pub struct RsInitOffReferenceObj {
+ base: Object,
+ #[reflect(init_off)]
+ pub items: Array<i64>,
}
-def_object_ref!(pub RsPlaceholderChild, RsPlaceholder, RsPlaceholderChildObj);
-
/// 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.
@@ -243,8 +268,8 @@ fn register_types() {
RsMarkerObj::type_index();
RsScalarsObj::type_index();
RsCxxChildObj::type_index();
- RsPlaceholderObj::type_index();
- RsPlaceholderChildObj::type_index();
+ RsOwnInitObj::type_index();
+ RsInitOffObj::type_index();
reexported::ReexportedObj::type_index();
});
}
@@ -388,7 +413,8 @@ fn test_shallow_copy_and_finality_are_type_attributes() {
assert!(!copy.same_as(&original));
assert_eq!((copy.x, copy.label.as_str()), (3, "m"));
- // C++ cannot construct the type from its fields.
+ // C++ generates `__ffi_init__` from the fields as a type attribute, not a
method.
+ assert!(Function::try_from(attr(RsPointObj::type_index(),
"__ffi_init__")).is_ok());
assert!(Function::from_type_key_method("testing.rust.RsPoint",
"__ffi_init__").is_err());
}
@@ -423,19 +449,112 @@ fn test_crate_path_names_a_reexported_tvm_ffi() {
assert_eq!(field_names::<reexported::ReexportedObj>(), ["v"]);
}
+fn type_attr_function(type_index: i32, name: &str) -> Function {
+ Function::try_from(get_type_attr(type_index, name).unwrap()).unwrap()
+}
+
+#[test]
+fn test_new_makes_the_object_with_its_fields_undefined() {
+ register_types();
+ let new = type_attr_function(RsPoint3Obj::type_index(), "__ffi_new__");
+ let object = ObjectRef::try_from(new.call_packed(&[]).unwrap()).unwrap();
+ assert_eq!(
+ Any::from(object.clone()).type_index(),
+ RsPoint3Obj::type_index()
+ );
+ // Reflection reads an undefined reference or string as None, as C++ reads
a
+ // null `ObjectRef`, and a scalar of a new object as zero. The object then
+ // goes with its fields undefined.
+ let header: &Object = ObjectRefCore::data(&object);
+ let field = |name| {
+ FieldGetter::new(RsPoint3Obj::type_index(), name)
+ .unwrap()
+ .get_any(header)
+ .unwrap()
+ };
+ let none = TypeIndex::kTVMFFINone as i32;
+ assert_eq!(field("tags").type_index(), none);
+ assert_eq!(field("label").type_index(), none);
+ assert_eq!(i64::try_from(field("z")).unwrap(), 0);
+}
+
#[test]
-fn test_placeholder_makes_the_object_from_its_parent_and_fields() {
+fn test_init_constructs_the_type_from_its_fields() {
register_types();
- let child = RsPlaceholderChild::from(RsPlaceholderChildObj::placeholder());
+ // The fields of the parent come first, as for a C++ type.
+ let init = type_attr_function(RsPoint3Obj::type_index(), "__ffi_init__");
+ let label = || tvm_ffi::String::from("l");
+ let tags = Array::<tvm_ffi::String>::new(vec!["a".into()]);
+ let p3 = init
+ .call_tuple((1_i64, label(), 2_i64, tags.clone(), 3_i64))
+ .unwrap();
+ let p3 = RsPoint3::try_from(p3).unwrap();
+ assert_eq!((p3.x, p3.label.as_str(), p3.z, p3.tag), (1, "l", 2, 3));
+ assert!(p3.tags.same_as(&tags));
+ drop(p3);
+
+ // An object that misses a field goes with the fields set so far, and
+ // drops the reference it holds.
+ let tags_count = || ObjectArc::strong_count(ObjectRefCore::data(&tags));
+ assert_eq!(tags_count(), 1);
+ let error = init
+ .call_tuple((1_i64, label(), 2_i64, tags.clone()))
+ .err()
+ .unwrap();
+ assert!(
+ error.message().contains("missing required argument: 'tag'"),
+ "{}",
+ error.message()
+ );
+ assert_eq!(tags_count(), 1);
+
+ // A type's own `__ffi_init__` is a method, which C++ generates none over.
+ assert!(get_type_attr(RsOwnInitObj::type_index(),
"__ffi_init__").is_none());
+ let own_init =
+ Function::from_type_key_method("testing.rust.RsOwnInit",
"__ffi_init__").unwrap();
+ let own =
RsOwnInit::try_from(own_init.call_tuple((3_i64,)).unwrap()).unwrap();
+ assert_eq!(own.v, 6);
+}
+
+#[test]
+fn test_json_graph_reads_back_the_fields() {
+ register_types();
+ let round_trip = |value: Any| {
+ let json = call(
+ "ffi.ToJSONGraphString",
+ &[AnyView::from(&value), AnyView::from(&Any::new())],
+ );
+ call("ffi.FromJSONGraphString", &[AnyView::from(&json)])
+ };
+ let p3 = point3(1, 2);
+ let back = RsPoint3::try_from(round_trip(Any::from(p3.clone()))).unwrap();
+ assert!(!back.same_as(&p3));
+ assert!(structural_equal(&back, &p3));
assert_eq!(
- (child.count, child.ratio, child.flag, child.z),
- (0, 0.0, false, 0)
+ (back.label.as_str(), back.tags.len(), back.tag),
+ ("p3", 2, 7)
+ );
+
+ // The C++ fields of a subtype of a C++ type, which the C++ setters set.
+ let child = cxx_child(7, 8, "c");
+ let back =
RsCxxChild::try_from(round_trip(Any::from(child.clone()))).unwrap();
+ assert!(!back.same_as(&child));
+ assert_eq!((back.v_i64, back.v_i32, back.name.as_str()), (7, 8, "c"));
+}
+
+#[test]
+fn test_init_off_fields_take_all_zero_bytes_as_a_value() {
+ register_types();
+ let init = type_attr_function(RsInitOffObj::type_index(), "__ffi_init__");
+ let value =
RsInitOff::try_from(init.call_tuple((5_i64,)).unwrap()).unwrap();
+ assert_eq!((value.v, value.count), (5, 0));
+ assert!(value.point.is_none());
+
+ let registered =
std::panic::catch_unwind(RsInitOffReferenceObj::type_index);
+ let message = registered.err().unwrap();
+ let message = message.downcast_ref::<std::string::String>().unwrap();
+ assert!(
+ message.contains("`init_off` field `items` of
`testing.rust.RsInitOffReference`"),
+ "{message}"
);
- assert_eq!(child.label.as_str(), "");
- assert!(child.tags.is_empty() && child.table.is_empty() &&
child.point.is_none());
- // A valid object of its type, which the reflection sees as any other.
- let other = RsPlaceholderChild::from(RsPlaceholderChildObj::placeholder());
- assert!(!other.same_as(&child));
- assert!(structural_equal(&child, &other));
- assert!(structural_equal(&deep_copy(&child), &child));
}
diff --git a/tests/python/test_stubgen_rust.py
b/tests/python/test_stubgen_rust.py
index 0ed51f2a..31b54513 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -779,6 +779,16 @@ impl ShallowCopy for ExprObj {
ty: Clone::clone(&self.ty),
}
}
+}
+
+unsafe impl UnsafeInit for ExprObj {
+ unsafe fn drop_defined_fields(this: *mut Self) {
+ unsafe {
+ UnsafeInit::drop_defined_fields(addr_of_mut!((*this).base));
+ drop_if_defined(addr_of_mut!((*this).span));
+ drop_if_defined(addr_of_mut!((*this).ty));
+ }
+ }
}"""
ADD_EXPECTED = """\
@@ -849,9 +859,12 @@ def test_render_complete_expr_golden() -> None:
assert text == EXPR_EXPECTED
assert _uses(imports) == {
"std::ops::Deref",
+ "std::ptr::addr_of_mut",
"tvm_ffi::Object",
"tvm_ffi::ObjectArc",
"tvm_ffi::ShallowCopy",
+ "tvm_ffi::UnsafeInit",
+ "tvm_ffi::reflection::drop_if_defined",
}
@@ -981,6 +994,46 @@ def test_complete_optional_field_mirrors() -> None:
assert "tvm_ffi::Optional" in _uses(imports)
+def test_unsafe_init_drops_each_field_by_its_all_zero_bytes() -> None:
+ """`UnsafeInit` skips an undefined field; a `TVMFFIAny` cell drops as is,
an enum not at all."""
+ info = _info(
+ "demo.Mixed",
+ (
+ _field("count", TypeSchema("Optional", (TypeSchema("int"),)), 24,
16),
+ _field("value", "Any", 40, 16),
+ _field("items", TypeSchema("Optional", (TypeSchema("Array"),)),
56, 8),
+ _field("name", "str", 64, 16),
+ _field("kind", "int", 80, 8),
+ ),
+ total_size=88,
+ )
+ imports = RUST.new_imports()
+ RUST.add_directive(imports, "enum", "demo.Mixed.kind -> Kind(i64) { A=0
}", 1)
+ text, imports = _render(info, imports)
+ assert (
+ "unsafe impl UnsafeInit for MixedObj {\n"
+ " unsafe fn drop_defined_fields(this: *mut Self) {\n"
+ " unsafe {\n"
+ "
UnsafeInit::drop_defined_fields(addr_of_mut!((*this).base));\n"
+ " drop_in_place(addr_of_mut!((*this).count));\n"
+ " drop_in_place(addr_of_mut!((*this).value));\n"
+ " drop_if_defined(addr_of_mut!((*this).items));\n"
+ " drop_if_defined(addr_of_mut!((*this).name));\n"
+ " }\n"
+ " }\n"
+ "}"
+ ) in text, text
+ assert {"std::ptr::drop_in_place", "tvm_ffi::reflection::drop_if_defined"}
<= _uses(imports)
+
+
+def test_final_type_has_no_unsafe_init() -> None:
+ """No Rust type derives from a final type, which needs no `UnsafeInit`."""
+ info = _info("demo.Leaf", (_field("a", "int", 24, 8),), total_size=32,
is_final=True)
+ text, imports = _render(info)
+ assert "UnsafeInit" not in text
+ assert "tvm_ffi::UnsafeInit" not in _uses(imports)
+
+
def test_complete_list_field_mirrors() -> None:
"""A `List<T>` field is an object handle and mirrors like `Array<T>`."""
info = _info(