This is an automated email from the ASF dual-hosted git repository.

tlopex 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 40c6c744 [Rust][Fix] Preserve object lifetime and thread-safety 
contracts (#766)
40c6c744 is described below

commit 40c6c744c2aabf0bc46f8fee529f5da6ec68d478
Author: Shushi Hong <[email protected]>
AuthorDate: Tue Sep 8 17:02:52 2026 -0400

    [Rust][Fix] Preserve object lifetime and thread-safety contracts (#766)
    
    This PR addresses object construction and lifetime issues in Rust
    bindings without changing the C ABI.
    - Add a no-alloc stubgen directive to preserve readable fields while
    suppressing generated allocators for registry-owned types and their
    descendants.
    - Reuse the existing nullable and custom-new directives for fields that
    native code may move out, without introducing a separate
    nullable-storage mechanism.
    - Make objects non-Send/non-Sync by default, including erased and
    borrowed views. Keep Function shareable by requiring thread-safe
    callback captures.
    - Require unique ownership for mutable ObjectArc access, align
    reference-count ordering with C++, and preserve allocation metadata when
    weak references outlive objects with trailing storage.
    
    These changes tighten Rust source-level contracts. Downstream code
    relying on cross-thread object sharing or non-thread-safe callback
    captures may require adaptation.
---
 examples/rust_stubgen/README.md                   |  34 +++-
 python/tvm_ffi/stub/cli.py                        |  12 +-
 python/tvm_ffi/stub/generator.py                  |   9 +-
 python/tvm_ffi/stub/python_generator/generator.py |   1 +
 python/tvm_ffi/stub/rust_generator/codegen.py     |  14 +-
 python/tvm_ffi/stub/rust_generator/consts.py      |  18 +-
 python/tvm_ffi/stub/rust_generator/directives.py  |  10 +-
 python/tvm_ffi/stub/rust_generator/generator.py   |   8 +-
 rust/tvm-ffi/src/function.rs                      |  42 ++++-
 rust/tvm-ffi/src/object.rs                        | 138 +++++++-------
 rust/tvm-ffi/tests/test_function.rs               |  26 +++
 rust/tvm-ffi/tests/test_object.rs                 |  76 ++++++++
 tests/python/test_stubgen_rust.py                 | 214 ++++++++++++++++++++++
 13 files changed, 513 insertions(+), 89 deletions(-)

diff --git a/examples/rust_stubgen/README.md b/examples/rust_stubgen/README.md
index 35906b31..12670a17 100644
--- a/examples/rust_stubgen/README.md
+++ b/examples/rust_stubgen/README.md
@@ -111,12 +111,42 @@ Besides `prefix` and `custom-new`, this example declares 
the integer field
 // tvm-ffi-stubgen(enum): rust_stubgen.IntPair.kind -> PairKind(i32) { 
Unordered=0, Ordered=1 }
 ```
 
-Three more are available: `field` names the Rust type of a field
+`field` names the Rust type of a field
 (`// tvm-ffi-stubgen(field): rust_stubgen.IntPair.a -> MyInt`), `nullable`
-wraps it in `Option` (`// tvm-ffi-stubgen(nullable): rust_stubgen.IntPair.a`),
+wraps an object-reference field in `Option` (for example, a nullable `span`),
 and `upcast` adds a conversion to a hand-written typed view
 (`// tvm-ffi-stubgen(upcast): rust_stubgen.IntPair -> MyView`).
 
+## Construction and ownership
+
+A complete layout does not always permit direct allocation. For a 
registry-owned
+type, keep its readable fields but suppress both generated allocators:
+
+```rust
+// tvm-ffi-stubgen(no-alloc): <type_key>
+```
+
+`no-alloc` takes precedence over `custom-new` and also applies to descendants.
+`no-alloc`, `nullable`, and `opaque` are shared across all input files in the 
same
+invocation, so include the files declaring these policies when generating
+descendants. Directives containing Rust type names remain file-local. The 
binding
+supplies the registry lookup; stubgen does not infer native constructor 
semantics.
+
+Layout metadata and pointer sizes describe the loaded libraries and the current
+process. Run stubgen against the ABI you intend to build for, not a different
+cross-compilation target.
+
+## Thread safety
+
+Complete and opaque objects inherit `!Send` and `!Sync` from `tvm_ffi::Object`,
+including when viewed through a base or `ObjectRef`. The zero-sized marker does
+not change their ABI layout. `Function` remains shareable, so its `from_packed`
+and `from_typed` callbacks require `Send + Sync` captures. Scoped structural
+callbacks are unaffected. `ObjectArc` requires unique ownership for mutable 
access.
+
+These are source-compatibility changes. Any unsafe thread-safety opt-in must
+cover hidden native state, destruction, and all accepted dynamic subtypes.
+
 ## Partial generation
 
 Every type an object refers to (its parent, its ancestors, the types of its
diff --git a/python/tvm_ffi/stub/cli.py b/python/tvm_ffi/stub/cli.py
index af8f8c24..83776827 100644
--- a/python/tvm_ffi/stub/cli.py
+++ b/python/tvm_ffi/stub/cli.py
@@ -39,7 +39,7 @@ from .lib_state import (
 from .utils import FuncInfo, InitConfig, Options
 
 if TYPE_CHECKING:
-    from collections.abc import Container
+    from collections.abc import Container, Sequence
 
     from .generator import Generator
 
@@ -110,6 +110,12 @@ def __main__() -> int:
         for code in file.code_blocks
         if code.kind == "object" and isinstance(code.param, str)
     )
+    shared_directives = [
+        code
+        for file in files
+        for code in file.code_blocks
+        if code.kind == "directive" and code.param[0] in 
generator.shared_directive_kinds
+    ]
     for file in files:
         if opt.verbose:
             print(f"{C.TERM_CYAN}[File] {file.path}{C.TERM_RESET}")
@@ -121,6 +127,7 @@ def __main__() -> int:
                 global_funcs,
                 generator=generator,
                 declared=declared,
+                shared_directives=shared_directives,
             )
         except Exception:
             failed += 1
@@ -322,13 +329,14 @@ def _stage_3(  # noqa: PLR0912
     global_funcs: dict[str, list[FuncInfo]],
     generator: Generator,
     declared: Container[str] = frozenset(),
+    shared_directives: Sequence[CodeBlock] = (),
 ) -> bool:
     """Process one file's blocks; return whether its content is (or would be) 
changed."""
     defined_funcs: set[str] = set()
     defined_types: set[str] = set()
     imports = generator.new_imports()
     # Stage 1. Hand the one-line directives the pipeline does not consume 
itself to the generator.
-    for code in file.code_blocks:
+    for code in [*shared_directives, *file.code_blocks]:
         if code.kind != "directive":
             continue
         name, payload = code.param
diff --git a/python/tvm_ffi/stub/generator.py b/python/tvm_ffi/stub/generator.py
index c22522a4..47182397 100644
--- a/python/tvm_ffi/stub/generator.py
+++ b/python/tvm_ffi/stub/generator.py
@@ -82,6 +82,10 @@ class Generator(Protocol):
     #: to the pipeline; any other undeclared name is an error.
     directive_kinds: frozenset[str]
 
+    #: Type policies that also apply to references/descendants in other input 
files.
+    #: Unlike local type spellings, these directives must not depend on Rust 
imports.
+    shared_directive_kinds: frozenset[str]
+
     def default_ty_map(self) -> dict[str, str]:
         """Return the default FFI-origin -> target-type name map for this 
language."""
         ...
@@ -95,8 +99,9 @@ class Generator(Protocol):
     def add_directive(self, imports: Any, name: str, payload: str, lineno: 
int) -> None:
         """Record a one-line directive (raw payload) into ``imports``.
 
-        The collector is per file, so a directive applies to the blocks of the
-        file it appears in. ``name`` is always one of :attr:`directive_kinds`;
+        The collector is per file. The pipeline also seeds it with directives
+        in :attr:`shared_directive_kinds` from the other input files.
+        ``name`` is always one of :attr:`directive_kinds`;
         the payload's grammar is the generator's to define.
         """
         ...
diff --git a/python/tvm_ffi/stub/python_generator/generator.py 
b/python/tvm_ffi/stub/python_generator/generator.py
index 5d784818..d2f46089 100644
--- a/python/tvm_ffi/stub/python_generator/generator.py
+++ b/python/tvm_ffi/stub/python_generator/generator.py
@@ -46,6 +46,7 @@ class PythonGenerator:
     syntax = C.PYTHON_SYNTAX
     source_exts = frozenset({".py", ".pyi"})
     directive_kinds: frozenset[str] = frozenset({"import-object"})
+    shared_directive_kinds: frozenset[str] = frozenset()
 
     def default_ty_map(self) -> dict[str, str]:
         """Return the default FFI-origin -> Python-type name map."""
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index 7385dab7..0eca3783 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -41,6 +41,10 @@ provided in the same run: ``ffi.*`` by the crate, a 
``ty-map`` by a hand-written
 binding whose object struct is ``<Name>Obj``, anything else by an ``object/``
 block in one of the processed files. Otherwise the block is an error naming the
 missing keys, so a partial binding never references a module that does not 
exist.
+
+Layout and allocation are separate: ``no-alloc`` suppresses both allocators
+without hiding readable fields. Thread restrictions are inherited from the
+crate's ``Object`` base, including for opaque views.
 """
 
 from __future__ import annotations
@@ -259,11 +263,7 @@ class _ObjectRenderer:
             payload.origin in C_RUST.RUST_ANY_BACKED_OPTIONAL_PAYLOADS
             or payload.origin == "Optional"
         )
-        expected = (
-            C_RUST.RUST_OPTIONAL_FIELD_SIZE
-            if any_backed
-            else C_RUST.RUST_OBJECT_OPTIONAL_FIELD_SIZE
-        )
+        expected = C_RUST.RUST_OPTIONAL_FIELD_SIZE if any_backed else 
C_RUST.RUST_POINTER_SIZE
         if field.size not in (None, expected):
             return None
         if any_backed:
@@ -534,7 +534,9 @@ class _ObjectRenderer:
                 ]
             )
         elif verdict.is_complete:
-            sections += self._allocator_sections(base, has_parent)
+            hierarchy = {self.type_key, *self.info.ancestors}
+            if not hierarchy.intersection(self.imports.directives.no_alloc):
+                sections += self._allocator_sections(base, has_parent)
         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 b6543dbf..eb85701d 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -18,9 +18,20 @@
 
 from __future__ import annotations
 
+import ctypes
+
 #: One-line directives the Rust backend consumes.
 RUST_DIRECTIVE_KINDS = frozenset(
-    {"import-object", "field", "nullable", "enum", "opaque", "upcast", 
"custom-new"}
+    {
+        "import-object",
+        "field",
+        "nullable",
+        "enum",
+        "opaque",
+        "upcast",
+        "custom-new",
+        "no-alloc",
+    }
 )
 
 #: Default FFI-origin -> Rust-type map; ``::`` paths get a ``use``, bare names 
do not.
@@ -81,13 +92,12 @@ RUST_SCALAR_WIDTHS = {
     "f64": 8,
 }
 
-#: Size of an object reference field; ``nullable`` may only wrap those.
-RUST_POINTER_SIZE = 8
+#: The registry is loaded in this process, so its object pointers have the 
host ABI size.
+RUST_POINTER_SIZE = ctypes.sizeof(ctypes.c_void_p)
 
 #: C++ ``Optional<T>`` is a 16-byte ``TVMFFIAny`` cell, or a nullable pointer 
for object payloads.
 RUST_OPTIONAL_PATH = "tvm_ffi::Optional"
 RUST_OPTIONAL_FIELD_SIZE = 16
-RUST_OBJECT_OPTIONAL_FIELD_SIZE = 8
 
 #: ``Optional`` payloads kept as the 16-byte cell (a nested ``Optional`` too); 
the size is checked.
 RUST_ANY_BACKED_OPTIONAL_PAYLOADS = frozenset(
diff --git a/python/tvm_ffi/stub/rust_generator/directives.py 
b/python/tvm_ffi/stub/rust_generator/directives.py
index 185b9065..e7fd262a 100644
--- a/python/tvm_ffi/stub/rust_generator/directives.py
+++ b/python/tvm_ffi/stub/rust_generator/directives.py
@@ -16,7 +16,7 @@
 # under the License.
 """The Rust backend's one-line directives: payload grammar and per-file 
storage.
 
-Three address one reflected field as ``<type_key>.<field>``, three address a 
type::
+Field directives address ``<type_key>.<field>``; type directives address 
``<type_key>``::
 
     // tvm-ffi-stubgen(field): tirx.Add.a -> PrimExpr
     // tvm-ffi-stubgen(nullable): ir.Expr.span
@@ -24,6 +24,7 @@ Three address one reflected field as ``<type_key>.<field>``, 
three address a typ
     // tvm-ffi-stubgen(opaque): ir.SourceName
     // tvm-ffi-stubgen(upcast): tirx.Add -> PrimExpr
     // tvm-ffi-stubgen(custom-new): tirx.Add
+    // tvm-ffi-stubgen(no-alloc): ir.SourceName
 
 ``field`` sets the field's Rust type (a name in scope, or a ``::`` path to
 ``use``); on a field inherited from an ancestor it narrows the allocator
