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 3598d6f8 [Rust] Optional Send/Sync, is::<T>(), type_key(), prelude, 
weak_count as std (#964)
3598d6f8 is described below

commit 3598d6f84d8604dcf641ddc235340c19a0f74843
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Nov 8 11:18:33 2026 -0500

    [Rust] Optional Send/Sync, is::<T>(), type_key(), prelude, weak_count as 
std (#964)
    
    - `Optional<T>` is `Send`/`Sync` when `T` is, like `Array<T>` and
    `Map<K, V>`.
    - `is::<T>()` on `Any`, `AnyView`, `MutationView` and object references:
    the strict type test of `downcast_ref`, as std `Any::is`, covering every
    `AnyCompatible` type including scalars.
    - `type_key()` on `Any`, `AnyView` and object references, as C++
    `GetTypeKey`.
    - `ObjectArc::weak_count` matches `Arc::weak_count`: the weak reference
    the strong references share is not counted.
    - `object::unsafe_::inc_ref` is crate-private like `dec_ref`;
    `ObjectDerivesFrom` and `marker`, named only by macro expansions, are
    `doc(hidden)`.
    - `Display for Error` prints `Kind: message`, as C++ `what()`; `Debug`
    keeps the traceback, without its leading blank line.
    - `format_err!(kind, fmt, args...)` makes an `Error` with the source
    location `bail!` records, without returning; `bail!` returns it.
    - `Error::new(kind, message)`; the backtrace, cause and extra context
    are set with `with_backtrace`, `with_cause` and `with_extra_context`,
    copy-on-write like `with_appended_backtrace`. They replace
    `new_with_cause_and_extra_context`.
    - `ErrorKind` is `Copy` and `Hash`. Callers use `Error::from_panic`
    directly; the hidden `function_internal::panic_to_error` is removed.
    - `String` and `Bytes` replace the blanket `From`/`PartialEq` impls over
    `AsRef<str>`/`AsRef<[u8]>` with concrete ones, as std `String` and
    `Vec<u8>` have. This lets `String` implement `AsRef<str>` and
    `Borrow<str>`, and `Bytes` `AsRef<[u8]>` and `Borrow<[u8]>`. Comparisons
    work in both directions, and both convert into std `String`/`Vec<u8>`.
    - `Debug` for `String` and `Bytes` prints as `str` and `[u8]` do;
    `Bytes` no longer implements `Display`.
    
    - `NDAllocator` is `Send + Sync`: a tensor frees its data through the
    allocator when the last reference drops, which may be on any thread.
    - `FieldGetter::of::<N>(name)` resolves a reflected field against `N`,
    so reads take `&N` without a per-object subtype check;
    `cached_field_getter!(N, "f")` caches one per call site, as
    `cached_global_func!` does. The Rust stub generator emits it for field
    accessors, and its enum conversions use the two-argument `Error::new`.
    - `TypeAttrDef::attr`/`native_fn_attr` are renamed
    `set_attr`/`set_native_fn_attr`, as `EnumDef::set_attr`.
    
    - `#[rs_object(type_key = "...", ...)]` names the type key, as
    `#[object(type_key = ...)]` does, and takes `crate = path`; a positional
    key is a compile error that shows the named form.
    - `into_typed_fn!(f, Fn(..) -> R)` accepts only `Fn` in the trait
    position; any other name is a compile error.
    - `Function::get_global` reports a missing function as `ValueError`, as
    C++ `Function::GetGlobalRequired`.
    - `AsPackedCallable`, the bound of `Function::from_typed` and
    `register_global_typed`, is re-exported and documented under
    `tvm_ffi::function`, with the accepted parameter types.
    
    
    - `MutationView::with_inplace_mode` is renamed `narrow_inplace_mode`: it
    only restricts the mode, never upgrading it.
---
 docs/guides/rust_lang_guide.rst                    |  16 +--
 .../rust/src/generated/rust_stubgen/mod.rs         |   2 +-
 python/tvm_ffi/stub/rust_generator/codegen.py      |  79 ++++++-----
 python/tvm_ffi/stub/rust_generator/consts.py       |   2 +
 rust/tvm-ffi-macros/src/rs_object.rs               |  34 +++--
 rust/tvm-ffi/src/any.rs                            |  53 ++++++-
 rust/tvm-ffi/src/big_int/int_ops.rs                |   6 +-
 rust/tvm-ffi/src/big_int/mod.rs                    |  15 +-
 rust/tvm-ffi/src/collections/map.rs                |   2 +-
 rust/tvm-ffi/src/collections/tensor.rs             |   7 +-
 rust/tvm-ffi/src/derive.rs                         |   6 +-
 rust/tvm-ffi/src/enum_ref.rs                       |   1 -
 rust/tvm-ffi/src/error.rs                          | 126 ++++++++++-------
 rust/tvm-ffi/src/extra/module.rs                   |  18 +--
 rust/tvm-ffi/src/extra/structural_common.rs        |   3 +-
 rust/tvm-ffi/src/extra/structural_eq_hash.rs       |   2 +-
 rust/tvm-ffi/src/extra/structural_mutate.rs        |  28 ++--
 rust/tvm-ffi/src/extra/structural_visit.rs         |   1 -
 rust/tvm-ffi/src/function.rs                       |  16 ++-
 rust/tvm-ffi/src/function_internal.rs              |  16 +--
 rust/tvm-ffi/src/lib.rs                            |   6 +-
 rust/tvm-ffi/src/macros.rs                         | 110 ++++++++++++---
 rust/tvm-ffi/src/object.rs                         |  47 ++++++-
 rust/tvm-ffi/src/optional.rs                       |   5 +
 rust/tvm-ffi/src/reflection/accessor.rs            | 146 ++++++++++++-------
 rust/tvm-ffi/src/reflection/enum_def.rs            |   4 -
 rust/tvm-ffi/src/reflection/native_function.rs     |  13 +-
 rust/tvm-ffi/src/reflection/registry.rs            |  21 ++-
 rust/tvm-ffi/src/reflection/unsafe_init.rs         |   2 +-
 rust/tvm-ffi/src/rvalue_ref.rs                     |   1 -
 rust/tvm-ffi/src/static_init.rs                    |   7 +-
 rust/tvm-ffi/src/string.rs                         | 156 +++++++++++++++------
 rust/tvm-ffi/tests/test_device.rs                  |   2 +-
 rust/tvm-ffi/tests/test_error.rs                   |  17 +--
 rust/tvm-ffi/tests/test_function_metadata.rs       |   2 +-
 rust/tvm-ffi/tests/test_object.rs                  |  14 +-
 rust/tvm-ffi/tests/test_object_ref_any.rs          |   2 +-
 rust/tvm-ffi/tests/test_object_reflection.rs       |  16 ++-
 rust/tvm-ffi/tests/test_rs_object.rs               |  22 +--
 rust/tvm-ffi/tests/test_static_init.rs             |   2 +-
 rust/tvm-ffi/tests/test_string.rs                  |   5 +-
 rust/tvm-ffi/tests/test_structural_mutate.rs       |  39 +++---
 rust/tvm-ffi/tests/test_structural_visit.rs        |   2 +-
 rust/tvm-ffi/tests/test_structural_visit_base.rs   |   6 +-
 tests/python/test_stubgen_rust.py                  |  69 +++------
 45 files changed, 715 insertions(+), 434 deletions(-)

diff --git a/docs/guides/rust_lang_guide.rst b/docs/guides/rust_lang_guide.rst
index 3fa3a679..8f378f57 100644
--- a/docs/guides/rust_lang_guide.rst
+++ b/docs/guides/rust_lang_guide.rst
@@ -319,7 +319,7 @@ loads, as a C++ type does:
    use tvm_ffi::reflection::ObjectDef;
    use tvm_ffi::{rs_object, Object, ObjectArc, Result};
 
-   #[rs_object("my.Point", s_eq_hash_kind = "TreeNode", reflection = 
PointObj::reflect_methods)]
+   #[rs_object(type_key = "my.Point", s_eq_hash_kind = "TreeNode", reflection 
= PointObj::reflect_methods)]
    pub struct PointObj {
        base_: Object,
        /// The x coordinate.
@@ -378,7 +378,7 @@ names the reference to the parent's object:
    use tvm_ffi::derive::ObjectRef;
    use tvm_ffi::{rs_object, Object, ObjectArc};
 
-   #[rs_object("my.Point", s_eq_hash_kind = "TreeNode")]
+   #[rs_object(type_key = "my.Point", s_eq_hash_kind = "TreeNode")]
    pub struct PointObj {
        base_: Object,
        pub x: i64,
@@ -391,7 +391,7 @@ names the reference to the parent's object:
        data: ObjectArc<PointObj>,
    }
 
-   #[rs_object("my.Point3", final)]
+   #[rs_object(type_key = "my.Point3", final)]
    pub struct Point3Obj {
        base_: PointObj,
        pub z: i64,
@@ -502,11 +502,7 @@ TVM FFI uses standard Rust ``Result`` types:
 
        // Custom errors
        if value < 0 {
-           return Err(Error::new(
-               VALUE_ERROR,
-               "Value must be non-negative",
-               ""
-           ));
+           return Err(Error::new(VALUE_ERROR, "Value must be non-negative"));
        }
 
        Ok(())
@@ -1024,7 +1020,7 @@ An enclosing Pattern region wins, and the prior region is 
restored even on error
 or panic. State remains available after the recursive call.
 
 Typed ``MutationView<'_, T>`` inputs work with the same recursive methods.
-Use ``value.with_inplace_mode(InplaceMode::Disallow)`` to restrict permission.
+Use ``value.narrow_inplace_mode(InplaceMode::Disallow)`` to restrict 
permission.
 Borrow the value in place through ``value.downcast_ref::<Node>()``, as with
 ``AnyView``, or other payloads through ``value.as_any_view()``; finish using 
those
 borrows before consuming the capability. A typed object capability also 
dereferences to its shared node.
@@ -1084,7 +1080,7 @@ while retaining typed scalar replacements:
            Ok((value + 1).into())
        }
        fn mutate_any(&mut self, value: MutationView<'_>, mutator: &mut 
StructuralMutatorView<'_, Self>) -> Result<UnchangedOr<Any>> {
-           mutator.default_mutate(self, 
value.with_inplace_mode(InplaceMode::Disallow))
+           mutator.default_mutate(self, 
value.narrow_inplace_mode(InplaceMode::Disallow))
        }
    }
 