@@ -32,6 +33,10 @@ integer newtype for it; ``opaque`` keeps a type opaque even 
when its layout is
 reproducible. ``upcast`` adds a typed view outside the ancestor chain;
 ``custom-new`` says the wrapper's ``new`` is hand-written; the generated one is
 named ``from_complete_fields`` instead.
+
+``no-alloc`` preserves readable fields but suppresses both allocators, 
including
+in descendants. Like ``nullable`` and ``opaque``, it applies across all files 
in
+a generation run. Directives containing Rust type names remain file-local.
 """
 
 from __future__ import annotations
@@ -65,6 +70,7 @@ class Directives:
     opaque: set[str] = dataclasses.field(default_factory=set)
     upcasts: dict[str, list[str]] = dataclasses.field(default_factory=dict)
     custom_new: set[str] = dataclasses.field(default_factory=set)
+    no_alloc: set[str] = dataclasses.field(default_factory=set)
 
     def add(self, name: str, payload: str, lineno: int) -> None:
         """Parse and store one directive; raise ``ValueError`` on a malformed 
payload."""
@@ -83,6 +89,8 @@ class Directives:
             self.upcasts.setdefault(_type_target(name, lhs, lineno), 
[]).append(rust_type)
         elif name == "custom-new":
             self.custom_new.add(_type_target(name, payload, lineno))
+        elif name == "no-alloc":
+            self.no_alloc.add(_type_target(name, payload, lineno))
         else:
             raise ValueError(f"Unknown directive `{name}` at line {lineno}")
 
diff --git a/python/tvm_ffi/stub/rust_generator/generator.py 
b/python/tvm_ffi/stub/rust_generator/generator.py
index edfc7c07..20b72b59 100644
--- a/python/tvm_ffi/stub/rust_generator/generator.py
+++ b/python/tvm_ffi/stub/rust_generator/generator.py
@@ -25,6 +25,7 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 
 from .. import consts as C
+from ..lib_state import object_info_from_type_key
 from . import consts as C_RUST
 from .codegen import (
     finalize_rust_module_tree,
@@ -43,12 +44,13 @@ if TYPE_CHECKING:
 
 
 class RustGenerator:
-    """Generator that emits opaque Rust bindings for reflected objects (see 
:mod:`.codegen`)."""
+    """Generator for complete and opaque Rust bindings (see 
:mod:`.codegen`)."""
 
     name = "rust"
     syntax = C.RUST_SYNTAX
     source_exts = frozenset({".rs"})
     directive_kinds = C_RUST.RUST_DIRECTIVE_KINDS
+    shared_directive_kinds = frozenset({"no-alloc", "nullable", "opaque"})
 
     def default_ty_map(self) -> dict[str, str]:
         """Return the default FFI-origin -> Rust-type name map."""
@@ -66,6 +68,8 @@ class RustGenerator:
             imports.record(payload.split(";", 1)[0].strip())
         else:
             imports.directives.add(name, payload, lineno)
+            if name == "no-alloc":
+                object_info_from_type_key(payload.strip())
 
     def canonical_type_name(self, type_key: str) -> str:
         """Return the Rust path for a defined type key (matches 
:attr:`RustUse.path`)."""
@@ -100,7 +104,7 @@ class RustGenerator:
         obj_info: ObjectInfo,
         declared: Container[str] = frozenset(),
     ) -> None:
-        """Emit the opaque Rust binding for an ``object/<key>`` block."""
+        """Emit the Rust binding for an ``object/<key>`` block."""
         generate_rust_object(code, ty_map, imports, opt, obj_info, declared)
 
     def generate_import_section_block(
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index d87e72a9..a6bcd5b7 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -38,12 +38,18 @@ pub struct FunctionObj {
     cell: TVMFFIFunctionCell,
 }
 
-/// Error reference class
+/// A shareable packed function. Rust callbacks must have thread-safe captures.
 #[derive(Clone, ObjectRef)]
 pub struct Function {
     data: ObjectArc<FunctionObj>,
 }
 
+// SAFETY: Rust callbacks require Send + Sync, including their captured state.
+// Foreign callbacks must uphold the same contract (see from_extern_c). Opt in
+// only the handle, not FunctionObj or its potentially stateful derived 
layouts.
+unsafe impl Send for Function {}
+unsafe impl Sync for Function {}
+
 //------------------------------------------------------------------------
 // CallbackFunctionObjImpl
 //------------------------------------------------------------------------
@@ -311,7 +317,18 @@ impl Function {
             Ok(())
         }
     }
-    /// Construct a function from a packed function
+    /// Construct a function from a packed function.
+    ///
+    /// A Function can be called, retained, and dropped on another thread, so 
its
+    /// captured state must be `Send + Sync`. This does not require its 
arguments
+    /// or result to be `Send`: they are supplied and returned on the calling 
thread.
+    ///
+    /// ```compile_fail
+    /// use std::rc::Rc;
+    /// use tvm_ffi::{Any, Function};
+    /// let state = Rc::new(1i64);
+    /// let _ = Function::from_packed(move |_| Ok(Any::from(*state)));
+    /// ```
     /// # Arguments
     /// * `func` - The packed function in signature of `Fn(&[AnyView]) -> 
Result<Any>`
     ///
@@ -319,7 +336,7 @@ impl Function {
     /// * `Function` - The function
     pub fn from_packed<F>(func: F) -> Self
     where
-        F: Fn(&[AnyView]) -> Result<Any> + 'static,
+        F: Fn(&[AnyView]) -> Result<Any> + Send + Sync + 'static,
     {
         unsafe {
             let callback_arc = 
ObjectArc::new(CallbackFunctionObjImpl::from_callback(func));
@@ -330,7 +347,19 @@ impl Function {
         }
     }
 
-    /// Construct a function from a typed function
+    /// Construct a function from a typed function.
+    ///
+    /// Captured state must be `Send + Sync`, as for [`Self::from_packed`].
+    ///
+    /// ```compile_fail
+    /// use std::cell::Cell;
+    /// use tvm_ffi::Function;
+    /// let state = Cell::new(0i64); // Send, but not Sync.
+    /// let _ = Function::from_typed(move || {
+    ///     state.set(state.get() + 1);
+    ///     Ok(state.get())
+    /// });
+    /// ```
     /// # Arguments
     /// * `func` - The typed function with function signature of `F(T0, T1, 
...) -> Result<O>`
     ///
@@ -338,7 +367,7 @@ impl Function {
     /// * `Function` - The function
     pub fn from_typed<F, I, O>(func: F) -> Self
     where
-        F: AsPackedCallable<I, O> + 'static,
+        F: AsPackedCallable<I, O> + Send + Sync + 'static,
     {
         let closure = move |packed_args: &[AnyView]| -> Result<Any> {
             let ret_value = func.call_packed(packed_args)?;
@@ -352,6 +381,9 @@ impl Function {
     /// `handle` must be a valid pointer (or null) that is compatible with
     /// `safe_call` and `deleter`. The caller must ensure the handle outlives
     /// the returned `Function` (or that `deleter` properly frees it).
+    /// `safe_call` must support concurrent calls from arbitrary threads, and
+    /// `deleter` must be safe to run on any thread. These requirements also 
apply
+    /// to the state behind `handle`.
     pub unsafe fn from_extern_c(
         handle: *mut std::ffi::c_void,
         safe_call: TVMFFISafeCallType,
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index d690a64b..2619817a 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -25,14 +25,24 @@ pub use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 /// Object related ABI handling
 use tvm_ffi_sys::{TVMFFIAny, TVMFFIGetTypeInfo, TVMFFIObject, 
COMBINED_REF_COUNT_BOTH_ONE};
 
-/// Object type is by default the TVMFFIObject
+/// The common object header, including for objects with unknown native state.
+///
+/// Objects are not `Send` or `Sync` by default: atomic reference counting does
+/// not make an object's fields or destructor thread-safe. Keeping this marker
+/// in the base also prevents casts to a base node or [`ObjectRef`] from 
erasing
+/// a derived object's thread restrictions. It does not change the C ABI 
layout.
+///
+/// An explicit unsafe `Send`/`Sync` implementation for a binding must account 
for
+/// every dynamic subtype it accepts, including hidden state and destruction.
 #[repr(C)]
 pub struct Object {
-    /// example implementation of the object
     header: TVMFFIObject,
+    _thread_confined: std::marker::PhantomData<std::rc::Rc<()>>,
 }
 
-/// Arc-like wrapper for Object that allows shared ownership
+/// Arc-like wrapper for Object that allows shared ownership.
+///
+/// Mutable dereferencing panics if another strong or external weak owner 
exists.
 ///
 /// \tparam T The type of the object to be wrapped
 #[repr(C)]
@@ -44,6 +54,19 @@ pub struct ObjectArc<T: ObjectCore> {
 unsafe impl<T: Send + Sync + ObjectCore> Send for ObjectArc<T> {}
 unsafe impl<T: Send + Sync + ObjectCore> Sync for ObjectArc<T> {}
 
+// The allocation length must outlive T's destructor when weak owners remain.
+// Keep it before the ABI-visible object, never in the live reference-count 
header.
+fn extra_items_layout<T: ObjectCoreWithExtraItems>(count: usize) -> 
(std::alloc::Layout, usize) {
+    use std::alloc::Layout;
+    let items = Layout::array::<T::ExtraItem>(count).expect("extra items 
layout overflow");
+    let (body, _) = Layout::new::<T>()
+        .extend(items)
+        .expect("object layout overflow");
+    Layout::new::<usize>()
+        .extend(body)
+        .expect("allocation layout overflow")
+}
+
 /// Traits that can be used to check if a type is an object
 ///
 /// This trait is unsafe because it is used to access the object header
@@ -340,7 +363,7 @@ pub mod unsafe_ {
     /// * `obj` - The object to increase the reference count
     #[inline]
     pub unsafe fn inc_ref(handle: *mut TVMFFIObject) {
-        let obj = &mut *handle;
+        let obj = &*handle;
         obj.combined_ref_count.fetch_add(1, Ordering::Relaxed);
     }
 
@@ -352,17 +375,16 @@ pub mod unsafe_ {
     /// * `obj` - The object to decrease the reference count
     #[inline]
     pub(crate) unsafe fn dec_ref(handle: *mut TVMFFIObject) {
-        let obj = &mut *handle;
-        let old_combined_count = obj
+        // Do not keep a Rust reference to the header across a deleter call.
+        let old_combined_count = (*handle)
             .combined_ref_count
-            .fetch_sub(COMBINED_REF_COUNT_STRONG_ONE, Ordering::Relaxed);
+            // Match C++ Object::DecRef: publish this owner's writes before the
+            // last owner acquires them and runs the destructor.
+            .fetch_sub(COMBINED_REF_COUNT_STRONG_ONE, Ordering::Release);
         if old_combined_count == COMBINED_REF_COUNT_BOTH_ONE {
-            if let Some(deleter) = obj.deleter {
-                fence(Ordering::Acquire);
-                deleter(
-                    obj as *mut TVMFFIObject as *mut c_void,
-                    kTVMFFIObjectDeleterFlagBitMaskBoth as i32,
-                );
+            fence(Ordering::Acquire);
+            if let Some(deleter) = (*handle).deleter {
+                deleter(handle.cast(), kTVMFFIObjectDeleterFlagBitMaskBoth as 
i32);
             }
         } else if (old_combined_count & COMBINED_REF_COUNT_MASK_U32)
             == COMBINED_REF_COUNT_STRONG_ONE
@@ -370,22 +392,16 @@ pub mod unsafe_ {
             // slow path, there is still a weak reference left
             // need to run two phase decrement
             fence(Ordering::Acquire);
-            if let Some(deleter) = obj.deleter {
-                deleter(
-                    obj as *mut TVMFFIObject as *mut c_void,
-                    kTVMFFIObjectDeleterFlagBitMaskStrong as i32,
-                );
+            if let Some(deleter) = (*handle).deleter {
+                deleter(handle.cast(), kTVMFFIObjectDeleterFlagBitMaskStrong 
as i32);
             }
-            let old_weak_count = obj
+            let old_weak_count = (*handle)
                 .combined_ref_count
                 .fetch_sub(COMBINED_REF_COUNT_WEAK_ONE, Ordering::Release);
             if old_weak_count == COMBINED_REF_COUNT_WEAK_ONE {
                 fence(Ordering::Acquire);
-                if let Some(deleter) = obj.deleter {
-                    deleter(
-                        obj as *mut TVMFFIObject as *mut c_void,
-                        kTVMFFIObjectDeleterFlagBitMaskWeak as i32,
-                    );
+                if let Some(deleter) = (*handle).deleter {
+                    deleter(handle.cast(), kTVMFFIObjectDeleterFlagBitMaskWeak 
as i32);
                 }
             }
         }
@@ -393,13 +409,13 @@ pub mod unsafe_ {
 
     #[inline]
     pub(crate) unsafe fn strong_count(handle: *mut TVMFFIObject) -> usize {
-        let obj = &mut *handle;
+        let obj = &*handle;
         (obj.combined_ref_count.load(Ordering::Relaxed) & 
COMBINED_REF_COUNT_MASK_U32) as usize
     }
 
     #[inline]
     pub(crate) unsafe fn weak_count(handle: *mut TVMFFIObject) -> usize {
-        let obj = &mut *handle;
+        let obj = &*handle;
         (obj.combined_ref_count.load(Ordering::Relaxed) >> 32) as usize
     }
 
@@ -424,31 +440,16 @@ pub mod unsafe_ {
         T: super::ObjectCoreWithExtraItems<ExtraItem = U>,
     {
         let obj = ptr as *mut T;
-        if flags == kTVMFFIObjectDeleterFlagBitMaskBoth as i32 {
-            let extra_items_count = T::extra_items_count(&(*obj));
+        if flags & kTVMFFIObjectDeleterFlagBitMaskStrong as i32 != 0 {
             std::ptr::drop_in_place(obj);
-            let layout = std::alloc::Layout::from_size_align(
-                std::mem::size_of::<T>() + extra_items_count * 
std::mem::size_of::<U>(),
-                std::mem::align_of::<T>(),
-            )
-            .unwrap();
-            std::alloc::dealloc(ptr as *mut u8, layout);
-        } else {
-            assert_eq!(std::mem::size_of::<T>() % std::mem::size_of::<u64>(), 
0);
-            if flags & kTVMFFIObjectDeleterFlagBitMaskStrong as i32 != 0 {
-                let extra_items_count = T::extra_items_count(&(*obj));
-                std::ptr::drop_in_place(obj);
-                std::ptr::write(obj as *mut u64, extra_items_count as u64);
-            }
-            if flags & kTVMFFIObjectDeleterFlagBitMaskWeak as i32 != 0 {
-                let extra_items_count = std::ptr::read(obj as *mut u64) as 
usize;
-                let layout = std::alloc::Layout::from_size_align(
-                    std::mem::size_of::<T>() + extra_items_count * 
std::mem::size_of::<U>(),
-                    std::mem::align_of::<T>(),
-                )
-                .unwrap();
-                std::alloc::dealloc(ptr as *mut u8, layout);
-            }
+        }
+        if flags & kTVMFFIObjectDeleterFlagBitMaskWeak as i32 != 0 {
+            // The object's offset depends only on alignment, not item count.
+            let (_, offset) = super::extra_items_layout::<T>(0);
+            let allocation = ptr.cast::<u8>().sub(offset);
+            let count = allocation.cast::<usize>().read();
+            let (layout, _) = super::extra_items_layout::<T>(count);
+            std::alloc::dealloc(allocation, layout);
         }
     }
 }
@@ -461,6 +462,7 @@ impl Object {
     pub fn new() -> Self {
         Self {
             header: TVMFFIObject::new(),
+            _thread_confined: std::marker::PhantomData,
         }
     }
 }
@@ -519,16 +521,13 @@ impl<T: ObjectCore> ObjectArc<T> {
             assert_eq!(std::mem::align_of::<T>() % std::mem::align_of::<U>(), 
0);
             assert_eq!(std::mem::size_of::<T>() % std::mem::align_of::<U>(), 
0);
             let extra_items_count = T::extra_items_count(&data);
-            let layout = std::alloc::Layout::from_size_align(
-                std::mem::size_of::<T>() + extra_items_count * 
std::mem::size_of::<U>(),
-                std::mem::align_of::<T>(),
-            )
-            .unwrap();
+            let (layout, offset) = extra_items_layout::<T>(extra_items_count);
             let raw_data_ptr = std::alloc::alloc(layout);
             if raw_data_ptr.is_null() {
                 std::alloc::handle_alloc_error(layout);
             }
-            let ptr = raw_data_ptr as *mut T;
+            raw_data_ptr.cast::<usize>().write(extra_items_count);
+            let ptr = raw_data_ptr.add(offset).cast::<T>();
             std::ptr::write(ptr, data);
             // now override the header directly
             std::ptr::write(
@@ -603,7 +602,7 @@ impl<T: ObjectCore> ObjectArc<T> {
     /// * `*mut T` - The raw pointer
     #[inline]
     pub unsafe fn as_raw_mut(this: &mut Self) -> *mut T {
-        this.ptr.as_mut()
+        this.ptr.as_ptr()
     }
 
     /// Get the strong reference count of the ObjectArc
@@ -615,9 +614,7 @@ impl<T: ObjectCore> ObjectArc<T> {
     /// * `usize` - The strong reference count
     #[inline]
     pub fn strong_count(this: &Self) -> usize {
-        unsafe {
-            unsafe_::strong_count(this.ptr.as_ref() as *const T as *mut T as 
*mut TVMFFIObject)
-        }
+        unsafe { 
unsafe_::strong_count(this.ptr.as_ptr().cast::<TVMFFIObject>()) }
     }
 
     /// Get the weak reference count of the ObjectArc
@@ -629,7 +626,7 @@ impl<T: ObjectCore> ObjectArc<T> {
     /// * `usize` - The weak reference count
     #[inline]
     pub fn weak_count(this: &Self) -> usize {
-        unsafe { unsafe_::weak_count(this.ptr.as_ref() as *const T as *mut T 
as *mut TVMFFIObject) }
+        unsafe { unsafe_::weak_count(this.ptr.as_ptr().cast::<TVMFFIObject>()) 
}
     }
 }
 
@@ -642,18 +639,29 @@ impl<T: ObjectCore> Deref for ObjectArc<T> {
     }
 }
 
-// implement DerefMut for ObjectArc
+// Exclusive Rust access requires both a single strong owner and no external
+// weak owners that could acquire another strong reference during the borrow.
 impl<T: ObjectCore> DerefMut for ObjectArc<T> {
     #[inline]
     fn deref_mut(&mut self) -> &mut Self::Target {
-        unsafe { self.ptr.as_mut() }
+        unsafe {
+            let header = &*self.ptr.as_ptr().cast::<TVMFFIObject>();
+            assert_eq!(
+                header
+                    .combined_ref_count
+                    .load(std::sync::atomic::Ordering::Acquire),
+                COMBINED_REF_COUNT_BOTH_ONE,
+                "cannot mutably borrow a shared ObjectArc"
+            );
+            self.ptr.as_mut()
+        }
     }
 }
 
 // implement Drop for ObjectArc
 impl<T: ObjectCore> Drop for ObjectArc<T> {
     fn drop(&mut self) {
-        unsafe { unsafe_::dec_ref(self.ptr.as_mut() as *mut T as *mut 
TVMFFIObject) }
+        unsafe { unsafe_::dec_ref(self.ptr.as_ptr().cast::<TVMFFIObject>()) }
     }
 }
 
@@ -661,7 +669,7 @@ impl<T: ObjectCore> Drop for ObjectArc<T> {
 impl<T: ObjectCore> Clone for ObjectArc<T> {
     #[inline]
     fn clone(&self) -> Self {
-        unsafe { unsafe_::inc_ref(self.ptr.as_ref() as *const T as *mut T as 
*mut TVMFFIObject) }
+        unsafe { unsafe_::inc_ref(self.ptr.as_ptr().cast::<TVMFFIObject>()) }
         Self {
             ptr: self.ptr,
             _phantom: std::marker::PhantomData,
diff --git a/rust/tvm-ffi/tests/test_function.rs 
b/rust/tvm-ffi/tests/test_function.rs
index 07ed6779..ccf2e9cb 100644
--- a/rust/tvm-ffi/tests/test_function.rs
+++ b/rust/tvm-ffi/tests/test_function.rs
@@ -53,6 +53,32 @@ fn test_function_from_packed() {
     assert_eq!(i32::try_from(result).unwrap(), 6);
 }
 
+#[test]
+fn test_function_thread_safe_captures_and_global_cache() {
+    use std::sync::atomic::{AtomicUsize, Ordering};
+    use std::sync::Arc;
+
+    let calls = Arc::new(AtomicUsize::new(0));
+    let state = calls.clone();
+    let function = Function::from_typed(move |value: i64| {
+        state.fetch_add(1, Ordering::Relaxed);
+        // Neither the argument holder nor the returned Any needs to be Send.
+        cached_global_func!("testing.echo").call_tuple((value,))
+    });
+    std::thread::scope(|scope| {
+        for value in 0..4i64 {
+            let function = &function;
+            scope.spawn(move || {
+                let result: i64 = 
function.call_tuple((value,)).unwrap().try_into().unwrap();
+                assert_eq!(result, value);
+            });
+        }
+    });
+    assert_eq!(calls.load(Ordering::Relaxed), 4);
+    std::thread::spawn(move || drop(function)).join().unwrap();
+    assert_eq!(Arc::strong_count(&calls), 1);
+}
+
 #[test]
 fn test_function_from_typed() {
     let offset = 2;
diff --git a/rust/tvm-ffi/tests/test_object.rs 
b/rust/tvm-ffi/tests/test_object.rs
index ebab18e0..54b3ccd1 100644
--- a/rust/tvm-ffi/tests/test_object.rs
+++ b/rust/tvm-ffi/tests/test_object.rs
@@ -21,6 +21,34 @@ use std::sync::Arc;
 use std::{collections::HashMap, hash::Hash};
 use tvm_ffi::*;
 
+// An erased owning or borrowed view must not acquire thread-safety merely
+// because its concrete object's non-thread-safe fields are no longer visible.
+macro_rules! assert_not_impl {
+    ($ty:ty: $bound:path) => {
+        const _: fn() = || {
+            trait AmbiguousIfImpl<A> {
+                fn check() {}
+            }
+            impl<T: ?Sized> AmbiguousIfImpl<()> for T {}
+            struct ImplementsBound;
+            impl<T: ?Sized + $bound> AmbiguousIfImpl<ImplementsBound> for T {}
+            let _ = <$ty as AmbiguousIfImpl<_>>::check;
+        };
+    };
+}
+
+assert_not_impl!(Object: Send);
+assert_not_impl!(Object: Sync);
+assert_not_impl!(ObjectArc<Object>: Send);
+assert_not_impl!(ObjectArc<Object>: Sync);
+assert_not_impl!(tvm_ffi::object::ObjectRef: Send);
+assert_not_impl!(tvm_ffi::object::ObjectRef: Sync);
+assert_not_impl!(ObjectIdentity: Send);
+assert_not_impl!(ObjectIdentity: Sync);
+assert_not_impl!(&Object: Send);
+assert_not_impl!(Any: Send);
+assert_not_impl!(AnyView<'static>: Send);
+
 // must have repr(C) for the object header stays in the same position
 #[repr(C)]
 struct TestIntObj {
@@ -100,6 +128,21 @@ fn test_object_arc() {
     assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
 }
 
+#[test]
+fn test_object_arc_mutable_borrow_requires_unique_ownership() {
+    let deleted = Arc::new(AtomicU32::new(0));
+    let mut value = ObjectArc::new(TestIntObj::new(1, deleted, 0));
+    let alias = value.clone();
+    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+        value.value = 2;
+    }));
+    assert!(result.is_err());
+    assert_eq!(alias.value, 1);
+    drop(alias);
+    value.value = 3;
+    assert_eq!(value.value, 3);
+}
+
 #[test]
 fn test_object_arc_with_extra_items() {
     let delete_counter = Arc::new(AtomicU32::new(0));
@@ -123,6 +166,39 @@ fn test_object_arc_with_extra_items() {
     assert_eq!(delete_counter.load(Ordering::Relaxed), 1);
 }
 
+#[test]
+fn test_extra_items_with_a_surviving_weak_owner() {
+    use tvm_ffi::tvm_ffi_sys::{
+        TVMFFIObjectDeleterFlagBitMask::kTVMFFIObjectDeleterFlagBitMaskWeak,
+        COMBINED_REF_COUNT_WEAK_ONE,
+    };
+    for count in [0, 3] {
+        let deleted = Arc::new(AtomicU32::new(0));
+        let value = ObjectArc::new_with_extra_items(TestIntObj::new(1, 
deleted.clone(), count));
+        // Model a native weak reference through the existing combined-count 
ABI.
+        unsafe {
+            let header = ObjectArc::as_raw(&value).cast::<TVMFFIObject>();
+            (*header)
+                .combined_ref_count
+                .fetch_add(COMBINED_REF_COUNT_WEAK_ONE, Ordering::Relaxed);
+            drop(value);
+            assert_eq!(deleted.load(Ordering::Relaxed), 1);
+            assert_eq!(
+                (*header)
+                    .combined_ref_count
+                    .fetch_sub(COMBINED_REF_COUNT_WEAK_ONE, Ordering::Release),
+                COMBINED_REF_COUNT_WEAK_ONE,
+            );
+            std::sync::atomic::fence(Ordering::Acquire);
+            (*header).deleter.unwrap()(
+                header.cast_mut().cast(),
+                kTVMFFIObjectDeleterFlagBitMaskWeak as i32,
+            );
+        }
+        assert_eq!(deleted.load(Ordering::Relaxed), 1);
+    }
+}
+
 #[test]
 fn test_object_arc_from_raw() {
     unsafe {
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index 05449b8c..35474ad8 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -18,7 +18,12 @@
 
 from __future__ import annotations
 
+import ctypes
+import os
 import re
+import shutil
+import subprocess
+import sys
 from collections.abc import Container, Iterator
 from pathlib import Path
 
@@ -26,6 +31,7 @@ import pytest
 import tvm_ffi.stub.cli as stub_cli
 import tvm_ffi.testing  # noqa: F401  (loads the `testing.*` fixture types)
 from tvm_ffi.core import TypeSchema
+from tvm_ffi.libinfo import find_libtvm_ffi
 from tvm_ffi.stub import consts as C
 from tvm_ffi.stub.cli import _stage_1, _stage_3
 from tvm_ffi.stub.file_utils import CodeBlock, FileInfo
@@ -241,6 +247,7 @@ def test_directives_parse() -> None:
     directives.add("upcast", "tirx.Add -> PrimExpr", 6)
     directives.add("upcast", "tirx.Add -> crate::typed::TypedExpr", 7)
     directives.add("custom-new", " tirx.Add ", 8)
+    directives.add("no-alloc", " ir.SourceName ", 9)
     assert directives.field_types == {"tirx.Add.a": "PrimExpr"}
     assert directives.nullable == {"ir.Expr.span"}
     assert directives.enums == {
@@ -250,6 +257,7 @@ def test_directives_parse() -> None:
     assert directives.opaque == {"ir.SourceName"}
     assert directives.upcasts == {"tirx.Add": ["PrimExpr", 
"crate::typed::TypedExpr"]}
     assert directives.custom_new == {"tirx.Add"}
+    assert directives.no_alloc == {"ir.SourceName"}
 
 
 @pytest.mark.parametrize(
@@ -284,6 +292,7 @@ def 
test_generator_declares_its_directives_and_records_imports() -> None:
         "opaque",
         "upcast",
         "custom-new",
+        "no-alloc",
     }
     imports = RUST.new_imports()
     RUST.add_directive(imports, "import-object", 
"tvm_ffi.libinfo.Foo;False;_Foo", 1)
@@ -881,6 +890,33 @@ def test_complete_optional_field_mirrors() -> None:
     assert "tvm_ffi::Optional" in _uses(imports)
 
 
[email protected]("pointer_size", [4, 8])
+def test_object_reference_fields_use_native_pointer_size(
+    monkeypatch: pytest.MonkeyPatch, pointer_size: int
+) -> None:
+    assert RC.RUST_POINTER_SIZE == ctypes.sizeof(ctypes.c_void_p)
+    monkeypatch.setattr(RC, "RUST_POINTER_SIZE", pointer_size)
+    info = _info(
+        "demo.References",
+        (
+            _field("value", "Object", 24, pointer_size),
+            _field(
+                "optional",
+                TypeSchema("Optional", (TypeSchema("Object"),)),
+                24 + pointer_size,
+                pointer_size,
+            ),
+        ),
+        total_size=24 + 2 * pointer_size,
+    )
+    imports = RustImports()
+    RUST.add_directive(imports, "nullable", "demo.References.value", 1)
+    text, _ = _render(info, imports)
+    assert "/// Complete:" in text
+    assert "pub value: Option<ObjectRef>," in text
+    assert "pub optional: Option<ObjectRef>," in text
+
+
 @pytest.mark.parametrize(
     "field",
     [
@@ -921,6 +957,22 @@ def test_custom_new_renames_the_wrapper_allocator() -> 
None:
     assert "    pub fn new(" not in text
 
 
+def test_no_alloc_preserves_fields_and_applies_to_descendants() -> None:
+    base = _info("demo.Base", (_field("value", "Object", 24, 8),), 
total_size=32)
+    child = _info("demo.Child", parent="demo.Base", total_size=32)
+    _register(base)
+    imports = RustImports()
+    for name in ("no-alloc", "custom-new"):
+        imports.directives.add(name, "demo.Base", 1)
+    for info in (base, child):
+        text, _ = _render(info, imports)
+        assert "/// Complete:" in text
+        assert "fn new(" not in text
+        assert "from_complete_fields" not in text
+    text, _ = _render(base, imports)
+    assert "pub value: ObjectRef," in text
+
+
 def test_upcast_directive_adds_typed_views() -> None:
     """`upcast` targets follow the ancestor chain; a `::` path is imported. 
Opaque: no allocator."""
     info = _info("demo.Leaf", parent="demo.Base")
@@ -1121,6 +1173,168 @@ def 
test_stage_3_applies_directives_to_a_registered_type(tmp_path: Path) -> None
     assert "use tvm_ffi::Error;" in text
 
 
+def test_cli_shares_object_policies_across_files(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    # The child lives in a separate file; it must not bypass the base's 
allocation policy.
+    for filename, key, directives in (
+        ("base.rs", "testing.TestCxxClassBase", ["no-alloc"]),
+        ("child.rs", "testing.TestCxxClassDerived", []),
+    ):
+        (tmp_path / filename).write_text(
+            "\n".join(
+                [f"{C.RUST_SYNTAX.directive(name)} {key}" for name in 
directives]
+                + [f"{C.RUST_SYNTAX.begin} object/{key}", C.RUST_SYNTAX.end, 
""]
+            ),
+            encoding="utf-8",
+        )
+    monkeypatch.setattr("sys.argv", ["tvm-ffi-stubgen", "--target", "rust", 
str(tmp_path)])
+    assert stub_cli.__main__() == 0
+    for filename in ("base.rs", "child.rs"):
+        text = (tmp_path / filename).read_text(encoding="utf-8")
+        assert "/// Complete:" in text
+        assert "fn new(" not in text
+    assert stub_cli.__main__() == 0
+
+
[email protected]("policy", ["nullable", "opaque"])
+def test_cli_preserves_parent_layout_policies_across_files(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, policy: str
+) -> None:
+    base = _info("demo.Base", (_field("value", "Object", 24, 8),), 
total_size=32)
+    child = _info("demo.Child", parent="demo.Base", total_size=32)
+    _register(base, child)
+    monkeypatch.setattr(stub_cli, "object_info_from_type_key", 
codegen.object_info_from_type_key)
+    target = "demo.Base.value" if policy == "nullable" else "demo.Base"
+    for filename, key in (("parent.rs", "demo.Base"), ("child.rs", 
"demo.Child")):
+        directive = f"{C.RUST_SYNTAX.directive(policy)} {target}\n" if key == 
"demo.Base" else ""
+        (tmp_path / filename).write_text(
+            f"{directive}{C.RUST_SYNTAX.begin} 
object/{key}\n{C.RUST_SYNTAX.end}\n",
+            encoding="utf-8",
+        )
+    # Process the child first: generation must not depend on its parent's file 
order.
+    monkeypatch.setattr(
+        "sys.argv",
+        [
+            "tvm-ffi-stubgen",
+            "--target",
+            "rust",
+            str(tmp_path / "child.rs"),
+            str(tmp_path / "parent.rs"),
+        ],
+    )
+    assert stub_cli.__main__() == 0
+    for filename in ("parent.rs", "child.rs"):
+        text = (tmp_path / filename).read_text(encoding="utf-8")
+        if policy == "nullable":
+            assert "pub fn new(value: Option<ObjectRef>) -> Self" in text
+        else:
+            assert "/// Opaque:" in text
+            assert "fn new(" not in text
+    assert stub_cli.__main__() == 0
+
+
[email protected](shutil.which("cargo") is None, reason="Rust toolchain not 
installed")
+def test_generated_object_contracts_compile_and_drop(tmp_path: Path) -> None:
+    """Compile the generated code, including negative API checks, against the 
real crate."""
+    imports = RustImports()
+    RUST.add_directive(imports, "no-alloc", "testing.TestCxxClassBase", 1)
+    RUST.add_directive(imports, "nullable", "testing.TestDeepCopyEdges.v_obj", 
2)
+    RUST.add_directive(imports, "custom-new", "testing.TestDeepCopyEdges", 3)
+    keys = ["TestCxxClassBase", "TestCxxClassHiddenField", "TestObjectBase", 
"TestDeepCopyEdges"]
+    bodies = [_render(object_info_from_type_key(f"testing.{key}"), imports)[0] 
for key in keys]
+    docs = "\n".join(
+        f"//! ```compile_fail\n//! {snippet}\n//! ```"
+        for snippet in (
+            "use generated_contracts::TestCxxClassBase; let _ = 
TestCxxClassBase::new(1, 2);",
+            "fn send<T: Send>() {} 
send::<generated_contracts::TestCxxClassHiddenField>();",
+            "fn sync<T: Sync>() {} 
sync::<generated_contracts::TestCxxClassHiddenField>();",
+            "fn send<T: Send>() {} 
send::<generated_contracts::TestObjectBase>();",
+            "use generated_contracts::TestDeepCopyEdges; let _ = 
TestDeepCopyEdges::new(1.into(), None);",
+        )
+    )
+    source = docs + "\n" + "\n".join(item.as_use_line() for item in 
imports.items)
+    source += "\n" + "\n".join(bodies)
+    source += r"""
+// Keep semantic construction in the binding, using the existing nullable 
allocator.
+impl TestDeepCopyEdges {
+    pub fn new(v_any: tvm_ffi::Any, v_obj: tvm_ffi::object::ObjectRef) -> Self 
{
+        Self::from_complete_fields(v_any, Some(v_obj))
+    }
+}
+
+#[test]
+fn native_null_storage_is_safe_to_drop() {
+    use tvm_ffi::object::ObjectRefCore;
+    use tvm_ffi::tvm_ffi_sys::{TVMFFIAny, TVMFFIGetTypeInfo, 
TVMFFIFieldSetter, TVMFFITestingDummyTarget};
+    use 
tvm_ffi::tvm_ffi_sys::TVMFFIFieldFlagBitMask::kTVMFFIFieldFlagBitSetterIsFunctionObj;
+    assert_eq!(unsafe { TVMFFITestingDummyTarget() }, 0);
+    let child = TestObjectBase::new(1, 2.0, "child".into());
+    let object = 
tvm_ffi::object::ObjectRef::try_from(tvm_ffi::Any::from(child.clone())).unwrap();
+    let holder = TestDeepCopyEdges::new(7i64.into(), object);
+    assert!(holder.v_obj.as_ref().unwrap().same_as(&child));
+    let native_child = FieldGetter::new(TestDeepCopyEdgesObj::type_index(), 
"v_obj")
+        .unwrap().get_any(&*holder).unwrap();
+    
assert!(tvm_ffi::object::ObjectRef::try_from(native_child).unwrap().same_as(&child));
+    // Clear the field using the existing C++ reflection setter, leaving the 
same
+    // null storage as a native move-out. No borrowed field reference is live 
here.
+    unsafe {
+        let info = &*TVMFFIGetTypeInfo(TestDeepCopyEdgesObj::type_index());
+        let field = (0..info.num_fields as usize).map(|i| &*info.fields.add(i))
+            .find(|f| f.name.as_str() == "v_obj").unwrap();
+        assert!(!field.setter.is_null());
+        assert_eq!(field.flags & (kTVMFFIFieldFlagBitSetterIsFunctionObj as 
i64), 0);
+        let setter: TVMFFIFieldSetter = std::mem::transmute(field.setter);
+        let ptr = ObjectArc::as_raw(TestDeepCopyEdges::data(&holder));
+        assert_eq!(setter(ptr.cast_mut().cast::<u8>().offset(field.offset as 
isize).cast(),
+                         &TVMFFIAny::new()), 0);
+    }
+    assert!(holder.v_obj.is_none());
+    drop(holder);
+    assert_eq!(ObjectArc::strong_count(TestObjectBase::data(&child)), 1);
+}
+"""
+    (tmp_path / "src").mkdir()
+    (tmp_path / "src/lib.rs").write_text(source, encoding="utf-8")
+    crate = Path(__file__).resolve().parents[2] / "rust/tvm-ffi"
+    (tmp_path / "Cargo.toml").write_text(
+        '[package]\nname = "generated_contracts"\nversion = "0.0.0"\nedition = 
"2021"\n'
+        f'[dependencies]\ntvm-ffi = {{ path = "{crate.as_posix()}" }}\n',
+        encoding="utf-8",
+    )
+    env = os.environ.copy()
+    # Do not inherit a workspace target directory: parallel test runs are 
independent.
+    env["CARGO_TARGET_DIR"] = str(tmp_path / "target")
+    loader = (
+        "PATH"
+        if sys.platform == "win32"
+        else ("DYLD_LIBRARY_PATH" if sys.platform == "darwin" else 
"LD_LIBRARY_PATH")
+    )
+    env[loader] = str(Path(find_libtvm_ffi()).parent) + os.pathsep + 
env.get(loader, "")
+    result = subprocess.run(
+        ["cargo", "test", "--quiet"],
+        cwd=tmp_path,
+        env=env,
+        capture_output=True,
+        text=True,
+        check=False,
+        timeout=180,
+    )
+    assert result.returncode == 0, result.stdout + result.stderr
+
+
+def test_cli_rejects_unknown_no_alloc_type(tmp_path: Path, monkeypatch: 
pytest.MonkeyPatch) -> None:
+    src = tmp_path / "mod.rs"
+    original = (
+        f"{C.RUST_SYNTAX.directive('no-alloc')} 
testing.MissingObjectContract\n"
+        f"{C.RUST_SYNTAX.begin} 
object/testing.TestCxxClassBase\n{C.RUST_SYNTAX.end}\n"
+    )
+    src.write_text(original, encoding="utf-8")
+    monkeypatch.setattr("sys.argv", ["tvm-ffi-stubgen", "--target", "rust", 
str(tmp_path)])
+    assert stub_cli.__main__() == 2
+    assert src.read_text(encoding="utf-8") == original
+
+
 def test_cli_init_generates_a_module_tree(tmp_path: Path, monkeypatch: 
pytest.MonkeyPatch) -> None:
     monkeypatch.setattr(
         "sys.argv",

Reply via email to