diff --git a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs 
b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
index ee27babc..2183fd4e 100644
--- a/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
+++ b/examples/rust_stubgen/rust/src/generated/rust_stubgen/mod.rs
@@ -64,7 +64,7 @@ impl TryFrom<i64> for PairKind {
     type Error = Error;
     fn try_from(value: i64) -> Result<Self> {
         i32::try_from(value).map(Self).map_err(|_| {
-            Error::new(VALUE_ERROR, &format!("PairKind value {value} does not 
fit i32"), "")
+            Error::new(VALUE_ERROR, &format!("PairKind value {value} does not 
fit i32"))
         })
     }
 }
diff --git a/python/tvm_ffi/stub/rust_generator/codegen.py 
b/python/tvm_ffi/stub/rust_generator/codegen.py
index a7f9acd9..d7128168 100644
--- a/python/tvm_ffi/stub/rust_generator/codegen.py
+++ b/python/tvm_ffi/stub/rust_generator/codegen.py
@@ -89,6 +89,42 @@ if TYPE_CHECKING:
     from .directives import EnumSpec
 
 
+def _read_lines(head: str, macro: str, args: list[str], read: str) -> 
list[str]:
+    """``head + macro!(args) + read`` in an accessor body, wrapped as rustfmt 
does at its depth.
+
+    The accessor sits in an ``impl``, so its lines gain one more indent. 
Arguments
+    wider than rustfmt's ``fn_call_width`` take one line each; otherwise the 
read
+    moves to its own line. A ``let`` keeps a column for its ``;``, and first 
tries
+    its value on the next line.
+    """
+    width = C_RUST.RUST_MAX_WIDTH - 4
+    indent = head[: len(head) - len(head.lstrip())]
+    joined = ", ".join(args)
+    call = f"{macro}!({joined})"
+    if len(joined) > C_RUST.RUST_FN_CALL_WIDTH:
+        *rest, last = args
+        return [
+            f"{head}{macro}!(",
+            *[f"{indent}    {arg}," for arg in rest],
+            f"{indent}    {last}",
+            f"{indent})",
+            f"{indent}{read}",
+        ]
+    let, eq, _ = head.partition(" = ")
+    line = f"{head}{call}{read}"
+    chained = [f"{head}{call}", f"{indent}    {read}"]
+    if not eq:
+        return [line] if len(line) <= width else chained
+    moved = f"{indent}    {call}{read}"
+    if len(line) < width - 1:
+        return [line]
+    if len(moved) < width - 1:
+        return [f"{let} =", moved]
+    if len(f"{head}{call}") < width:
+        return chained
+    return [f"{let} =", f"{indent}    {call}", f"{indent}        {read}"]
+
+
 def _call_lines(open_: str, items: list[str], close: str) -> list[str]:
     """``open_ + items + close`` on one line, or wrapped the way rustfmt does 
when it would overflow.
 
@@ -421,8 +457,8 @@ class _ObjectRenderer:
         target = f"{self.type_key}.{field.name}"
         name = rust_ident(field.name)
         vis = self._visibility(target)
-        static = self._getter_static_lines(field.name)
-        getter = "GETTER.clone()?"
+        cached = self.imports.record("tvm_ffi::cached_field_getter")
+        args = [self.obj_struct, f'"{field.name}"']
         allow = _allow_non_snake_case([name])
 
         enum = directives.enums.get(target)
@@ -430,8 +466,7 @@ class _ObjectRenderer:
             return [
                 *allow,
                 f"{vis} fn {name}(&self) -> Result<{enum.name}> {{",
-                *static,
-                f"    let raw: i64 = {getter}.get(self)?;",
+                *_read_lines("    let raw: i64 = ", cached, args, 
".get(self)?;"),
                 f"    {enum.name}::try_from(raw)",
                 "}",
             ]
@@ -447,8 +482,7 @@ class _ObjectRenderer:
             return [
                 *allow,
                 f"{vis} fn {name}(&self) -> Result<{any_type}> {{",
-                *static,
-                f"    {getter}.get_any(self)",
+                *_read_lines("    ", cached, args, ".get_any(self)"),
                 "}",
             ]
         if target in directives.nullable and not 
rust_type.startswith("Option<"):
@@ -456,35 +490,10 @@ class _ObjectRenderer:
         return [
             *allow,
             f"{vis} fn {name}(&self) -> Result<{rust_type}> {{",
-            *static,
-            f"    {getter}.get(self)",
+            *_read_lines("    ", cached, args, ".get(self)"),
             "}",
         ]
 
-    def _getter_static_lines(self, field_name: str) -> list[str]:
-        """Declare the accessor's ``static GETTER``, which resolves the field 
on first use only.
-
-        A fn-local ``static`` cannot name ``Self``, so the owner is spelled 
out.
-        Wrapped as rustfmt does at the accessor's depth (``impl``, then 
``fn``).
-        """
-        lazy = 
self.imports.record("tvm_ffi::extra::cold_init_lazy_lock::ColdInitLazyLock")
-        static = f"        static GETTER: {lazy}<Result<FieldGetter>> ="
-        owner, key = f"{self.obj_struct}::type_index()", f'"{field_name}"'
-        call = f"FieldGetter::new({owner}, {key})"
-        one_line = f"{static} {lazy}::new(|| {call});"
-        moved = f"            {lazy}::new(|| {call});"
-        if len(one_line) <= C_RUST.RUST_MAX_WIDTH:
-            lines = [one_line]
-        elif len(moved) <= C_RUST.RUST_MAX_WIDTH:
-            lines = [static, moved]
-        else:
-            lines = [
-                f"{static} {lazy}::new(|| {{",
-                *_call_lines("            FieldGetter::new(", [owner, key], 
")"),
-                "        });",
-            ]
-        return [line[4:] for line in lines]
-
     def _enum_lines(self, spec: EnumSpec) -> list[str]:
         """Render the type an ``enum`` directive declares: an open integer 
newtype, or a closed enum."""
         error = self.imports.record("tvm_ffi::Error")
@@ -494,7 +503,7 @@ class _ObjectRenderer:
         if spec.closed:
             message = f'&format!("{spec.name} has no value {{value}}")'
             rejected = _call_lines(
-                f"            _ => Err({error}::new(", [value_error, message, 
'""'], ")),"
+                f"            _ => Err({error}::new(", [value_error, message], 
")),"
             )
             return [
                 *allow,
@@ -537,7 +546,7 @@ class _ObjectRenderer:
             f"    fn try_from(value: i64) -> {result}<Self> {{",
             f"        {spec.repr}::try_from(value).map(Self).map_err(|_| {{",
             f'            {error}::new({value_error}, &format!("{spec.name} 
value {{value}} does not fit '
-            f'{spec.repr}"), "")',
+            f'{spec.repr}"))',
             "        })",
             "    }",
             "}",
@@ -829,8 +838,6 @@ class _ObjectRenderer:
         is_enum = C_RUST.RUST_ENUM_TYPE_KEY in self.info.ancestors
         has_accessors = bool(fields) and not verdict.is_complete
         if has_accessors:
-            self.imports.record("tvm_ffi::ObjectCore")  # `<Obj>::type_index()`
-            self.imports.record("tvm_ffi::reflection::FieldGetter")
             self.imports.record("tvm_ffi::Result")
 
         # The reference comes first, so a `///` written above the block 
documents it.
diff --git a/python/tvm_ffi/stub/rust_generator/consts.py 
b/python/tvm_ffi/stub/rust_generator/consts.py
index 4c341890..2f66c5d1 100644
--- a/python/tvm_ffi/stub/rust_generator/consts.py
+++ b/python/tvm_ffi/stub/rust_generator/consts.py
@@ -183,3 +183,5 @@ RUST_GENERATED_MEMBERS = frozenset({"base_", "data_"})
 
 #: ``rustfmt``'s default ``max_width``; a wider allocator signature wraps one 
parameter per line.
 RUST_MAX_WIDTH = 100
+#: ``rustfmt``'s default ``fn_call_width``; wider call arguments take one line 
each.
+RUST_FN_CALL_WIDTH = 60
diff --git a/rust/tvm-ffi-macros/src/rs_object.rs 
b/rust/tvm-ffi-macros/src/rs_object.rs
index 26f32f19..8b02d4c0 100644
--- a/rust/tvm-ffi-macros/src/rs_object.rs
+++ b/rust/tvm-ffi-macros/src/rs_object.rs
@@ -34,7 +34,7 @@ pub(crate) fn rs_object(args: TokenStream, item: TokenStream) 
-> TokenStream {
         .into()
 }
 
-/// `#[rs_object("type.key", final, ...)]`.
+/// `#[rs_object(type_key = "type.key", final, ...)]`.
 struct RsObjectArgs {
     type_key: LitStr,
     type_final: bool,
@@ -48,14 +48,15 @@ struct RsObjectArgs {
 
 impl Parse for RsObjectArgs {
     fn parse(input: ParseStream) -> syn::Result<Self> {
-        let type_key = input.parse::<LitStr>().map_err(|error| {
-            syn::Error::new(
-                error.span(),
-                "expected the type key first, as in 
`#[rs_object(\"my.Point\")]`",
-            )
-        })?;
+        if input.peek(LitStr) {
+            return Err(syn::Error::new(
+                input.span(),
+                "name the type key, as in `#[rs_object(type_key = 
\"my.Point\")]`",
+            ));
+        }
+        let mut type_key = None;
         let mut args = RsObjectArgs {
-            type_key,
+            type_key: LitStr::new("", input.span()),
             type_final: false,
             immutable: false,
             s_eq_hash_kind: None,
@@ -66,9 +67,11 @@ impl Parse for RsObjectArgs {
         };
         let mut seen = Vec::new();
         while !input.is_empty() {
-            input.parse::<Token![,]>()?;
-            if input.is_empty() {
-                break;
+            if !seen.is_empty() {
+                input.parse::<Token![,]>()?;
+                if input.is_empty() {
+                    break;
+                }
             }
             // `final` and `crate` are keywords.
             let name = input.call(syn::Ident::parse_any)?;
@@ -80,6 +83,7 @@ impl Parse for RsObjectArgs {
                 input.parse::<Token![=]>()?;
             }
             match key.as_str() {
+                "type_key" => type_key = Some(input.parse()?),
                 "final" => args.type_final = true,
                 "immutable" => args.immutable = true,
                 "s_eq_hash_kind" => args.s_eq_hash_kind = Some(input.parse()?),
@@ -90,7 +94,7 @@ impl Parse for RsObjectArgs {
                 _ => {
                     return Err(syn::Error::new(
                         name.span(),
-                        "expected `final`, `immutable`, `s_eq_hash_kind = 
\"<kind>\"`, `child_slots = <n>`, \
+                        "expected `type_key = \"<key>\"`, `final`, 
`immutable`, `s_eq_hash_kind = \"<kind>\"`, `child_slots = <n>`, \
                          `child_slots_can_overflow = <bool>`, `reflection = 
<path>` or \
                          `crate = <path>`",
                     ));
@@ -98,6 +102,12 @@ impl Parse for RsObjectArgs {
             }
             seen.push(key);
         }
+        args.type_key = type_key.ok_or_else(|| {
+            syn::Error::new(
+                input.span(),
+                "missing the type key, as in `#[rs_object(type_key = 
\"my.Point\")]`",
+            )
+        })?;
         if args.type_final
             && (args.child_slots.is_some() || 
args.child_slots_can_overflow.is_some())
         {
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index 54f6498f..f660644f 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -137,6 +137,13 @@ impl<'a> AnyView<'a> {
         self.data.type_index
     }
 
+    /// The registered type key of the viewed value, such as `"int"` or
+    /// `"ffi.Array"`. See also [`Any::type_key`].
+    #[inline]
+    pub fn type_key(&self) -> &'static str {
+        object::registered_type_key(self.data.type_index)
+    }
+
     /// Whether the value is None.
     #[inline(always)]
     pub fn is_none(&self) -> bool {
@@ -189,6 +196,15 @@ impl<'a> AnyView<'a> {
         unsafe { T::downcast_ref_from_any(&self.data) }
     }
 
+    /// Whether the viewed value is a `T` as is, by the strict check of
+    /// [`downcast_ref`](Self::downcast_ref), without conversion.
+    /// See also [`Any::is`].
+    #[inline(always)]
+    pub fn is<T: AnyCompatible>(&self) -> bool {
+        // SAFETY: the view holds a valid cell.
+        unsafe { T::check_any_strict(&self.data) }
+    }
+
     /// The strong reference count of the held object, for tests and debugging;
     /// `None` if the value is not an object.
     #[doc(hidden)]
@@ -289,6 +305,19 @@ impl Any {
         self.data.type_index
     }
 
+    /// The registered type key of the stored value, such as `"int"` or
+    /// `"ffi.Array"`, as C++ `Any::GetTypeKey`.
+    ///
+    /// ```
+    /// use tvm_ffi::{Any, Array};
+    /// assert_eq!(Any::from(1_i64).type_key(), "int");
+    /// assert_eq!(Any::from(Array::from_vec(vec![1_i64])).type_key(), 
"ffi.Array");
+    /// ```
+    #[inline]
+    pub fn type_key(&self) -> &'static str {
+        object::registered_type_key(self.data.type_index)
+    }
+
     /// Whether the value is None.
     #[inline(always)]
     pub fn is_none(&self) -> bool {
@@ -328,6 +357,24 @@ impl Any {
         unsafe { T::downcast_ref_from_any(&self.data) }
     }
 
+    /// Whether the stored value is a `T` as is, by the strict check of
+    /// [`downcast_ref`](Self::downcast_ref), without conversion, as std
+    /// `Any::is`. It covers every [`AnyCompatible`] type, such as `bool`.
+    ///
+    /// ```
+    /// use tvm_ffi::{Any, Array};
+    /// let value = Any::from(Array::from_vec(vec![1_i64, 2]));
+    /// assert!(value.is::<Array<i64>>());
+    /// assert!(!value.is::<Array<f64>>());
+    /// assert!(Any::from(true).is::<bool>());
+    /// assert!(!Any::from(1_i64).is::<f64>());
+    /// ```
+    #[inline(always)]
+    pub fn is<T: AnyCompatible>(&self) -> bool {
+        // SAFETY: the owned cell is valid.
+        unsafe { T::check_any_strict(&self.data) }
+    }
+
     /// Return a pointer to the raw ABI representation, for a runtime call that
     /// writes an owned result into it as an out-parameter.
     ///
@@ -540,7 +587,7 @@ impl<'a, T: AnyCompatible> TryFrom<AnyView<'a>> for 
TryFromTemp<T> {
                             T::get_mismatch_type_info(&value.data),
                             T::type_str()
                         );
-                        crate::error::Error::new(crate::error::TYPE_ERROR, 
&msg, "")
+                        crate::error::Error::new(crate::error::TYPE_ERROR, 
&msg)
                     })
                     .map(TryFromTemp::new)
             }
@@ -574,7 +621,7 @@ impl<T: AnyCompatible> TryFrom<Any> for TryFromTemp<T> {
                             T::get_mismatch_type_info(&value.data),
                             T::type_str()
                         );
-                        crate::error::Error::new(crate::error::TYPE_ERROR, 
&msg, "")
+                        crate::error::Error::new(crate::error::TYPE_ERROR, 
&msg)
                     })
                     .map(TryFromTemp::new)
             }
@@ -629,7 +676,7 @@ impl<T: AnyCompatible> ArgTryFromAnyView for T {
                         T::get_mismatch_type_info(&value.data),
                         T::type_str()
                     );
-                    crate::error::Error::new(crate::error::TYPE_ERROR, &msg, 
"")
+                    crate::error::Error::new(crate::error::TYPE_ERROR, &msg)
                 })
             }
         }
diff --git a/rust/tvm-ffi/src/big_int/int_ops.rs 
b/rust/tvm-ffi/src/big_int/int_ops.rs
index 9ba0f290..4573f328 100644
--- a/rust/tvm-ffi/src/big_int/int_ops.rs
+++ b/rust/tvm-ffi/src/big_int/int_ops.rs
@@ -77,15 +77,15 @@ impl WordsBuf {
 }
 
 pub(super) fn zero_division() -> Error {
-    Error::new(ZERO_DIVISION_ERROR, "Division by zero", "")
+    Error::new(ZERO_DIVISION_ERROR, "Division by zero")
 }
 
 fn negative_shift() -> Error {
-    Error::new(VALUE_ERROR, "Negative BigInt shift count", "")
+    Error::new(VALUE_ERROR, "Negative BigInt shift count")
 }
 
 fn overflow(message: &str) -> Error {
-    Error::new(OVERFLOW_ERROR, message, "")
+    Error::new(OVERFLOW_ERROR, message)
 }
 
 /// Whether a nonempty signed-word view represents a negative integer.
diff --git a/rust/tvm-ffi/src/big_int/mod.rs b/rust/tvm-ffi/src/big_int/mod.rs
index bd17e6f8..65d83695 100644
--- a/rust/tvm-ffi/src/big_int/mod.rs
+++ b/rust/tvm-ffi/src/big_int/mod.rs
@@ -387,11 +387,7 @@ macro_rules! impl_try_from_big_int {
 
             fn try_from(value: &BigInt) -> Result<Self> {
                 value.$to().ok_or_else(|| {
-                    Error::new(
-                        OVERFLOW_ERROR,
-                        &format!("{} does not fit in {}", value, 
stringify!($t)),
-                        "",
-                    )
+                    Error::new(OVERFLOW_ERROR, &format!("{} does not fit in 
{}", value, stringify!($t)))
                 })
             }
         }
@@ -413,13 +409,12 @@ impl TryFrom<f64> for BigInt {
             return Ok(Self::from_i64(value as i64));
         }
         if value.is_nan() {
-            return Err(Error::new(VALUE_ERROR, "Cannot convert NaN to BigInt", 
""));
+            return Err(Error::new(VALUE_ERROR, "Cannot convert NaN to 
BigInt"));
         }
         if value.is_infinite() {
             return Err(Error::new(
                 OVERFLOW_ERROR,
                 "Cannot convert infinity to BigInt",
-                "",
             ));
         }
         // |value| >= 2^63 is an integer: shift the 53-bit significand to its 
binary exponent.
@@ -445,11 +440,7 @@ impl FromStr for BigInt {
             _ => (false, text),
         };
         if digits.is_empty() || !digits.bytes().all(|byte| 
byte.is_ascii_digit()) {
-            return Err(Error::new(
-                VALUE_ERROR,
-                "Invalid decimal string for BigInt",
-                "",
-            ));
+            return Err(Error::new(VALUE_ERROR, "Invalid decimal string for 
BigInt"));
         }
         // Accumulate 19-digit chunks: magnitude = magnitude * 10^len + chunk.
         let mut magnitude = Vec::new();
diff --git a/rust/tvm-ffi/src/collections/map.rs 
b/rust/tvm-ffi/src/collections/map.rs
index 04232e1c..adac8de6 100644
--- a/rust/tvm-ffi/src/collections/map.rs
+++ b/rust/tvm-ffi/src/collections/map.rs
@@ -78,7 +78,7 @@ fn element_from_any<T: ContainerElement>(value: Any) -> 
Result<T> {
                 T::container_get_mismatch_type_info(value.as_raw_ffi_any()),
                 T::container_type_str()
             );
-            Error::new(crate::error::TYPE_ERROR, &message, "")
+            Error::new(crate::error::TYPE_ERROR, &message)
         })
     }
 }
diff --git a/rust/tvm-ffi/src/collections/tensor.rs 
b/rust/tvm-ffi/src/collections/tensor.rs
index 24ec0200..f9bfe62b 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -41,7 +41,10 @@ use tvm_ffi_sys::{TVMFFIEnvTensorAlloc, TVMFFIObjectHandle};
 /// (sub-byte types are packed), and `free_data` must release
 /// exactly what `alloc_data` returned. `free_data` may be called for a tensor
 /// whose `data` is null, when `alloc_data` panicked, and must then do nothing.
-pub unsafe trait NDAllocator: 'static {
+///
+/// `Send + Sync`: the tensor frees its data through the allocator when the
+/// last reference drops, which may be on any thread.
+pub unsafe trait NDAllocator: Send + Sync + 'static {
     /// The minimum alignment of the allocated data, in bytes.
     const MIN_ALIGN: usize;
     /// Allocate the data of a tensor.
@@ -646,7 +649,7 @@ impl<'a> TensorView<'a> {
             "{context}Cannot convert from type `{}` to `DLTensor*`",
             crate::object::type_key_of_index(value.type_index)
         );
-        crate::error::Error::new(crate::error::TYPE_ERROR, &message, "")
+        crate::error::Error::new(crate::error::TYPE_ERROR, &message)
     }
 }
 
diff --git a/rust/tvm-ffi/src/derive.rs b/rust/tvm-ffi/src/derive.rs
index 868514be..fb2ec075 100644
--- a/rust/tvm-ffi/src/derive.rs
+++ b/rust/tvm-ffi/src/derive.rs
@@ -112,7 +112,7 @@ pub use tvm_ffi_macros::Object;
 /// use tvm_ffi::derive::ObjectRef;
 /// use tvm_ffi::{rs_object, Object, ObjectArc};
 ///
-/// #[rs_object("my_ext.MyPair")]
+/// #[rs_object(type_key = "my_ext.MyPair")]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
@@ -124,7 +124,7 @@ pub use tvm_ffi_macros::Object;
 ///     data: ObjectArc<MyPairObj>,
 /// }
 ///
-/// #[rs_object("my_ext.MyTriple", final)]
+/// #[rs_object(type_key = "my_ext.MyTriple", final)]
 /// pub struct MyTripleObj {
 ///     base_: MyPairObj,
 ///     pub third: i64,
@@ -155,7 +155,7 @@ pub use tvm_ffi_macros::Object;
 /// use tvm_ffi::derive::ObjectRef;
 /// use tvm_ffi::{rs_object, Array, Object, ObjectArc};
 ///
-/// #[rs_object("my_ext.MyPair")]
+/// #[rs_object(type_key = "my_ext.MyPair")]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
diff --git a/rust/tvm-ffi/src/enum_ref.rs b/rust/tvm-ffi/src/enum_ref.rs
index 8f751493..29f5c6bf 100644
--- a/rust/tvm-ffi/src/enum_ref.rs
+++ b/rust/tvm-ffi/src/enum_ref.rs
@@ -122,7 +122,6 @@ pub trait EnumRef: ObjectRefCore {
                     "Enum `{}` has no value named `{name}`",
                     Self::ContainerType::TYPE_KEY
                 ),
-                "",
             ));
         };
         any_into(value)
diff --git a/rust/tvm-ffi/src/error.rs b/rust/tvm-ffi/src/error.rs
index 57cae23e..af996786 100644
--- a/rust/tvm-ffi/src/error.rs
+++ b/rust/tvm-ffi/src/error.rs
@@ -22,7 +22,9 @@ use crate::derive::{Object, ObjectRef};
 use crate::object::{self, Object, ObjectArc, ObjectRefCore};
 use crate::type_traits::AnyCompatible;
 use std::ffi::c_void;
-use tvm_ffi_sys::TVMFFIBacktraceUpdateMode::kTVMFFIBacktraceUpdateModeAppend;
+use tvm_ffi_sys::TVMFFIBacktraceUpdateMode::{
+    kTVMFFIBacktraceUpdateModeAppend, kTVMFFIBacktraceUpdateModeReplace,
+};
 use tvm_ffi_sys::{
     TVMFFIAny, TVMFFIByteArray, TVMFFIErrorCell, 
TVMFFIErrorCreateWithCauseAndExtraContext,
     TVMFFIErrorMoveFromRaised, TVMFFIErrorSetRaised, TVMFFIObjectHandle, 
TVMFFITypeIndex,
@@ -82,7 +84,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 ///
 /// Use the constants such as [`VALUE_ERROR`] and [`TYPE_ERROR`], or
 /// [`ErrorKind::new("MyError")`](ErrorKind::new) for a custom kind.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub struct ErrorKind<'a>(&'a str);
 
 impl<'a> ErrorKind<'a> {
@@ -91,7 +93,7 @@ impl<'a> ErrorKind<'a> {
     /// ```
     /// use tvm_ffi::{Error, ErrorKind};
     /// const MY_ERROR: ErrorKind = ErrorKind::new("MyError");
-    /// let error = Error::new(MY_ERROR, "bad input", "");
+    /// let error = Error::new(MY_ERROR, "bad input");
     /// assert_eq!(error.kind().as_str(), "MyError");
     /// ```
     pub const fn new(name: &'a str) -> Self {
@@ -155,7 +157,7 @@ impl Error {
                     .map(|s| s.as_str())
             })
             .unwrap_or("unknown payload");
-        Self::new(INTERNAL_ERROR, &format!("panicked: {message}"), "")
+        Self::new(INTERNAL_ERROR, &format!("panicked: {message}"))
     }
 
     /// The `TypeError` for a reference that 
[`downcast`](crate::ObjectRefCore::downcast)
@@ -176,55 +178,57 @@ impl Error {
             B::get_mismatch_type_info(&cell),
             B::type_str()
         );
-        Self::new(TYPE_ERROR, &message, "")
+        Self::new(TYPE_ERROR, &message)
     }
 
-    /// Create an error.
+    /// Create an error with an empty backtrace.
     ///
-    /// Prefer [`bail!`](crate::bail), which also records the source location.
+    /// Prefer [`format_err!`](crate::format_err) or [`bail!`](crate::bail),
+    /// which also record the source location. Add the other parts with
+    /// [`with_backtrace`](Self::with_backtrace),
+    /// [`with_cause`](Self::with_cause) and
+    /// [`with_extra_context`](Self::with_extra_context).
     ///
     /// # Arguments
     ///
     /// * `kind` - The kind of the error, such as [`VALUE_ERROR`].
     /// * `message` - The message of the error.
-    /// * `backtrace` - The backtrace of the error, most recent call first; 
may be empty.
     ///
     /// # Returns
     ///
     /// The error.
-    pub fn new(kind: ErrorKind<'_>, message: &str, backtrace: &str) -> Self {
-        Self::create(kind, message, backtrace, None, std::ptr::null_mut())
+    pub fn new(kind: ErrorKind<'_>, message: &str) -> Self {
+        Self::create(kind, message, "", None, std::ptr::null_mut())
     }
 
-    /// Create an error retaining its cause and any application-specific 
context.
-    ///
-    /// # Arguments
-    ///
-    /// * `kind` - The kind of the error.
-    /// * `message` - The message of the error.
-    /// * `backtrace` - The backtrace of the error, most recent call first; 
may be empty.
-    /// * `cause_chain` - The error that caused this one, if any.
-    /// * `extra_context` - An object of application-specific context, if any;
-    ///   `Send + Sync`, since the error is.
-    ///
-    /// # Returns
+    /// Set `cause` as the error that caused this one, the
+    /// [`cause_chain`](Self::cause_chain). The error is updated in place when
+    /// it is not shared, and copied otherwise.
     ///
-    /// The error.
-    pub fn new_with_cause_and_extra_context<T: ObjectRefCore + Send + Sync>(
-        kind: ErrorKind<'_>,
-        message: &str,
-        backtrace: &str,
-        cause_chain: Option<&Error>,
-        extra_context: Option<&T>,
-    ) -> Self {
-        let extra_context = extra_context.map_or(std::ptr::null_mut(), 
|context| {
-            ObjectArc::as_ptr(T::data(context)) as TVMFFIObjectHandle
-        });
-        Self::create(kind, message, backtrace, cause_chain, extra_context)
+    /// ```
+    /// use tvm_ffi::{Error, RUNTIME_ERROR, VALUE_ERROR};
+    /// let cause = Error::new(VALUE_ERROR, "bad input");
+    /// let error = Error::new(RUNTIME_ERROR, "load 
failed").with_cause(&cause);
+    /// assert_eq!(error.cause_chain().unwrap().message(), "bad input");
+    /// ```
+    #[inline]
+    pub fn with_cause(mut self, cause: &Error) -> Self {
+        *self.cause_chain_mut() = Some(cause.clone());
+        self
     }
 
-    /// As [`Self::new_with_cause_and_extra_context`], with `extra_context` a
-    /// borrowed object handle or null.
+    /// Set `context` as the [`extra_context`](Self::extra_context), an object
+    /// of application-specific context; `Send + Sync`, since the error is.
+    /// The error is updated in place when it is not shared, and copied
+    /// otherwise.
+    #[inline]
+    pub fn with_extra_context<T: ObjectRefCore + Send + Sync>(mut self, 
context: &T) -> Self {
+        *self.extra_context_mut() = 
Some(object::into_root_object_ref(context.clone()));
+        self
+    }
+
+    /// Create an error from its parts, with `extra_context` a borrowed object
+    /// handle or null.
     fn create(
         kind: ErrorKind<'_>,
         message: &str,
@@ -352,6 +356,14 @@ impl Error {
         Some(unsafe { &*(handle as *const _ as *const object::ObjectRef) })
     }
 
+    /// The `cause_chain` slot, to update in place.
+    fn cause_chain_mut(&mut self) -> &mut Option<Error> {
+        let error = self.make_mut();
+        // SAFETY: this handle is the only reference, and the slot owns a 
nullable
+        // error handle, which is exactly an `Option<Error>`.
+        unsafe { &mut 
*std::ptr::addr_of_mut!((*error).cell.cause_chain).cast::<Option<Error>>() }
+    }
+
     /// The `extra_context` slot, to update in place.
     pub(crate) fn extra_context_mut(&mut self) -> &mut 
Option<object::ObjectRef> {
         let error = self.make_mut();
@@ -371,15 +383,31 @@ impl Error {
     /// The traceback of the error.
     pub fn traceback_most_recent_call_last(&self) -> String {
         let backtrace = self.backtrace();
-        let backtrace_lines = backtrace.split('\n');
         let mut traceback = String::new();
-        for line in backtrace_lines.rev() {
+        for line in backtrace.split('\n').rev().filter(|line| 
!line.is_empty()) {
             traceback.push_str(line);
             traceback.push('\n');
         }
         traceback
     }
 
+    /// Replace the backtrace, most recent call first. The error is updated in
+    /// place when it is not shared, and copied otherwise.
+    #[inline]
+    pub fn with_backtrace(mut self, backtrace: &str) -> Self {
+        let error = self.make_mut();
+        // SAFETY: `make_mut` made this handle the only reference to `error`.
+        unsafe {
+            let backtrace_data = TVMFFIByteArray::from_str(backtrace);
+            ((*error).cell.update_backtrace)(
+                error.cast::<c_void>(),
+                &backtrace_data,
+                kTVMFFIBacktraceUpdateModeReplace as i32,
+            );
+        }
+        self
+    }
+
     /// Append the backtrace to the error.
     ///
     /// [`attach_context!`](crate::attach_context) appends the current source
@@ -423,27 +451,29 @@ impl Error {
     }
 }
 
+/// Formats the kind and the message, such as `ValueError: bad x`, as C++
+/// `what()`; [`Debug`](std::fmt::Debug) adds the traceback.
 impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}: {}", self.kind().as_str(), self.message())
+    }
+}
+
+/// Formats the traceback, most recent call last, then the kind and the
+/// message, as Python prints an exception; `unwrap` and `expect` print it.
+impl std::fmt::Debug for Error {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         if self.backtrace().is_empty() {
-            return write!(f, "{}: {}", self.kind().as_str(), self.message());
+            return std::fmt::Display::fmt(self, f);
         }
         write!(
             f,
-            "Traceback (most recent call last):\n{}{}: {}",
+            "Traceback (most recent call last):\n{}{self}",
             self.traceback_most_recent_call_last(),
-            self.kind().as_str(),
-            self.message()
         )
     }
 }
 
-impl std::fmt::Debug for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        std::fmt::Display::fmt(self, f)
-    }
-}
-
 impl std::error::Error for Error {
     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         self.cause_chain().map(|cause| cause as _)
diff --git a/rust/tvm-ffi/src/extra/module.rs b/rust/tvm-ffi/src/extra/module.rs
index adee30d0..acb681bb 100644
--- a/rust/tvm-ffi/src/extra/module.rs
+++ b/rust/tvm-ffi/src/extra/module.rs
@@ -58,7 +58,7 @@ impl Module {
     /// # Errors
     /// If the library cannot be loaded.
     pub fn load_from_file<Str: AsRef<str>>(file_name: Str) -> Result<Module> {
-        let file_name = crate::string::String::from(file_name);
+        let file_name = crate::string::String::from(file_name.as_ref());
         crate::cached_global_func!("ffi.ModuleLoadFromFile")
             .call_tuple((file_name,))?
             .try_into()
@@ -79,7 +79,7 @@ impl Module {
     /// Returns an error if the module has no function `name`, as Python's
     /// `Module.get_function` raises.
     pub fn get_function<Str: AsRef<str>>(&self, name: Str) -> Result<Function> 
{
-        let name = crate::string::String::from(name);
+        let name = crate::string::String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleGetFunction")
             .call_tuple((self, name, false))?
             .try_into()
@@ -88,7 +88,7 @@ impl Module {
     /// As [`get_function`](Self::get_function), but also searching the modules
     /// this one imports, depth first in import order.
     pub fn get_function_with_imports<Str: AsRef<str>>(&self, name: Str) -> 
Result<Function> {
-        let name = crate::string::String::from(name);
+        let name = crate::string::String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleGetFunction")
             .call_tuple((self, name, true))?
             .try_into()
@@ -104,7 +104,7 @@ impl Module {
     /// # Returns
     /// Whether the function exists.
     pub fn implements_function<Str: AsRef<str>>(&self, name: Str) -> 
Result<bool> {
-        let name = String::from(name);
+        let name = String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleImplementsFunction")
             .call_tuple((self, name, false))?
             .try_into()
@@ -113,7 +113,7 @@ impl Module {
     /// As [`implements_function`](Self::implements_function), but also 
searching the modules
     /// this one imports, depth first in import order.
     pub fn implements_function_with_imports<Str: AsRef<str>>(&self, name: Str) 
-> Result<bool> {
-        let name = String::from(name);
+        let name = String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleImplementsFunction")
             .call_tuple((self, name, true))?
             .try_into()
@@ -133,7 +133,7 @@ impl Module {
     /// # Returns
     /// The metadata as a JSON string, or `None` if the function has none.
     pub fn get_function_metadata<Str: AsRef<str>>(&self, name: Str) -> 
Result<Option<String>> {
-        let name = String::from(name);
+        let name = String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleGetFunctionMetadata")
             .call_tuple((self, name, false))?
             .try_into()
@@ -145,7 +145,7 @@ impl Module {
         &self,
         name: Str,
     ) -> Result<Option<String>> {
-        let name = String::from(name);
+        let name = String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleGetFunctionMetadata")
             .call_tuple((self, name, true))?
             .try_into()
@@ -164,7 +164,7 @@ impl Module {
     /// # Returns
     /// The doc string, or `None` if the function has none.
     pub fn get_function_doc<Str: AsRef<str>>(&self, name: Str) -> 
Result<Option<String>> {
-        let name = String::from(name);
+        let name = String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleGetFunctionDoc")
             .call_tuple((self, name, false))?
             .try_into()
@@ -176,7 +176,7 @@ impl Module {
         &self,
         name: Str,
     ) -> Result<Option<String>> {
-        let name = String::from(name);
+        let name = String::from(name.as_ref());
         crate::cached_global_func!("ffi.ModuleGetFunctionDoc")
             .call_tuple((self, name, true))?
             .try_into()
diff --git a/rust/tvm-ffi/src/extra/structural_common.rs 
b/rust/tvm-ffi/src/extra/structural_common.rs
index 7bbeba04..94b8cd86 100644
--- a/rust/tvm-ffi/src/extra/structural_common.rs
+++ b/rust/tvm-ffi/src/extra/structural_common.rs
@@ -39,7 +39,7 @@ use std::ops::ControlFlow;
 #[cold]
 #[inline(never)]
 pub(super) fn runtime_error(message: &str) -> Error {
-    Error::new(RUNTIME_ERROR, message, "")
+    Error::new(RUNTIME_ERROR, message)
 }
 
 #[cold]
@@ -131,7 +131,6 @@ pub(crate) fn reject_error_value(value: Any) -> Result<Any> 
{
     Err(Error::new(
         TYPE_ERROR,
         "a structural handler returned an Error as a value; return it as Err 
to fail",
-        "",
     ))
 }
 
diff --git a/rust/tvm-ffi/src/extra/structural_eq_hash.rs 
b/rust/tvm-ffi/src/extra/structural_eq_hash.rs
index a60d1f8c..8a028d3e 100644
--- a/rust/tvm-ffi/src/extra/structural_eq_hash.rs
+++ b/rust/tvm-ffi/src/extra/structural_eq_hash.rs
@@ -60,7 +60,7 @@ pub struct StructuralOptions {
 /// use tvm_ffi::derive::ObjectRef;
 /// use tvm_ffi::{rs_object, structural_equal, Array, Object, ObjectArc};
 ///
-/// #[rs_object("my_ext.MyPair", final, s_eq_hash_kind = "TreeNode")]
+/// #[rs_object(type_key = "my_ext.MyPair", final, s_eq_hash_kind = 
"TreeNode")]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs 
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index dc80da21..09b696d6 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -395,7 +395,7 @@ impl<'a, T> MutationView<'a, T> {
     ///
     /// The view with the restricted mode.
     #[inline(always)]
-    pub fn with_inplace_mode(mut self, requested: InplaceMode) -> Self {
+    pub fn narrow_inplace_mode(mut self, requested: InplaceMode) -> Self {
         if requested == InplaceMode::Disallow {
             self.mode = InplaceMode::Disallow;
         }
@@ -436,6 +436,12 @@ impl<'a, T> MutationView<'a, T> {
         unsafe { U::downcast_ref_from_any(&self.value) }
     }
 
+    /// Whether the value is a `U`, as [`Any::is`].
+    #[inline(always)]
+    pub fn is<U: crate::type_traits::AnyCompatible>(&self) -> bool {
+        self.as_any_view().is::<U>()
+    }
+
     /// Narrow the matched type without acquiring an owning reference.
     ///
     /// The check is the strict one of `match_any!`: only a value that fits 
`U` as
@@ -527,7 +533,7 @@ impl<T: object::ObjectRefCore> MutationView<'_, T> {
     /// ```
     /// # use tvm_ffi::*;
     /// # use tvm_ffi::StructuralMutatorView;
-    /// # #[rs_object("my_ext.MyPair", final)]
+    /// # #[rs_object(type_key = "my_ext.MyPair", final)]
     /// # pub struct MyPairObj { base_: Object, pub first: Array<i64>, pub 
second: Array<i64> }
     /// # #[derive(Clone, tvm_ffi::derive::ObjectRef)]
     /// # pub struct MyPair { data: ObjectArc<MyPairObj> }
@@ -1726,24 +1732,16 @@ static SHALLOW_COPY_COLUMN: 
ColdInitLazyLock<TypeAttrColumn> = ColdInitLazyLock:
 #[cold]
 #[inline(never)]
 fn missing_shallow_copy_error(type_index: i32, attribute: &str) -> Error {
-    Error::new(
-        TYPE_ERROR,
-        &format!(
+    Error::new(TYPE_ERROR, &format!(
             "type `{}` cannot use reflected structural mutation because it 
does not define `{attribute}`",
             crate::object::type_key_of_index(type_index)
-        ),
-        "",
-    )
+        ))
 }
 
 #[cold]
 #[inline(never)]
 fn invalid_shallow_copy_hook_error(attribute: &str) -> Error {
-    Error::new(
-        TYPE_ERROR,
-        &format!("{attribute} must be an ffi.Function"),
-        "",
-    )
+    Error::new(TYPE_ERROR, &format!("{attribute} must be an ffi.Function"))
 }
 
 #[cold]
@@ -1752,7 +1750,6 @@ fn invalid_shallow_copy_result_error() -> Error {
     Error::new(
         TYPE_ERROR,
         "shallow copy callback must return a distinct object with the same 
type as its input",
-        "",
     )
 }
 
@@ -1762,7 +1759,6 @@ fn missing_field_setter_error(name: &str) -> Error {
     Error::new(
         TYPE_ERROR,
         &format!("cannot structurally mutate field `{name}` because it does 
not define a setter"),
-        "",
     )
 }
 
@@ -1780,7 +1776,6 @@ fn invalid_mutation_hook_error() -> Error {
     Error::new(
         TYPE_ERROR,
         "__s_mutate__ must be an opaque function pointer or ffi.Function",
-        "",
     )
 }
 
@@ -1790,7 +1785,6 @@ fn var_remap_key_error() -> Error {
     Error::new(
         TYPE_ERROR,
         "variable-remap keys must be object-backed values",
-        "",
     )
 }
 
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs 
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 349ee77c..767b35c3 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -1057,7 +1057,6 @@ fn invalid_visit_hook_result_error() -> Error {
     Error::new(
         TYPE_ERROR,
         "structural visit hook must return None or ffi.VisitInterrupt",
-        "",
     )
 }
 
diff --git a/rust/tvm-ffi/src/function.rs b/rust/tvm-ffi/src/function.rs
index 064a42a2..47eeb329 100644
--- a/rust/tvm-ffi/src/function.rs
+++ b/rust/tvm-ffi/src/function.rs
@@ -19,12 +19,11 @@
 //! [`Function`], a callable shared with C++ and Python, and the global 
function
 //! registry.
 
-pub use crate::function_internal::{PackedCallArg, PackedCallArgs};
+pub use crate::function_internal::{AsPackedCallable, PackedCallArg, 
PackedCallArgs};
 
 use crate::any::{Any, AnyView};
 use crate::derive::{Object, ObjectRef};
 use crate::error::{Error, Result};
-use crate::function_internal::AsPackedCallable;
 use crate::object::{Object, ObjectArc, ObjectCore};
 use crate::type_traits::AnyCompatible;
 use tvm_ffi_sys::{
@@ -122,7 +121,7 @@ impl<F: Fn(&[AnyView]) -> Result<Any> + Send + Sync + 
'static> CallbackFunctionO
             (this.callback)(packed_args)
         })) {
             Ok(ret_value) => ret_value,
-            Err(payload) => 
Err(crate::function_internal::panic_to_error(payload)),
+            Err(payload) => Err(Error::from_panic(payload)),
         };
         match ret_value {
             Ok(value) => {
@@ -227,8 +226,9 @@ impl Function {
     ///
     /// # Errors
     ///
-    /// Returns an error if no function is registered as `name`. Probe with
-    /// `.ok()` where a missing function is expected.
+    /// Returns a `ValueError` if no function is registered as `name`, as C++
+    /// `Function::GetGlobalRequired`. Probe with `.ok()` where a missing
+    /// function is expected.
     ///
     /// ```
     /// use tvm_ffi::Function;
@@ -246,7 +246,7 @@ impl Function {
             let mut result: TVMFFIObjectHandle = ::std::ptr::null_mut();
             crate::check_safe_call!(TVMFFIFunctionGetGlobal(&name_arg, &mut 
result))?;
             if result.is_null() {
-                crate::bail!(crate::error::RUNTIME_ERROR, "Function {} not 
found", name);
+                crate::bail!(crate::error::VALUE_ERROR, "Function {} not 
found", name);
             }
             Ok(Self {
                 data: ObjectArc::<FunctionObj>::from_raw(result as *mut 
FunctionObj),
@@ -385,7 +385,9 @@ impl Function {
     ///
     /// # Arguments
     ///
-    /// * `func` - The typed function, of signature `Fn(T0, T1, ...) -> 
Result<O>`.
+    /// * `func` - The typed function, an [`AsPackedCallable`] such as
+    ///   `Fn(T0, T1, ...) -> Result<O>`; each parameter is an `AnyCompatible`
+    ///   type, a `TensorView` or an `RValueRef<T>`.
     ///
     /// # Returns
     ///
diff --git a/rust/tvm-ffi/src/function_internal.rs 
b/rust/tvm-ffi/src/function_internal.rs
index 885a0a21..05ed2eb6 100644
--- a/rust/tvm-ffi/src/function_internal.rs
+++ b/rust/tvm-ffi/src/function_internal.rs
@@ -28,13 +28,6 @@ use tvm_ffi_sys::{TVMFFIAny, TVMFFIByteArray, 
TVMFFIStringFromByteArray};
 //------------------------------------------------------------------------
 // PackedCallable
 //------------------------------------------------------------------------
-/// The error to raise for a panic caught at an FFI boundary, where unwinding
-/// into the caller would abort the process.
-#[doc(hidden)]
-pub fn panic_to_error(payload: Box<dyn std::any::Any + Send>) -> Error {
-    Error::from_panic(payload)
-}
-
 /// The arguments of a packed call from C.
 ///
 /// # Safety
@@ -52,8 +45,15 @@ pub unsafe fn packed_args<'a>(args: *const TVMFFIAny, 
num_args: i32) -> &'a [Any
     std::slice::from_raw_parts(args as *const AnyView, num_args as usize)
 }
 
+/// A closure `Fn(T0, ..., Tn) -> Result<O>` with 0 to 8 parameters, which
+/// [`Function::from_typed`](crate::Function::from_typed) and
+/// [`Function::register_global_typed`](crate::Function::register_global_typed)
+/// wrap. Each parameter is any [`AnyCompatible`] type, a
+/// [`TensorView`](crate::TensorView), valid during the call, or an
+/// [`RValueRef<T>`](crate::rvalue_ref::RValueRef); `O` converts into
+/// [`Any`].
 pub trait AsPackedCallable<I, O> {
-    // Call the function in packed convention
+    /// Call the function with packed arguments.
     fn call_packed(&self, packed_args: &[AnyView]) -> Result<Any>;
 
     /// The type schema of the function, a JSON description of its signature.
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 41229d66..79de4f47 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -243,7 +243,7 @@ pub use tvm_ffi_macros::match_any;
 
 /// Define an object type in Rust that every language can use.
 ///
-/// `#[rs_object("type.Key", options...)]` registers the struct as an object
+/// `#[rs_object(type_key = "type.Key", options...)]` registers the struct as 
an object
 /// type with the reflection of every field, so that structural equality and
 /// hashing, `repr`, deep copy and Python see the fields. The first field,
 /// `base_`, is the parent type's object, embedded at the start of the struct:
@@ -265,7 +265,7 @@ pub use tvm_ffi_macros::match_any;
 ///
 /// # Arguments
 ///
-/// * `"type.Key"` - The type key.
+/// * `type_key = "type.Key"` - The type key, required.
 /// * `final` - The type has no subtypes.
 /// * `immutable` - Shared objects of the type are never mutated, so its
 ///   references cross threads when its fields can
@@ -322,7 +322,7 @@ pub use tvm_ffi_macros::match_any;
 /// use tvm_ffi::derive::ObjectRef;
 /// use tvm_ffi::{rs_object, Object, ObjectArc};
 ///
-/// #[rs_object("my_ext.MyPair", final)]
+/// #[rs_object(type_key = "my_ext.MyPair", final)]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
diff --git a/rust/tvm-ffi/src/macros.rs b/rust/tvm-ffi/src/macros.rs
index 31ff2d91..f10cf237 100644
--- a/rust/tvm-ffi/src/macros.rs
+++ b/rust/tvm-ffi/src/macros.rs
@@ -76,6 +76,51 @@ macro_rules! cached_global_func {
     }};
 }
 
+/// Resolve a reflected field of the object type `N` once, and keep its getter
+/// for the process: a `&'static FieldGetter<N>`.
+///
+/// Each call site has its own cache, which later calls read without a lock.
+/// `N` must be a concrete type, not `Self` or a generic parameter. It panics,
+/// naming the field, if `N` has no such field. See also
+/// [`FieldGetter::of`](crate::reflection::FieldGetter::of), which returns an
+/// error instead.
+///
+/// # Examples
+///
+/// ```
+/// use tvm_ffi::{cached_field_getter, rs_object, Object, ObjectArc};
+///
+/// #[rs_object(type_key = "my_ext.Point")]
+/// pub struct PointObj {
+///     base_: Object,
+///     x: i64,
+/// }
+///
+/// let point = ObjectArc::new(PointObj { base_: Object::new(), x: 3 });
+/// let x: i64 = cached_field_getter!(PointObj, "x").get(&*point)?;
+/// assert_eq!(x, 3);
+/// # Ok::<(), tvm_ffi::Error>(())
+/// ```
+#[macro_export]
+macro_rules! cached_field_getter {
+    ($owner:ty, $name:literal) => {{
+        static GETTER: $crate::extra::cold_init_lazy_lock::ColdInitLazyLock<
+            $crate::reflection::FieldGetter<$owner>,
+        > = $crate::extra::cold_init_lazy_lock::ColdInitLazyLock::new(|| {
+            
$crate::reflection::FieldGetter::of::<$owner>($name).unwrap_or_else(|_| {
+                ::core::panic!(::core::concat!(
+                    "`",
+                    ::core::stringify!($owner),
+                    "` has no reflected field `",
+                    $name,
+                    "`"
+                ))
+            })
+        });
+        &*GETTER
+    }};
+}
+
 /// Turn the return code of a safe call into a `Result`, taking the raised
 /// error when it is nonzero: `check_safe_call!(function(args))?`.
 #[doc(hidden)]
@@ -92,6 +137,33 @@ macro_rules! check_safe_call {
     }};
 }
 
+/// Make an [`Error`](crate::Error) of a kind with a formatted message,
+/// recording the source location in the backtrace, as [`bail!`](crate::bail)
+/// does without returning.
+///
+/// ```
+/// use tvm_ffi::{format_err, Result, KEY_ERROR};
+///
+/// fn lookup(names: &[&str], name: &str) -> Result<usize> {
+///     names.iter().position(|n| *n == name).ok_or_else(|| 
format_err!(KEY_ERROR, "no {name}"))
+/// }
+///
+/// assert_eq!(lookup(&["a"], "b").unwrap_err().to_string(), "KeyError: no b");
+/// ```
+#[macro_export]
+macro_rules! format_err {
+    ($error_kind:expr, $fmt:expr $(, $args:expr)* $(,)?) => {{
+        let context = ::std::format!(
+            "  File \"{}\", line {}, in {}\n",
+            ::core::file!(),
+            ::core::line!(),
+            $crate::function_name!()
+        );
+        $crate::error::Error::new($error_kind, &::std::format!($fmt $(, 
$args)*))
+            .with_backtrace(&context)
+    }};
+}
+
 /// Return an [`Error`](crate::Error) of a kind with a formatted message from
 /// the current function.
 ///
@@ -127,13 +199,7 @@ macro_rules! check_safe_call {
 #[macro_export]
 macro_rules! bail {
     ($error_kind:expr, $fmt:expr $(, $args:expr)* $(,)?) => {{
-        let context = ::std::format!(
-            "  File \"{}\", line {}, in {}\n",
-            ::core::file!(),
-            ::core::line!(),
-            $crate::function_name!()
-        );
-        return 
::core::result::Result::Err($crate::error::Error::new($error_kind, 
&::std::format!($fmt $(, $args)*), &context));
+        return ::core::result::Result::Err($crate::format_err!($error_kind, 
$fmt $(, $args)*));
     }};
 }
 
@@ -396,7 +462,7 @@ macro_rules! impl_object_ref_upcast {
 /// use tvm_ffi::{Object, ObjectArc, ObjectRefCore};
 /// use tvm_ffi::object::ObjectRefAnyCompatible;
 ///
-/// #[rs_object("my_ext.MyPair", final)]
+/// #[rs_object(type_key = "my_ext.MyPair", final)]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
@@ -714,7 +780,7 @@ macro_rules! tvm_ffi_dll_export_typed_func {
                     $crate::function_internal::call_packed_callable($func, 
packed_args)
                 })) {
                     ::core::result::Result::Ok(ret_value) => ret_value,
-                    ::core::result::Result::Err(payload) => 
::core::result::Result::Err($crate::function_internal::panic_to_error(payload)),
+                    ::core::result::Result::Err(payload) => 
::core::result::Result::Err($crate::error::Error::from_panic(payload)),
                 };
                 match ret_value {
                     ::core::result::Result::Ok(value) => {
@@ -785,7 +851,7 @@ macro_rules! __tvm_ffi_dll_export_metadata {
                     $crate::function_internal::exported_metadata(&$func)
                 })) {
                     ::core::result::Result::Ok(metadata) => metadata,
-                    ::core::result::Result::Err(payload) => 
::core::result::Result::Err($crate::function_internal::panic_to_error(payload)),
+                    ::core::result::Result::Err(payload) => 
::core::result::Result::Err($crate::error::Error::from_panic(payload)),
                 };
                 $crate::function_internal::write_exported_str(metadata, result)
             }
@@ -850,7 +916,7 @@ macro_rules! __tvm_ffi_dll_export_doc {
 #[macro_export]
 macro_rules! into_typed_fn {
     // Case for 0 arguments
-    ($f:expr, $trait:ident() -> $ret_ty:ty) => {{
+    ($f:expr, Fn() -> $ret_ty:ty) => {{
         let _f = $f;
         move || -> $ret_ty {
             ::core::convert::TryInto::try_into(_f.call_tuple(())?)
@@ -858,7 +924,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 1 argument
-    ($f:expr, $trait:ident($t0:ty) -> $ret_ty:ty) => {{
+    ($f:expr, Fn($t0:ty) -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0| -> $ret_ty {
             ::core::convert::TryInto::try_into(_f.call_tuple((a0,))?)
@@ -866,7 +932,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 2 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty) -> $ret_ty:ty) => {{
+    ($f:expr, Fn($t0:ty, $t1:ty) -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1| -> $ret_ty {
             ::core::convert::TryInto::try_into(_f.call_tuple((a0, a1))?)
@@ -874,7 +940,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 3 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty, $t2:ty) -> $ret_ty:ty) => {{
+    ($f:expr, Fn($t0:ty, $t1:ty, $t2:ty) -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1, a2: $t2| -> $ret_ty {
             ::core::convert::TryInto::try_into(_f.call_tuple((a0, a1, a2))?)
@@ -882,7 +948,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 4 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty, $t2:ty, $t3:ty) -> $ret_ty:ty) => {{
+    ($f:expr, Fn($t0:ty, $t1:ty, $t2:ty, $t3:ty) -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1, a2: $t2, a3: $t3| -> $ret_ty {
             ::core::convert::TryInto::try_into(_f.call_tuple((a0, a1, a2, 
a3))?)
@@ -890,7 +956,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 5 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty) -> 
$ret_ty:ty) => {{
+    ($f:expr, Fn($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty) -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1, a2: $t2, a3: $t3, a4: $t4| -> $ret_ty {
             ::core::convert::TryInto::try_into(_f.call_tuple((a0, a1, a2, a3, 
a4))?)
@@ -898,7 +964,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 6 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty, $t5:ty) -> 
$ret_ty:ty) => {{
+    ($f:expr, Fn($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty, $t5:ty) -> 
$ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1, a2: $t2, a3: $t3, a4: $t4, a5: $t5| -> $ret_ty 
{
             ::core::convert::TryInto::try_into(_f.call_tuple((a0, a1, a2, a3, 
a4, a5))?)
@@ -906,7 +972,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 7 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty, $t5:ty, 
$t6:ty)
+    ($f:expr, Fn($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty, $t5:ty, $t6:ty)
         -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1, a2: $t2, a3: $t3, a4: $t4, a5: $t5, a6: $t6| 
-> $ret_ty {
@@ -915,7 +981,7 @@ macro_rules! into_typed_fn {
         }
     }};
     // Case for 8 arguments
-    ($f:expr, $trait:ident($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty, $t5:ty, 
$t6:ty, $t7:ty)
+    ($f:expr, Fn($t0:ty, $t1:ty, $t2:ty, $t3:ty, $t4:ty, $t5:ty, $t6:ty, 
$t7:ty)
         -> $ret_ty:ty) => {{
         let _f = $f;
         move |a0: $t0, a1: $t1, a2: $t2, a3: $t3, a4: $t4, a5: $t5, a6: $t6, 
a7: $t7| -> $ret_ty {
@@ -923,4 +989,10 @@ macro_rules! into_typed_fn {
                 .map_err(::core::convert::Into::into)
         }
     }};
+    ($f:expr, $($signature:tt)*) => {
+        ::core::compile_error!(::core::concat!(
+            "expected `Fn(A, ...) -> R` with 0 to 8 arguments, ",
+            "as in `into_typed_fn!(func, Fn(i32) -> Result<i32>)`"
+        ))
+    };
 }
diff --git a/rust/tvm-ffi/src/object.rs b/rust/tvm-ffi/src/object.rs
index 10c52b3e..4e18c8d3 100644
--- a/rust/tvm-ffi/src/object.rs
+++ b/rust/tvm-ffi/src/object.rs
@@ -291,13 +291,16 @@ impl<T: ObjectCore> ObjectArc<T> {
         unsafe { unsafe_::is_unique(this.ptr.as_ptr().cast::<TVMFFIObject>()) }
     }
 
-    /// The number of weak references, as `Arc::weak_count`.
+    /// The number of weak references, as `Arc::weak_count`: the weak
+    /// reference that the strong references hold together is not counted.
     ///
     /// # Arguments
     /// * `this` - The handle.
     #[inline]
     pub fn weak_count(this: &Self) -> usize {
-        unsafe { unsafe_::weak_count(this.ptr.as_ptr().cast::<TVMFFIObject>()) 
}
+        // While a strong reference lives, the raw count includes the one weak
+        // reference that all strong references share.
+        unsafe { unsafe_::weak_count(this.ptr.as_ptr().cast::<TVMFFIObject>()) 
- 1 }
     }
 }
 
@@ -433,10 +436,12 @@ pub unsafe trait ObjectCore: Sized + 'static {
 /// # Safety
 ///
 /// The blanket implementations are the only sound ones: do not implement it.
+#[doc(hidden)]
 #[diagnostic::on_unimplemented(message = "`{Self}` does not derive from 
`{A}`")]
 pub unsafe trait ObjectDerivesFrom<A, Depth> {}
 
 /// Inferred depths for [`ObjectDerivesFrom`].
+#[doc(hidden)]
 pub mod marker {
     /// `A` is the object type itself.
     pub enum Itself {}
@@ -547,6 +552,31 @@ pub unsafe trait ObjectRefCore: Sized + Clone + 
AnyCompatible {
             == ObjectArc::as_ptr(Other::data(other)).cast::<()>()
     }
 
+    /// The registered type key of the object's runtime type, such as
+    /// `"ffi.Array"`, as C++ `ObjectRef::GetTypeKey`.
+    #[inline]
+    fn type_key(&self) -> &'static str {
+        // SAFETY: ObjectRefCore guarantees a live object with a registered
+        // type index.
+        let type_index =
+            unsafe { 
(*ObjectArc::as_ptr(Self::data(self)).cast::<TVMFFIObject>()).type_index };
+        registered_type_key(type_index)
+    }
+
+    /// Whether the reference is a `T` as is, such as a subtype or a container
+    /// whose elements are checked too, by the strict check of
+    /// [`downcast`](Self::downcast), without a reference count change.
+    /// See also [`crate::Any::is`].
+    #[inline(always)]
+    fn is<T: AnyCompatible>(&self) -> bool {
+        let mut any_data = TVMFFIAny::new();
+        // SAFETY: the view of `self` is a valid cell while `self` is borrowed.
+        unsafe {
+            Self::copy_to_any_view(self, &mut any_data);
+            T::check_any_strict(&any_data)
+        }
+    }
+
     /// Borrow the underlying object as `T` when it is one.
     ///
     /// `T` is an object type such as `MyPairObj`, or a managed reference such
@@ -631,7 +661,7 @@ pub unsafe trait ObjectRefCore: Sized + Clone + 
AnyCompatible {
 /// use tvm_ffi::{rs_object, Any, AnyView, Object, ObjectArc};
 /// use tvm_ffi::object::ObjectRefAnyCompatible;
 ///
-/// #[rs_object("my_ext.MyPair", final)]
+/// #[rs_object(type_key = "my_ext.MyPair", final)]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
@@ -960,6 +990,15 @@ pub(crate) fn into_root_object_ref<S: 
ObjectRefCore>(value: S) -> ObjectRef {
     }
 }
 
+/// The type key of `type_index`, the type index of a value, which the runtime
+/// always registers.
+#[inline]
+pub(crate) fn registered_type_key(type_index: i32) -> &'static str {
+    // SAFETY: a value's type index is registered, and type information lives
+    // as long as the process; a type key is UTF-8 text.
+    unsafe { (*TVMFFIGetTypeInfo(type_index)).type_key.as_str() }
+}
+
 /// The registered type key of `type_index`, or `<type_index N>` when it has 
none.
 pub(crate) fn type_key_of_index(type_index: i32) -> String {
     match crate::reflection::type_key(type_index) {
@@ -1019,7 +1058,7 @@ pub mod unsafe_ {
 
     /// Increase the strong reference count of `handle`.
     #[inline]
-    pub unsafe fn inc_ref(handle: *mut TVMFFIObject) {
+    pub(crate) unsafe fn inc_ref(handle: *mut TVMFFIObject) {
         let obj = &*handle;
         obj.combined_ref_count.fetch_add(1, Ordering::Relaxed);
     }
diff --git a/rust/tvm-ffi/src/optional.rs b/rust/tvm-ffi/src/optional.rs
index d17208df..3a895661 100644
--- a/rust/tvm-ffi/src/optional.rs
+++ b/rust/tvm-ffi/src/optional.rs
@@ -93,6 +93,11 @@ pub struct Optional<T: OptionalCompatible> {
     _marker: PhantomData<T>,
 }
 
+// SAFETY: the cell holds only a `T` or `None`, so sending or sharing an
+// `Optional` is as safe as sending or sharing its value.
+unsafe impl<T: OptionalCompatible + Send + Sync> Send for Optional<T> {}
+unsafe impl<T: OptionalCompatible + Send + Sync> Sync for Optional<T> {}
+
 // Must stay 16 bytes / `TVMFFIAny`-aligned to overlay a C++ `ffi::Optional<T>`
 // field in place, independent of `T`.
 const _: () = assert!(
diff --git a/rust/tvm-ffi/src/reflection/accessor.rs 
b/rust/tvm-ffi/src/reflection/accessor.rs
index 8b99d7f7..0f008790 100644
--- a/rust/tvm-ffi/src/reflection/accessor.rs
+++ b/rust/tvm-ffi/src/reflection/accessor.rs
@@ -20,6 +20,7 @@
 //! Reads of registered types: methods, fields and type attributes.
 
 use std::ffi::c_void;
+use std::marker::PhantomData;
 use std::ptr::NonNull;
 use std::sync::{PoisonError, RwLock};
 
@@ -75,7 +76,6 @@ impl TypeAttrColumn {
             Error::new(
                 crate::INTERNAL_ERROR,
                 &format!("type attribute column `{name}` was not created"),
-                "",
             )
         })
     }
@@ -286,14 +286,29 @@ pub fn get_method_by_index(type_index: i32, method_name: 
&str) -> Result<Functio
     }
 }
 /// Resolves a reflected field once, then uses its registered C ABI getter.
-#[derive(Clone, Copy)]
-pub struct FieldGetter {
+///
+/// [`FieldGetter::new`] resolves a field by runtime type index into a
+/// `FieldGetter`, which checks the type of each object it reads. 
[`FieldGetter::of`] resolves a field of the
+/// object type `N` into a `FieldGetter<N>`, which reads only `&N`, so the
+/// type system replaces that check; 
[`cached_field_getter!`](crate::cached_field_getter)
+/// keeps one for the process.
+pub struct FieldGetter<N = ()> {
     owner_type_index: i32,
     owner_type_depth: i32,
     field_offset: i64,
     getter: TVMFFIFieldGetter,
+    owner: PhantomData<fn(&N)>,
+}
+
+impl<N> Clone for FieldGetter<N> {
+    #[inline(always)]
+    fn clone(&self) -> Self {
+        *self
+    }
 }
 
+impl<N> Copy for FieldGetter<N> {}
+
 impl FieldGetter {
     /// Resolve a reflected field declared by `type_index` or one of its bases.
     ///
@@ -302,40 +317,16 @@ impl FieldGetter {
     /// Returns a `TypeError` if `type_index` is not registered or the type has
     /// no reflected field `field_name` with a getter.
     pub fn new(type_index: i32, field_name: &str) -> Result<Self> {
-        // The C API aborts the process on an unregistered index.
-        if !is_registered(type_index) {
-            return Err(Error::new(
-                TYPE_ERROR,
-                &format!("Cannot find type info for type_index={type_index}"),
-                "",
-            ));
-        }
-        let type_info = unsafe { TVMFFIGetTypeInfo(type_index) };
+        resolve(type_index, field_name)
+    }
 
-        let field = unsafe { find_field(type_info, field_name) }.ok_or_else(|| 
{
-            let type_key = unsafe { (*type_info).type_key.as_str() };
-            Error::new(
-                TYPE_ERROR,
-                &format!("Cannot find reflected field `{field_name}` in type 
`{type_key}`"),
-                "",
-            )
-        })?;
-        let field = unsafe { field.as_ref() };
-        let getter = field.getter.ok_or_else(|| {
-            // SAFETY: a field name is UTF-8 text.
-            let name = unsafe { field.name.as_str() };
-            Error::new(
-                TYPE_ERROR,
-                &format!("Reflected field `{name}` has no getter"),
-                "",
-            )
-        })?;
-        Ok(Self {
-            owner_type_index: type_index,
-            owner_type_depth: unsafe { (*type_info).type_depth },
-            field_offset: field.offset,
-            getter,
-        })
+    /// Resolve a reflected field declared by `N` or one of its bases.
+    ///
+    /// # Errors
+    ///
+    /// As [`FieldGetter::new`] for `N::type_index()`.
+    pub fn of<N: ObjectCore>(field_name: &str) -> Result<FieldGetter<N>> {
+        resolve(N::type_index(), field_name)
     }
 
     /// Read the field as an owning [`Any`].
@@ -357,13 +348,48 @@ impl FieldGetter {
                 &format!(
                     "Cannot read a field of type_index={} from object 
type_index={dynamic_type_index}",
                     self.owner_type_index
-                ),
-                "",
-            ));
+                )));
         }
+        // SAFETY: `object` has the resolved type or a subtype, which embeds 
it.
+        unsafe { self.read(object) }
+    }
+
+    /// Read and convert the field to `T`.
+    pub fn get<N, T>(&self, object: &N) -> Result<T>
+    where
+        N: ObjectCore,
+        T: TryFrom<Any, Error = Error>,
+    {
+        T::try_from(self.get_any(object)?)
+    }
+}
+
+impl<N: ObjectCore> FieldGetter<N> {
+    /// Read the field as an owning [`Any`].
+    #[inline(always)]
+    pub fn get_any(&self, object: &N) -> Result<Any> {
+        // SAFETY: the field was resolved for `N`, whose layout `object` has.
+        unsafe { self.read(object) }
+    }
+
+    /// Read and convert the field to `T`.
+    #[inline(always)]
+    pub fn get<T>(&self, object: &N) -> Result<T>
+    where
+        T: TryFrom<Any, Error = Error>,
+    {
+        T::try_from(self.get_any(object)?)
+    }
+}
 
+impl<N> FieldGetter<N> {
+    /// # Safety
+    ///
+    /// `object` has the layout of the type the field was resolved for.
+    #[inline(always)]
+    unsafe fn read<M>(&self, object: &M) -> Result<Any> {
         let field_address = unsafe {
-            object_pointer
+            std::ptr::from_ref(object)
                 .cast::<u8>()
                 .offset(self.field_offset as isize)
                 .cast_mut()
@@ -375,15 +401,41 @@ impl FieldGetter {
         }
         Ok(result)
     }
+}
 
-    /// Read and convert the field to `T`.
-    pub fn get<N, T>(&self, object: &N) -> Result<T>
-    where
-        N: ObjectCore,
-        T: TryFrom<Any, Error = Error>,
-    {
-        T::try_from(self.get_any(object)?)
+fn resolve<N>(type_index: i32, field_name: &str) -> Result<FieldGetter<N>> {
+    // The C API aborts the process on an unregistered index.
+    if !is_registered(type_index) {
+        return Err(Error::new(
+            TYPE_ERROR,
+            &format!("Cannot find type info for type_index={type_index}"),
+        ));
     }
+    let type_info = unsafe { TVMFFIGetTypeInfo(type_index) };
+
+    let field = unsafe { find_field(type_info, field_name) }.ok_or_else(|| {
+        let type_key = unsafe { (*type_info).type_key.as_str() };
+        Error::new(
+            TYPE_ERROR,
+            &format!("Cannot find reflected field `{field_name}` in type 
`{type_key}`"),
+        )
+    })?;
+    let field = unsafe { field.as_ref() };
+    let getter = field.getter.ok_or_else(|| {
+        // SAFETY: a field name is UTF-8 text.
+        let name = unsafe { field.name.as_str() };
+        Error::new(
+            TYPE_ERROR,
+            &format!("Reflected field `{name}` has no getter"),
+        )
+    })?;
+    Ok(FieldGetter {
+        owner_type_index: type_index,
+        owner_type_depth: unsafe { (*type_info).type_depth },
+        field_offset: field.offset,
+        getter,
+        owner: PhantomData,
+    })
 }
 
 unsafe fn find_field(
diff --git a/rust/tvm-ffi/src/reflection/enum_def.rs 
b/rust/tvm-ffi/src/reflection/enum_def.rs
index 63278daf..3c2e25b7 100644
--- a/rust/tvm-ffi/src/reflection/enum_def.rs
+++ b/rust/tvm-ffi/src/reflection/enum_def.rs
@@ -117,14 +117,12 @@ impl<E: EnumRef> EnumDef<E> {
             return Err(Error::new(
                 VALUE_ERROR,
                 &format!("a value name of `{type_key}` must be non-empty"),
-                "",
             ));
         }
         if !is_instance_of::<EnumObj>(type_index) || is_int_enum(type_index) {
             return Err(Error::new(
                 TYPE_ERROR,
                 &format!("`{type_key}` is not an enum type whose values Rust 
can register"),
-                "",
             ));
         }
         // The assert above sees only the Rust mirror, which may be opaque: a
@@ -142,7 +140,6 @@ impl<E: EnumRef> EnumDef<E> {
                     "`{type_key}` objects have fields beyond `ffi.Enum`, so 
Rust cannot \
                      register its values"
                 ),
-                "",
             ));
         }
         let _registering = lock_type_registration();
@@ -156,7 +153,6 @@ impl<E: EnumRef> EnumDef<E> {
             return Err(Error::new(
                 VALUE_ERROR,
                 &format!("Enum `{type_key}` already has a value named 
`{name}`"),
-                "",
             ));
         }
         let object = ObjectArc::into_raw(ObjectArc::new(EnumObj::new(index, 
name)));
diff --git a/rust/tvm-ffi/src/reflection/native_function.rs 
b/rust/tvm-ffi/src/reflection/native_function.rs
index 06f42b02..8f6d19aa 100644
--- a/rust/tvm-ffi/src/reflection/native_function.rs
+++ b/rust/tvm-ffi/src/reflection/native_function.rs
@@ -42,7 +42,7 @@ use crate::{Any, AnyView, Error, Function, Result, 
TYPE_ERROR};
 /// it. Reading a hook with [`from_any`](Self::from_any) is `unsafe`, the
 /// caller vouching for the signature, and then calling it is safe. Building a
 /// hook from a Rust function with `into()` is safe; storing it, with
-/// [`TypeAttrDef::native_fn_attr`](super::TypeAttrDef::native_fn_attr) or
+/// 
[`TypeAttrDef::set_native_fn_attr`](super::TypeAttrDef::set_native_fn_attr) or
 /// [`EnumDef::set_native_fn_attr`](super::EnumDef::set_native_fn_attr), or
 /// converting it to [`Any`] for another registry with
 /// [`into_any`](Self::into_any), is `unsafe`, the caller vouching that the
@@ -54,7 +54,7 @@ use crate::{Any, AnyView, Error, Function, Result, 
TYPE_ERROR};
 /// use tvm_ffi::reflection::{NativeFunction, TypeAttrColumn, TypeAttrDef};
 /// use tvm_ffi::{rs_object, Object, ObjectArc, ObjectCore, Result};
 ///
-/// #[rs_object("my_ext.MyPair")]
+/// #[rs_object(type_key = "my_ext.MyPair")]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     first: i64,
@@ -67,7 +67,7 @@ use crate::{Any, AnyView, Error, Function, Result, 
TYPE_ERROR};
 ///
 /// let hook: NativeFunction<(&MyPairObj,), i64> = sum.into();
 /// // SAFETY: `my_ext.sum` holds `i64(const MyPairObj*)` hooks.
-/// unsafe { TypeAttrDef::<MyPairObj>::new().native_fn_attr("my_ext.sum", 
hook)? };
+/// unsafe { TypeAttrDef::<MyPairObj>::new().set_native_fn_attr("my_ext.sum", 
hook)? };
 ///
 /// let stored = 
TypeAttrColumn::lookup("my_ext.sum").unwrap().get(MyPairObj::type_index()).unwrap();
 /// // SAFETY: `my_ext.sum` holds `i64(const MyPairObj*)` hooks.
@@ -119,8 +119,7 @@ impl<Args: NativeArgs, R: TryFrom<Any>> 
NativeFunction<Args, R> {
         if result.type_index() == TVMFFITypeIndex::kTVMFFIError as i32 {
             return Err(result.try_into()?);
         }
-        R::try_from(result)
-            .map_err(|_| Error::new(TYPE_ERROR, "Packed hook result type 
mismatch", ""))
+        R::try_from(result).map_err(|_| Error::new(TYPE_ERROR, "Packed hook 
result type mismatch"))
     }
 }
 
@@ -169,7 +168,7 @@ impl<Args, R> NativeFunction<Args, R> {
                 "Cannot convert from type `{}` to `NativeFunction`",
                 type_key_of_index(raw.type_index)
             );
-            return Err(Error::new(TYPE_ERROR, &message, ""));
+            return Err(Error::new(TYPE_ERROR, &message));
         }
         Ok(Self {
             data: value,
@@ -332,7 +331,7 @@ mod sealed {
                         // argument for the duration of the call, or null.
                         $(let Some($raw) = (unsafe { $raw.as_ref() }) else {
                             let message = "A native hook argument is null";
-                            return into_raw::<R>(Err(Error::new(TYPE_ERROR, 
message, "")));
+                            return into_raw::<R>(Err(Error::new(TYPE_ERROR, 
message)));
                         };)*
                         // A panic aborts here rather than unwind into the 
caller.
                         into_raw(function($($raw),*))
diff --git a/rust/tvm-ffi/src/reflection/registry.rs 
b/rust/tvm-ffi/src/reflection/registry.rs
index 05595700..5f893ea2 100644
--- a/rust/tvm-ffi/src/reflection/registry.rs
+++ b/rust/tvm-ffi/src/reflection/registry.rs
@@ -174,7 +174,7 @@ impl BitOr for FieldFlags {
 /// use tvm_ffi::reflection::{self, ObjectDef};
 /// use tvm_ffi::{rs_object, Object, ObjectArc, Result};
 ///
-/// #[rs_object("my_ext.MyPair", final, reflection = 
MyPairObj::reflect_methods)]
+/// #[rs_object(type_key = "my_ext.MyPair", final, reflection = 
MyPairObj::reflect_methods)]
 /// pub struct MyPairObj {
 ///     base_: Object,
 ///     pub first: i64,
@@ -378,7 +378,6 @@ impl<N: ObjectCore> ObjectDef<N> {
             self.error = Some(Error::new(
                 VALUE_ERROR,
                 &format!("duplicate method `{name}` of `{}`", N::TYPE_KEY),
-                "",
             ));
             return self;
         }
@@ -417,7 +416,6 @@ impl<N: ObjectCore> ObjectDef<N> {
             self.error = Some(Error::new(
                 error.kind(),
                 &format!("registering method `{name}`: {}", error.message()),
-                "",
             ));
         }
         self
@@ -487,7 +485,7 @@ pub unsafe fn register_object<N: ObjectCore + ShallowCopy>(
 }
 
 /// Registers type attributes of the object type `T`, each as soon as
-/// [`attr`](Self::attr) names it, for every language to read with
+/// [`set_attr`](Self::set_attr) names it, for every language to read with
 /// [`TypeAttrColumn`](crate::reflection::TypeAttrColumn).
 ///
 /// As in C++, attributes register during static initialization, such as in a
@@ -502,12 +500,12 @@ pub unsafe fn register_object<N: ObjectCore + 
ShallowCopy>(
 /// use tvm_ffi::reflection::TypeAttrColumn;
 /// use tvm_ffi::{rs_object, Object, ObjectCore};
 ///
-/// #[rs_object("my_ext.MyPair")]
+/// #[rs_object(type_key = "my_ext.MyPair")]
 /// pub struct MyPairObj {
 ///     base_: Object,
 /// }
 ///
-/// TypeAttrDef::<MyPairObj>::new().attr("my_ext.arity", 2_i64)?;
+/// TypeAttrDef::<MyPairObj>::new().set_attr("my_ext.arity", 2_i64)?;
 ///
 /// let column = TypeAttrColumn::lookup("my_ext.arity").unwrap();
 /// let arity: i64 = column.get(MyPairObj::type_index()).unwrap().try_into()?;
@@ -541,8 +539,8 @@ impl<T: ObjectCore> TypeAttrDef<T> {
     ///
     /// Returns an error if `T` already has the attribute `name`, or a
     /// `TypeError` if `value` is an opaque pointer: register a native hook
-    /// with [`native_fn_attr`](Self::native_fn_attr).
-    pub fn attr(self, name: &str, value: impl Into<Any> + Send + Sync) -> 
Result<Self> {
+    /// with [`set_native_fn_attr`](Self::set_native_fn_attr).
+    pub fn set_attr(self, name: &str, value: impl Into<Any> + Send + Sync) -> 
Result<Self> {
         let value = value.into();
         ensure_not_opaque_ptr(name, &value)?;
         self.register(name, value)
@@ -567,7 +565,7 @@ impl<T: ObjectCore> TypeAttrDef<T> {
     ///
     /// Every caller of the column `name` calls its hooks with exactly `Args`
     /// and reads `R`: a native caller calls the function pointer unchecked.
-    pub unsafe fn native_fn_attr<Args, R>(
+    pub unsafe fn set_native_fn_attr<Args, R>(
         self,
         name: &str,
         f: NativeFunction<Args, R>,
@@ -618,7 +616,6 @@ fn shallow_copy_any<N: ObjectCore + ShallowCopy>(object: 
&ObjectRef) -> Result<A
                 N::TYPE_KEY,
                 object_type_key(object)
             ),
-            "",
         ));
     };
     let copy = ObjectArc::new(node.shallow_copy());
@@ -666,7 +663,7 @@ unsafe extern "C" fn field_getter<F: AnyCompatible>(
         Ok(()) => 0,
         Err(_) => {
             let message = format!("panic while reading a field of type {}", 
F::type_str());
-            Error::set_raised(&Error::new(INTERNAL_ERROR, &message, ""));
+            Error::set_raised(&Error::new(INTERNAL_ERROR, &message));
             -1
         }
     }
@@ -697,7 +694,6 @@ unsafe extern "C" fn field_setter<F: AnyCompatible>(
                     F::type_str(),
                     F::get_mismatch_type_info(&*value)
                 ),
-                "",
             )),
         }
     }));
@@ -707,7 +703,6 @@ unsafe extern "C" fn field_setter<F: AnyCompatible>(
         Err(_) => Error::new(
             INTERNAL_ERROR,
             &format!("panic while writing a field of type {}", F::type_str()),
-            "",
         ),
     };
     Error::set_raised(&error);
diff --git a/rust/tvm-ffi/src/reflection/unsafe_init.rs 
b/rust/tvm-ffi/src/reflection/unsafe_init.rs
index d11fea85..28a0410b 100644
--- a/rust/tvm-ffi/src/reflection/unsafe_init.rs
+++ b/rust/tvm-ffi/src/reflection/unsafe_init.rs
@@ -209,7 +209,7 @@ pub unsafe extern "C" fn unsafe_init_creator<T: UnsafeInit>(
         Ok(()) => 0,
         Err(_) => {
             let message = format!("panic while creating an object of `{}`", 
T::TYPE_KEY);
-            Error::set_raised(&Error::new(INTERNAL_ERROR, &message, ""));
+            Error::set_raised(&Error::new(INTERNAL_ERROR, &message));
             -1
         }
     }
diff --git a/rust/tvm-ffi/src/rvalue_ref.rs b/rust/tvm-ffi/src/rvalue_ref.rs
index d63cc456..858a7d94 100644
--- a/rust/tvm-ffi/src/rvalue_ref.rs
+++ b/rust/tvm-ffi/src/rvalue_ref.rs
@@ -196,6 +196,5 @@ where
             "{prefix}Cannot convert from `{source}` to `RValueRef<{}>`",
             T::type_str()
         ),
-        "",
     )
 }
diff --git a/rust/tvm-ffi/src/static_init.rs b/rust/tvm-ffi/src/static_init.rs
index 35ea6be1..b137df8b 100644
--- a/rust/tvm-ffi/src/static_init.rs
+++ b/rust/tvm-ffi/src/static_init.rs
@@ -28,7 +28,6 @@ use std::ffi::c_void;
 use std::panic::catch_unwind;
 use std::sync::{Once, OnceLock};
 
-use crate::function_internal::panic_to_error;
 use crate::{Any, Error, Function, Result, INTERNAL_ERROR};
 
 #[doc(hidden)]
@@ -234,7 +233,7 @@ impl StaticInitBlock {
             (None, Ok(())) => Ok(()),
             (None, Err(text)) => {
                 let message = format!("The static init block at {} failed: 
{text}", self.place());
-                Err(Error::new(INTERNAL_ERROR, &message, ""))
+                Err(Error::new(INTERNAL_ERROR, &message))
             }
         }
     }
@@ -246,7 +245,7 @@ impl StaticInitBlock {
     fn run_once(&self) -> Result<()> {
         let marker = self.marker();
         if Function::get_global(&marker).is_err() {
-            catch_unwind(self.body).unwrap_or_else(|payload| 
Err(panic_to_error(payload)))?;
+            catch_unwind(self.body).unwrap_or_else(|payload| 
Err(Error::from_panic(payload)))?;
             Function::register_global(&marker, Function::from_packed(|_| 
Ok(Any::default())))?;
         }
         Ok(())
@@ -310,7 +309,7 @@ pub fn run_static_init_blocks() -> Result<()> {
             error.message()
         ));
     }
-    Err(Error::new(failures[0].1.kind(), &message, ""))
+    Err(Error::new(failures[0].1.kind(), &message))
 }
 
 /// The constructor of every [`static_init_block!`](crate::static_init_block), 
which the loader
diff --git a/rust/tvm-ffi/src/string.rs b/rust/tvm-ffi/src/string.rs
index 2eba2150..bf5929ef 100644
--- a/rust/tvm-ffi/src/string.rs
+++ b/rust/tvm-ffi/src/string.rs
@@ -114,14 +114,9 @@ unsafe impl ObjectCoreWithExtraItems for BytesObj {
     }
 }
 
-impl<T> From<T> for Bytes
-where
-    T: AsRef<[u8]>,
-{
-    #[inline]
-    /// Copy the bytes of `src`.
-    fn from(src: T) -> Self {
-        let value: &[u8] = src.as_ref();
+impl From<&[u8]> for Bytes {
+    /// Copy the bytes of `value`.
+    fn from(value: &[u8]) -> Self {
         // to be compatible with normal c++
         const MAX_SMALL_BYTES_LEN: usize = 7;
         unsafe {
@@ -224,17 +219,64 @@ impl Hash for Bytes {
     }
 }
 
-impl Display for Bytes {
+/// Formats as `[u8]` does, such as `[97, 98]`.
+impl Debug for Bytes {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        Debug::fmt(&self.as_slice(), f)
+        Debug::fmt(self.as_slice(), f)
     }
 }
 
-impl Debug for Bytes {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ffi.Bytes")
-            .field("data", &self.as_slice())
-            .finish()
+macro_rules! impl_bytes_from {
+    ($([$($g:tt)*] $t:ty),* $(,)?) => {
+        $(impl<$($g)*> From<$t> for Bytes {
+            #[inline]
+            fn from(value: $t) -> Self {
+                Self::from(&value[..])
+            }
+        })*
+    };
+}
+
+impl_bytes_from!([] &mut [u8], [] Vec<u8>, [] &Vec<u8>, [] Box<[u8]>, [const 
N: usize] [u8; N], [const N: usize] &[u8; N]);
+
+macro_rules! impl_bytes_eq {
+    ($([$($g:tt)*] $t:ty),* $(,)?) => {
+        $(impl<$($g)*> PartialEq<$t> for Bytes {
+            #[inline]
+            fn eq(&self, other: &$t) -> bool {
+                self.as_slice() == &other[..]
+            }
+        }
+
+        impl<$($g)*> PartialEq<Bytes> for $t {
+            #[inline]
+            fn eq(&self, other: &Bytes) -> bool {
+                &self[..] == other.as_slice()
+            }
+        })*
+    };
+}
+
+impl_bytes_eq!([] [u8], ['a] &'a [u8], [] Vec<u8>, [const N: usize] [u8; N], 
['a, const N: usize] &'a [u8; N]);
+
+impl AsRef<[u8]> for Bytes {
+    #[inline]
+    fn as_ref(&self) -> &[u8] {
+        self.as_slice()
+    }
+}
+
+impl std::borrow::Borrow<[u8]> for Bytes {
+    #[inline]
+    fn borrow(&self) -> &[u8] {
+        self.as_slice()
+    }
+}
+
+impl From<Bytes> for Vec<u8> {
+    #[inline]
+    fn from(value: Bytes) -> Self {
+        value.as_slice().to_vec()
     }
 }
 
@@ -331,15 +373,10 @@ impl String {
     }
 }
 
-impl<T> From<T> for String
-where
-    T: AsRef<str>,
-{
-    #[inline]
-    /// Copy the string `src`.
-    fn from(src: T) -> Self {
+impl From<&str> for String {
+    /// Copy the string `value`.
+    fn from(value: &str) -> Self {
         unsafe {
-            let value: &str = src.as_ref();
             let bytes = value.as_bytes();
             const MAX_SMALL_BYTES_LEN: usize = 7;
             if bytes.len() <= MAX_SMALL_BYTES_LEN {
@@ -423,29 +460,69 @@ impl PartialEq for String {
     }
 }
 
-// Allows `my_string == "hello"`
-impl<T> PartialEq<T> for String
-where
-    T: AsRef<str>,
-{
+macro_rules! impl_string_from {
+    ($([$($g:tt)*] $t:ty),* $(,)?) => {
+        $(impl<$($g)*> From<$t> for String {
+            #[inline]
+            fn from(value: $t) -> Self {
+                Self::from(&value[..])
+            }
+        })*
+    };
+}
+
+impl_string_from!([] &mut str, [] std::string::String, [] 
&std::string::String, [] Box<str>, ['a] std::borrow::Cow<'a, str>);
+
+macro_rules! impl_string_eq {
+    ($([$($g:tt)*] $t:ty),* $(,)?) => {
+        $(impl<$($g)*> PartialEq<$t> for String {
+            #[inline]
+            fn eq(&self, other: &$t) -> bool {
+                self.as_str() == &other[..]
+            }
+        }
+
+        impl<$($g)*> PartialEq<String> for $t {
+            #[inline]
+            fn eq(&self, other: &String) -> bool {
+                &self[..] == other.as_str()
+            }
+        })*
+    };
+}
+
+impl_string_eq!([] str, ['a] &'a str, [] std::string::String, ['a] 
std::borrow::Cow<'a, str>);
+
+impl AsRef<str> for String {
     #[inline]
-    fn eq(&self, other: &T) -> bool {
-        self.as_str() == other.as_ref()
+    fn as_ref(&self) -> &str {
+        self.as_str()
     }
 }
 
-impl Eq for String {}
+impl AsRef<[u8]> for String {
+    #[inline]
+    fn as_ref(&self) -> &[u8] {
+        self.as_bytes()
+    }
+}
+
+impl std::borrow::Borrow<str> for String {
+    #[inline]
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
 
-impl<T> PartialEq<T> for Bytes
-where
-    T: AsRef<[u8]>,
-{
+impl From<String> for std::string::String {
     #[inline]
-    fn eq(&self, other: &T) -> bool {
-        self.as_slice() == other.as_ref()
+    fn from(value: String) -> Self {
+        value.as_str().to_owned()
     }
 }
 
+impl Eq for String {}
+
 impl PartialOrd for String {
     #[inline]
     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
@@ -473,11 +550,10 @@ impl Display for String {
     }
 }
 
+/// Formats as `str` does, such as `"hi"`.
 impl Debug for String {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ffi.String")
-            .field("data", &self.as_str())
-            .finish()
+        Debug::fmt(self.as_str(), f)
     }
 }
 
diff --git a/rust/tvm-ffi/tests/test_device.rs 
b/rust/tvm-ffi/tests/test_device.rs
index 05467e95..d70f35d3 100644
--- a/rust/tvm-ffi/tests/test_device.rs
+++ b/rust/tvm-ffi/tests/test_device.rs
@@ -42,7 +42,7 @@ fn test_device_stream_restored_after_error() {
     // SAFETY: dummy_stream is a test value; CPU device accepts any stream 
handle.
     let result: Result<()> = unsafe {
         with_stream(&device, dummy_stream, || {
-            Err(Error::new(VALUE_ERROR, "closure failed", ""))
+            Err(Error::new(VALUE_ERROR, "closure failed"))
         })
     };
     assert_eq!(result.unwrap_err().message(), "closure failed");
diff --git a/rust/tvm-ffi/tests/test_error.rs b/rust/tvm-ffi/tests/test_error.rs
index e209ddd7..51ea1a61 100644
--- a/rust/tvm-ffi/tests/test_error.rs
+++ b/rust/tvm-ffi/tests/test_error.rs
@@ -20,7 +20,7 @@ use tvm_ffi::*;
 
 #[test]
 fn test_error_new() {
-    let error = Error::new(RUNTIME_ERROR, "test error", "test traceback");
+    let error = Error::new(RUNTIME_ERROR, "test error").with_backtrace("test 
traceback");
     assert_eq!(error.kind(), RUNTIME_ERROR);
     assert_eq!(error.message(), "test error");
     assert_eq!(error.backtrace(), "test traceback");
@@ -28,7 +28,7 @@ fn test_error_new() {
 
 #[test]
 fn test_error_from_raised() {
-    let error0 = Error::new(RUNTIME_ERROR, "test error", "test traceback");
+    let error0 = Error::new(RUNTIME_ERROR, "test error").with_backtrace("test 
traceback");
     Error::set_raised(&error0);
     let error1 = Error::from_raised();
     assert_eq!(error1.kind(), RUNTIME_ERROR);
@@ -62,13 +62,10 @@ fn test_error_with_context() {
 
     let payload = Array::from_vec(vec![7i64]);
     for shared in [false, true] {
-        let error = Error::new_with_cause_and_extra_context(
-            RUNTIME_ERROR,
-            "outer",
-            "origin",
-            Some(&error0),
-            Some(&payload),
-        );
+        let error = Error::new(RUNTIME_ERROR, "outer")
+            .with_backtrace("origin")
+            .with_cause(&error0)
+            .with_extra_context(&payload);
         let retained = shared.then(|| error.clone());
         let output = Error::with_appended_backtrace(error, " frame");
         assert_eq!(output.backtrace(), "origin frame");
@@ -85,7 +82,7 @@ fn test_error_with_context() {
 
 #[test]
 fn test_error_any_convert() {
-    let error = Error::new(RUNTIME_ERROR, "test error", "test traceback");
+    let error = Error::new(RUNTIME_ERROR, "test error").with_backtrace("test 
traceback");
     let any = Any::from(error.clone());
     let error2 = any.clone().downcast_ref::<Error>().cloned().unwrap();
     assert_eq!(error2.kind(), RUNTIME_ERROR);
diff --git a/rust/tvm-ffi/tests/test_function_metadata.rs 
b/rust/tvm-ffi/tests/test_function_metadata.rs
index 753bc986..f0583490 100644
--- a/rust/tvm-ffi/tests/test_function_metadata.rs
+++ b/rust/tvm-ffi/tests/test_function_metadata.rs
@@ -22,7 +22,7 @@
 //! `export-metadata` feature, as C++ ones with
 //! `TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`; global functions always do.
 
-use tvm_ffi::function_internal::AsPackedCallable;
+use tvm_ffi::function::AsPackedCallable;
 #[cfg(feature = "export-metadata")]
 use tvm_ffi::tvm_ffi_sys::TVMFFIAny;
 use tvm_ffi::*;
diff --git a/rust/tvm-ffi/tests/test_object.rs 
b/rust/tvm-ffi/tests/test_object.rs
index 5a09d921..71f5384d 100644
--- a/rust/tvm-ffi/tests/test_object.rs
+++ b/rust/tvm-ffi/tests/test_object.rs
@@ -89,25 +89,25 @@ fn test_object_arc() {
     assert_eq!(view.debug_strong_count(), Some(1));
     assert_eq!(obj_arc.value, 11);
     assert_eq!(ObjectArc::strong_count(&obj_arc), 1);
-    assert_eq!(ObjectArc::weak_count(&obj_arc), 1);
+    assert_eq!(ObjectArc::weak_count(&obj_arc), 0);
 
     let ref1 = obj_arc.clone();
     assert_eq!(ObjectArc::strong_count(&obj_arc), 2);
-    assert_eq!(ObjectArc::weak_count(&obj_arc), 1);
+    assert_eq!(ObjectArc::weak_count(&obj_arc), 0);
 
     let ref2 = obj_arc.clone();
     assert_eq!(ObjectArc::strong_count(&obj_arc), 3);
-    assert_eq!(ObjectArc::weak_count(&obj_arc), 1);
+    assert_eq!(ObjectArc::weak_count(&obj_arc), 0);
     assert_eq!(ref1.value, 11);
     // drop obj_arc
     drop(obj_arc);
     assert_eq!(ObjectArc::strong_count(&ref1), 2);
-    assert_eq!(ObjectArc::weak_count(&ref1), 1);
+    assert_eq!(ObjectArc::weak_count(&ref1), 0);
     assert_eq!(delete_counter.load(Ordering::Relaxed), 0);
     // drop ref1
     drop(ref1);
     assert_eq!(ObjectArc::strong_count(&ref2), 1);
-    assert_eq!(ObjectArc::weak_count(&ref2), 1);
+    assert_eq!(ObjectArc::weak_count(&ref2), 0);
     assert_eq!(delete_counter.load(Ordering::Relaxed), 0);
     // drop ref2
     drop(ref2);
@@ -121,7 +121,7 @@ fn test_object_arc_with_extra_items() {
         unsafe { ObjectArc::new_with_extra_items(TestIntObj::new(12, 
delete_counter.clone(), 10)) };
     assert_eq!(obj_arc.value, 12);
     assert_eq!(ObjectArc::strong_count(&obj_arc), 1);
-    assert_eq!(ObjectArc::weak_count(&obj_arc), 1);
+    assert_eq!(ObjectArc::weak_count(&obj_arc), 0);
     assert_eq!(delete_counter.load(Ordering::Relaxed), 0);
     unsafe {
         let raw = ObjectArc::as_mut_ptr(&mut obj_arc);
@@ -267,7 +267,7 @@ fn test_object_arc_from_raw() {
         let obj_arc2 = ObjectArc::from_raw(raw_ptr);
         assert_eq!(obj_arc2.value, 11);
         assert_eq!(ObjectArc::strong_count(&obj_arc2), 1);
-        assert_eq!(ObjectArc::weak_count(&obj_arc2), 1);
+        assert_eq!(ObjectArc::weak_count(&obj_arc2), 0);
         assert_eq!(delete_counter.load(Ordering::Relaxed), 0);
         // drop obj_arc2
         drop(obj_arc2);
diff --git a/rust/tvm-ffi/tests/test_object_ref_any.rs 
b/rust/tvm-ffi/tests/test_object_ref_any.rs
index c7f484af..5d5dcfbd 100644
--- a/rust/tvm-ffi/tests/test_object_ref_any.rs
+++ b/rust/tvm-ffi/tests/test_object_ref_any.rs
@@ -26,7 +26,7 @@ use tvm_ffi::{impl_any_compatible_conversions, match_any, 
rs_object};
 use tvm_ffi::{Any, AnyCompatible, AnyView, Object, ObjectRefCore};
 
 // A derived reference.
-#[rs_object("testing.object_ref_any.Pair", final)]
+#[rs_object(type_key = "testing.object_ref_any.Pair", final)]
 pub struct PairObj {
     base_: Object,
     pub first: i64,
diff --git a/rust/tvm-ffi/tests/test_object_reflection.rs 
b/rust/tvm-ffi/tests/test_object_reflection.rs
index 7d0f54cf..969a85d3 100644
--- a/rust/tvm-ffi/tests/test_object_reflection.rs
+++ b/rust/tvm-ffi/tests/test_object_reflection.rs
@@ -348,10 +348,12 @@ fn 
test_types_with_only_methods_register_their_reflection() {
 #[test]
 fn test_reflected_fields_read_and_write_through_the_c_abi() {
     let p3 = point3(3, 4);
-    let x = FieldGetter::new(Point3Obj::type_index(), "x").unwrap();
-    let z = FieldGetter::new(Point3Obj::type_index(), "z").unwrap();
-    assert_eq!(x.get::<_, i64>(&*p3.data).unwrap(), 3);
-    assert_eq!(z.get::<_, i64>(&*p3.data).unwrap(), 4);
+    let x = FieldGetter::of::<Point3Obj>("x").unwrap();
+    assert_eq!(x.get::<i64>(&p3.data).unwrap(), 3);
+    let z: i64 = tvm_ffi::cached_field_getter!(Point3Obj, "z")
+        .get(&p3.data)
+        .unwrap();
+    assert_eq!(z, 4);
 
     let p = point(1, 2.0, "p");
     let y = &fields(type_info::<PointObj>())[1];
@@ -600,13 +602,13 @@ fn test_native_function() -> Result<()> {
     // A Rust closure as a native hook, whose error is the hook's.
     let x_of = |p: &PointObj| -> Result<i64> {
         match p.x {
-            0 => Err(Error::new(VALUE_ERROR, "zero", "")),
+            0 => Err(Error::new(VALUE_ERROR, "zero")),
             x => Ok(x),
         }
     };
     let hook = NativeFunction::<(&PointObj,), i64>::from(x_of);
     // SAFETY: `testing.x_of` holds `i64(const PointObj*)` hooks.
-    unsafe { TypeAttrDef::<PointObj>::new().native_fn_attr("testing.x_of", 
hook)? };
+    unsafe { TypeAttrDef::<PointObj>::new().set_native_fn_attr("testing.x_of", 
hook)? };
     let stored = TypeAttrColumn::lookup("testing.x_of")
         .unwrap()
         .get(PointObj::type_index());
@@ -627,7 +629,7 @@ fn test_native_function() -> Result<()> {
         }
     }
     let error = TypeAttrDef::<PointObj>::new()
-        .attr("testing.raw", RawPointer)
+        .set_attr("testing.raw", RawPointer)
         .err()
         .unwrap();
     assert_eq!(error.kind(), TYPE_ERROR);
diff --git a/rust/tvm-ffi/tests/test_rs_object.rs 
b/rust/tvm-ffi/tests/test_rs_object.rs
index 3bffb1e9..25f5d3bd 100644
--- a/rust/tvm-ffi/tests/test_rs_object.rs
+++ b/rust/tvm-ffi/tests/test_rs_object.rs
@@ -30,7 +30,7 @@ use tvm_ffi_sys::{TVMFFIFieldInfo, TVMFFIGetTypeInfo, 
TVMFFISEqHashKind, TVMFFIT
 
 /// A root type, under `ffi.Object`.
 #[rs_object(
-    "testing.rust.RsPoint",
+    type_key = "testing.rust.RsPoint",
     s_eq_hash_kind = "TreeNode",
     reflection = RsPointObj::reflect_methods
 )]
@@ -60,7 +60,7 @@ impl RsPointObj {
 }
 
 /// A subtype of a Rust-defined type, with a private field.
-#[rs_object("testing.rust.RsPoint3", final)]
+#[rs_object(type_key = "testing.rust.RsPoint3", final)]
 pub struct RsPoint3Obj {
     base_: RsPointObj,
     pub z: i64,
@@ -76,7 +76,7 @@ pub struct RsPoint3 {
 }
 
 /// A subtype without fields, which still registers its reflection.
-#[rs_object("testing.rust.RsMarker", final)]
+#[rs_object(type_key = "testing.rust.RsMarker", final)]
 pub struct RsMarkerObj {
     base_: RsPointObj,
 }
@@ -88,7 +88,7 @@ pub struct RsMarker {
 }
 
 /// Scalar fields, whose literal arguments to `new` infer as the field types.
-#[rs_object("testing.rust.RsScalars", final)]
+#[rs_object(type_key = "testing.rust.RsScalars", final)]
 pub struct RsScalarsObj {
     base_: Object,
     pub big: i64,
@@ -133,7 +133,11 @@ pub struct TestCxxClassBase {
 }
 
 /// A subtype of a C++ type.
-#[rs_object("testing.rust.RsCxxChild", final, s_eq_hash_kind = "TreeNode")]
+#[rs_object(
+    type_key = "testing.rust.RsCxxChild",
+    final,
+    s_eq_hash_kind = "TreeNode"
+)]
 pub struct RsCxxChildObj {
     base_: TestCxxClassBaseObj,
     pub name: tvm_ffi::String,
@@ -146,7 +150,7 @@ pub struct RsCxxChild {
 }
 
 /// A type that defines its own `__ffi_init__`, over which C++ generates none.
-#[rs_object("testing.rust.RsOwnInit", final, reflection = 
RsOwnInitObj::reflect_methods)]
+#[rs_object(type_key = "testing.rust.RsOwnInit", final, reflection = 
RsOwnInitObj::reflect_methods)]
 pub struct RsOwnInitObj {
     base_: Object,
     pub v: i64,
@@ -173,7 +177,7 @@ impl RsOwnInitObj {
 }
 
 /// `init_off` fields, which `__ffi_init__` leaves all-zero: a value of these 
types.
-#[rs_object("testing.rust.RsInitOff", final)]
+#[rs_object(type_key = "testing.rust.RsInitOff", final)]
 pub struct RsInitOffObj {
     base_: Object,
     pub v: i64,
@@ -189,7 +193,7 @@ pub struct RsInitOff {
 }
 
 /// An `init_off` reference, which all-zero bytes leave undefined: its 
registration fails.
-#[rs_object("testing.rust.RsInitOffReference", final)]
+#[rs_object(type_key = "testing.rust.RsInitOffReference", final)]
 pub struct RsInitOffReferenceObj {
     base_: Object,
     #[reflect(init_off)]
@@ -202,7 +206,7 @@ mod reexported {
     #[allow(dead_code)]
     mod tvm_ffi {}
 
-    #[::tvm_ffi::rs_object("testing.rust.RsReexported", crate = ::tvm_ffi)]
+    #[::tvm_ffi::rs_object(type_key = "testing.rust.RsReexported", crate = 
::tvm_ffi)]
     pub struct ReexportedObj {
         base_: ::tvm_ffi::Object,
         pub v: i64,
diff --git a/rust/tvm-ffi/tests/test_static_init.rs 
b/rust/tvm-ffi/tests/test_static_init.rs
index f1e4d460..0ab1a644 100644
--- a/rust/tvm-ffi/tests/test_static_init.rs
+++ b/rust/tvm-ffi/tests/test_static_init.rs
@@ -77,7 +77,7 @@ mod failing {
     }
 
     tvm_ffi::static_init_block! {
-        Err(Error::new(VALUE_ERROR, "the second block fails", ""))
+        Err(Error::new(VALUE_ERROR, "the second block fails"))
     }
 
     tvm_ffi::static_init_block! {
diff --git a/rust/tvm-ffi/tests/test_string.rs 
b/rust/tvm-ffi/tests/test_string.rs
index 8d8384f5..b7b5fdf6 100644
--- a/rust/tvm-ffi/tests/test_string.rs
+++ b/rust/tvm-ffi/tests/test_string.rs
@@ -287,7 +287,7 @@ fn test_bytes_debug() {
     let data = b"hello";
     let bytes = Bytes::from(data.as_slice());
     let debug_str = format!("{:?}", bytes);
-    assert!(debug_str.contains("Bytes"));
+    assert_eq!(debug_str, format!("{:?}", data.as_slice()));
 }
 
 #[test]
@@ -295,8 +295,7 @@ fn test_string_debug() {
     let input = "world";
     let s = String::from(input);
     let debug_str = format!("{:?}", s);
-    assert!(debug_str.contains("String"));
-    assert!(debug_str.contains("world"));
+    assert_eq!(debug_str, "\"world\"");
 }
 
 #[test]
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs 
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index cd2fb8a0..6d981d0a 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -551,9 +551,9 @@ fn 
default_mutation_mode_preserves_ownership_and_unchanged_results() {
         ) -> Result<UnchangedOr<Any>> {
             let mode = self.mode;
             if self.increment {
-                mutator.default_mutate(self, value.with_inplace_mode(mode))
+                mutator.default_mutate(self, value.narrow_inplace_mode(mode))
             } else {
-                let result = mutator.default_mutate(self, 
value.with_inplace_mode(mode))?;
+                let result = mutator.default_mutate(self, 
value.narrow_inplace_mode(mode))?;
                 assert!(result.is_unchanged());
                 Ok(result)
             }
@@ -597,7 +597,7 @@ fn 
borrowed_capabilities_cannot_enable_low_level_default_reuse() {
                 ObjectArc::strong_count(<Array<i64> as 
ObjectRefCore>::data(&source)),
                 1
             );
-            let value = value.with_inplace_mode(InplaceMode::Allow);
+            let value = value.narrow_inplace_mode(InplaceMode::Allow);
             assert_eq!(value.inplace_mode(), InplaceMode::Disallow);
             let result = mutator.default_mutate(self, value)?;
             let result = Array::<i64>::try_from(
@@ -685,7 +685,7 @@ fn owned_entry_mode_is_forwarded_by_generated_callbacks() {
                 let mut owner: Any = child.into();
                 let alias = self.shared.then(|| owner.clone());
                 let count = if self.shared { 2 } else { 1 };
-                let capability = MutationView::from(&mut 
owner).with_inplace_mode(self.mode);
+                let capability = MutationView::from(&mut 
owner).narrow_inplace_mode(self.mode);
                 assert_eq!(capability.inplace_mode(), self.mode);
                 assert_eq!(capability.as_any_view().debug_strong_count(), 
Some(count));
                 assert_eq!(
@@ -804,7 +804,7 @@ fn 
consuming_callbacks_forward_permissions_without_temporary_owners() {
             });
             self.observe(&value);
             mutator
-                .default_mutate(self, value.with_inplace_mode(self.requested))
+                .default_mutate(self, 
value.narrow_inplace_mode(self.requested))
                 .into_mutate_result()
         }
         fn mutate_integer(
@@ -871,7 +871,7 @@ fn 
consuming_callbacks_forward_permissions_without_temporary_owners() {
     assert_eq!(a.size, 1);
     assert_eq!(a.inplace_mode(), Allow);
     assert_eq!(a.as_any_view().debug_strong_count(), Some(1));
-    let mut value = value.with_inplace_mode(Disallow);
+    let mut value = value.narrow_inplace_mode(Disallow);
     let b = value.field(|node| &node.b);
     assert_eq!(b.inplace_mode(), Disallow);
     assert_eq!(b.as_any_view().debug_strong_count(), Some(1));
@@ -965,7 +965,7 @@ fn 
consuming_default_descent_preserves_unchanged_and_propagates_errors() {
         ) -> Result<UnchangedOr<Any>> {
             if value.as_any_view().downcast_ref::<i64>().copied().is_some() {
                 return if self.0 {
-                    Err(Error::new(RUNTIME_ERROR, "child failed", ""))
+                    Err(Error::new(RUNTIME_ERROR, "child failed"))
                 } else {
                     Ok(UnchangedOr::<Any>::unchanged())
                 };
@@ -1131,14 +1131,11 @@ fn 
callback_errors_preserve_message_and_add_object_context() {
     unsafe impl Send for VisitErrorContext {}
     // SAFETY: as for `Send`.
     unsafe impl Sync for VisitErrorContext {}
-    let cause = Error::new(RUNTIME_ERROR, "cause", "");
-    let source = Error::new_with_cause_and_extra_context(
-        RUNTIME_ERROR,
-        "callback failed",
-        "origin",
-        Some(&cause),
-        Some(&VisitErrorContext::try_from(context.clone()).unwrap()),
-    );
+    let cause = Error::new(RUNTIME_ERROR, "cause");
+    let source = Error::new(RUNTIME_ERROR, "callback failed")
+        .with_backtrace("origin")
+        .with_cause(&cause)
+        
.with_extra_context(&VisitErrorContext::try_from(context.clone()).unwrap());
     let mut errors = Vec::new();
     for order in [WalkOrder::PreOrder, WalkOrder::PostOrder] {
         let leaf_error = structural_map(
@@ -1258,12 +1255,12 @@ fn map_errors_name_the_replaced_or_rebuilt_node() {
                     if order == WalkOrder::PreOrder {
                         Ok(value.into())
                     } else {
-                        Err(Error::new(RUNTIME_ERROR, "failed", ""))
+                        Err(Error::new(RUNTIME_ERROR, "failed"))
                     }
                 },
                 |value: i64| -> Result<i64> {
                     if order == WalkOrder::PreOrder {
-                        Err(Error::new(RUNTIME_ERROR, "failed", ""))
+                        Err(Error::new(RUNTIME_ERROR, "failed"))
                     } else {
                         Ok(value + 1)
                     }
@@ -1345,7 +1342,7 @@ fn 
shared_map_callback_error_preserves_source_and_reports_object_context() {
     let error = match structural_map(
         source.clone(),
         |_integer: i64| -> Result<i64> {
-            Err(Error::new(RUNTIME_ERROR, "map mapper failed", "origin"))
+            Err(Error::new(RUNTIME_ERROR, "map mapper 
failed").with_backtrace("origin"))
         },
         WalkOrder::PostOrder,
     ) {
@@ -2324,7 +2321,7 @@ fn mutation_descent_preserves_ownership_and_markers() {
             if self.retain {
                 self.retained = Some(Any::from(value.as_any_view()));
             }
-            let result = mutator.default_mutate(self, 
value.with_inplace_mode(self.mode))?;
+            let result = mutator.default_mutate(self, 
value.narrow_inplace_mode(self.mode))?;
             if !self.increment {
                 assert!(result.is_unchanged());
             }
@@ -2450,7 +2447,7 @@ fn mutation_regions_retargeting_and_error_restore() {
                 assert_eq!(mutator.def_region_kind(), Use);
                 let error: Result<()> = mutator.with_def_region_kind(Pattern, 
|mutator| {
                     assert_eq!(mutator.def_region_kind(), Pattern);
-                    Err(Error::new(RUNTIME_ERROR, "handler region error", ""))
+                    Err(Error::new(RUNTIME_ERROR, "handler region error"))
                 });
                 assert!(error
                     .unwrap_err()
@@ -2497,7 +2494,7 @@ fn mutation_regions_retargeting_and_error_restore() {
                     })?;
                     let error: Result<()> = mutator.with_def_region_kind(Use, 
|mutator| {
                         assert_eq!(mutator.def_region_kind(), Pattern);
-                        Err(Error::new(RUNTIME_ERROR, "scoped error", ""))
+                        Err(Error::new(RUNTIME_ERROR, "scoped error"))
                     });
                     assert!(error.is_err());
                     assert_eq!(mutator.def_region_kind(), Pattern);
diff --git a/rust/tvm-ffi/tests/test_structural_visit.rs 
b/rust/tvm-ffi/tests/test_structural_visit.rs
index 706157be..76a2b985 100644
--- a/rust/tvm-ffi/tests/test_structural_visit.rs
+++ b/rust/tvm-ffi/tests/test_structural_visit.rs
@@ -29,7 +29,7 @@ use tvm_ffi::{
 };
 
 fn runtime_error(message: &str) -> Error {
-    Error::new(RUNTIME_ERROR, message, "")
+    Error::new(RUNTIME_ERROR, message)
 }
 
 // Nested recursion restores regions after normal, interrupt, error and panic 
exits.
diff --git a/rust/tvm-ffi/tests/test_structural_visit_base.rs 
b/rust/tvm-ffi/tests/test_structural_visit_base.rs
index f2ffba63..488b8512 100644
--- a/rust/tvm-ffi/tests/test_structural_visit_base.rs
+++ b/rust/tvm-ffi/tests/test_structural_visit_base.rs
@@ -61,7 +61,7 @@ impl Drop for CountingBase {
 unsafe impl StructuralVisitorBase for CountingBase {
     fn new() -> Result<Self> {
         if FAIL_INIT.get() {
-            return Err(Error::new(RUNTIME_ERROR, "base initialization failed", 
""));
+            return Err(Error::new(RUNTIME_ERROR, "base initialization 
failed"));
         }
         Ok(Self {
             prefix: StructuralVisitorObj::default(),
@@ -318,7 +318,7 @@ fn base_failure_and_owned_results_release_exactly_once() {
     drop(result);
     assert_eq!(payload.debug_strong_count(), Some(1));
 
-    let error = Error::new(RUNTIME_ERROR, "base descent failed", "");
+    let error = Error::new(RUNTIME_ERROR, "base descent failed");
     let owner = Any::from(error.clone());
     BASE_RESULT.with_borrow_mut(|slot| *slot = Some(Err(error)));
     let result = visit(&Array::from_vec(vec![-1_i64, 99]), Unmatched)
@@ -366,7 +366,7 @@ fn 
callback_outcomes_preserve_reborrows_and_owners_through_specialized_defaults(
     for fail in [false, true] {
         DROPPED.with_borrow_mut(Vec::clear);
         let payload = Any::from(Array::from_vec(vec![42_i64]));
-        let error = Error::new(RUNTIME_ERROR, "callback failed", "");
+        let error = Error::new(RUNTIME_ERROR, "callback failed");
         let error_owner = Any::from(error.clone());
         let mut visitor = Halt {
             result: Some(if fail {
diff --git a/tests/python/test_stubgen_rust.py 
b/tests/python/test_stubgen_rust.py
index edfbbecc..68db1b8d 100644
--- a/tests/python/test_stubgen_rust.py
+++ b/tests/python/test_stubgen_rust.py
@@ -384,33 +384,23 @@ pub struct PairObj {{
 
 impl PairObj {{
     pub fn a(&self) -> Result<i64> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| FieldGetter::new(PairObj::type_index(), 
"a"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(PairObj, "a").get(self)
     }}
 
     pub fn tag(&self) -> Result<Option<String>> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| FieldGetter::new(PairObj::type_index(), 
"tag"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(PairObj, "tag").get(self)
     }}
 
     pub fn items(&self) -> Result<Array<Any>> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| FieldGetter::new(PairObj::type_index(), 
"items"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(PairObj, "items").get(self)
     }}
 
     pub fn owner(&self) -> Result<ObjectRef> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| FieldGetter::new(PairObj::type_index(), 
"owner"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(PairObj, "owner").get(self)
     }}
 
     pub fn r#type(&self) -> Result<Any> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| FieldGetter::new(PairObj::type_index(), 
"type"));
-        GETTER.clone()?.get_any(self)
+        cached_field_getter!(PairObj, "type").get_any(self)
     }}
 }}"""
 
@@ -432,10 +422,8 @@ def test_render_root_object() -> None:
     assert _uses(imports) == {
         "tvm_ffi::Object",
         "tvm_ffi::ObjectArc",
-        "tvm_ffi::ObjectCore",
-        "tvm_ffi::reflection::FieldGetter",
+        "tvm_ffi::cached_field_getter",
         "tvm_ffi::Result",
-        "tvm_ffi::extra::cold_init_lazy_lock::ColdInitLazyLock",
         "tvm_ffi::String",
         "tvm_ffi::Array",
         "tvm_ffi::Any",
@@ -447,7 +435,7 @@ def test_render_object_without_fields_has_no_impl() -> None:
     text, imports = _render(_info("demo.Marker", is_final=True))
     assert '#[object(type_key = "demo.Marker", final)]' in text
     assert "impl MarkerObj" not in text
-    assert "FieldGetter" not in _uses(imports)
+    assert "tvm_ffi::cached_field_getter" not in _uses(imports)
 
 
 def test_render_thread_safe_only_when_not_mutable() -> None:
@@ -591,7 +579,7 @@ impl TryFrom<i64> for IterVarType {{
     type Error = Error;
     fn try_from(value: i64) -> Result<Self> {{
         i32::try_from(value).map(Self).map_err(|_| {{
-            Error::new(VALUE_ERROR, &format!("IterVarType value {{value}} does 
not fit i32"), "")
+            Error::new(VALUE_ERROR, &format!("IterVarType value {{value}} does 
not fit i32"))
         }})
     }}
 }}
@@ -615,34 +603,24 @@ impl Deref for IterVarObj {{
 
 impl IterVarObj {{
     pub fn dom(&self) -> Result<Option<Range>> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| 
FieldGetter::new(IterVarObj::type_index(), "dom"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(IterVarObj, "dom").get(self)
     }}
 
     pub fn var(&self) -> Result<PrimVar> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| 
FieldGetter::new(IterVarObj::type_index(), "var"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(IterVarObj, "var").get(self)
     }}
 
     pub fn iter_type(&self) -> Result<IterVarType> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| 
FieldGetter::new(IterVarObj::type_index(), "iter_type"));
-        let raw: i64 = GETTER.clone()?.get(self)?;
+        let raw: i64 = cached_field_getter!(IterVarObj, 
"iter_type").get(self)?;
         IterVarType::try_from(raw)
     }}
 
     pub fn thread_tag(&self) -> Result<String> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| 
FieldGetter::new(IterVarObj::type_index(), "thread_tag"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(IterVarObj, "thread_tag").get(self)
     }}
 
     pub fn span(&self) -> Result<Option<Span>> {{
-        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =
-            ColdInitLazyLock::new(|| 
FieldGetter::new(IterVarObj::type_index(), "span"));
-        GETTER.clone()?.get(self)
+        cached_field_getter!(IterVarObj, "span").get(self)
     }}
 }}
 
@@ -932,8 +910,7 @@ def test_reflected_base_and_data_keep_their_names() -> None:
     RUST.add_directive(imports, "visibility", "demo.Node.cache_ -> 
pub(crate)", 1)
     text, _ = _render(_info("demo.Node", (_field("base", "int"), 
_field("cache_", "int"))), imports)
     assert (
-        "pub fn base(&self) -> Result<i64> {\n        static GETTER: 
ColdInitLazyLock<Result<FieldGetter>> ="
-        in text
+        'pub fn base(&self) -> Result<i64> {\n        
cached_field_getter!(NodeObj, "base")' in text
     )
     assert "    pub(crate) fn cache_(&self) -> Result<i64> {" in text
     with pytest.raises(ValueError, match=r"`visibility` directive at line 2"):
@@ -954,7 +931,7 @@ def test_render_complete_enum_field() -> None:
     assert "    pub kind: Kind,\n" in text
     assert "    pub fn new(a: i64, kind: impl Into<Kind>) -> Self {" in text
     assert {"tvm_ffi::Result", "tvm_ffi::Error", "tvm_ffi::VALUE_ERROR"} <= 
_uses(imports)
-    assert "tvm_ffi::reflection::FieldGetter" not in _uses(imports)
+    assert "tvm_ffi::cached_field_getter" not in _uses(imports)
 
 
 def test_allow_missing_docs_and_closed_enum() -> None:
@@ -983,7 +960,7 @@ def test_allow_missing_docs_and_closed_enum() -> None:
         "        match value {\n"
         "            0 => Ok(Self::A),\n"
         "            -1 => Ok(Self::B),\n"
-        '            _ => Err(Error::new(VALUE_ERROR, &format!("Kind has no 
value {value}"), "")),\n'
+        '            _ => Err(Error::new(VALUE_ERROR, &format!("Kind has no 
value {value}"))),\n'
         "        }\n"
     ) in text
     # The enum, the object struct, the wrapper and both allocator `impl`s.
@@ -1448,7 +1425,7 @@ def test_registry_complete_chain_is_mirrored() -> None:
         in base
     )
     assert "assert!(::core::mem::size_of::<TestCxxClassBaseObj>() == 40);" in 
base
-    assert "FieldGetter" not in base
+    assert "cached_field_getter" not in base
     assert "    pub fn new(v_i64: i64, v_i32: i32) -> Self {" in base
 
     dd, imports = _render(
@@ -1547,9 +1524,7 @@ def test_cli_init_generates_a_module_tree(tmp_path: Path, 
monkeypatch: pytest.Mo
     assert (root / "mod.rs").read_text(encoding="utf-8") == "pub mod 
testing;\n"
     text = (root / "testing" / "mod.rs").read_text(encoding="utf-8")
     assert text.startswith("#![allow(dead_code, unused_imports)]\n")
-    assert (
-        "use tvm_ffi::reflection::FieldGetter;" in text
-    )  # the opaque fixtures read through getters
+    assert "use tvm_ffi::cached_field_getter;" in text  # the opaque fixtures 
read through getters
     assert '#[object(type_key = "testing.TestCxxClassDerivedDerived"' in text
     # Without a directive for the narrow `v_i32`, the chain is opaque.
     assert (
@@ -1891,9 +1866,7 @@ def 
test_ffi_field_type_off_the_default_ty_map_reads_as_any() -> None:
     text, imports = _render(_info("demo.Doc", (_field("interrupt", 
"ffi.VisitInterrupt"),)))
     assert (
         "    pub fn interrupt(&self) -> Result<Any> {\n"
-        "        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =\n"
-        '            ColdInitLazyLock::new(|| 
FieldGetter::new(DocObj::type_index(), "interrupt"));\n'
-        "        GETTER.clone()?.get_any(self)\n"
+        '        cached_field_getter!(DocObj, "interrupt").get_any(self)\n'
     ) in text
     assert not any("VisitInterrupt" in path for path in _uses(imports))
 
@@ -1913,9 +1886,7 @@ def 
test_any_field_of_an_opaque_type_reads_through_get_any() -> None:
     text, _ = _render(_info("demo.Literal", (_field("value", "Any"),)))
     assert (
         "    pub fn value(&self) -> Result<Any> {\n"
-        "        static GETTER: ColdInitLazyLock<Result<FieldGetter>> =\n"
-        '            ColdInitLazyLock::new(|| 
FieldGetter::new(LiteralObj::type_index(), "value"));\n'
-        "        GETTER.clone()?.get_any(self)\n"
+        '        cached_field_getter!(LiteralObj, "value").get_any(self)\n'
     ) in text
 
 

Reply via email to