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 41c43379 [EXTRA][Rust] Make structural visiting generic over its
native base (#907)
41c43379 is described below
commit 41c433790bfec09d8b63f7d2c6e07927bc245835
Author: Tianqi Chen <[email protected]>
AuthorDate: Tue Nov 3 22:12:46 2026 -0500
[EXTRA][Rust] Make structural visiting generic over its native base (#907)
Make `StructuralVisitEngine<Base = StructuralVisitorObj>` use the
selected native base for initialization, type identity and default
traversal. The shared engine owns runtime allocation, callback dispatch,
recursive dispatcher reborrows and owning result handling; existing
ordinary visitor entry points keep their behavior.
Add the unsafe `StructuralVisitorBase` contract and a one-pointer
generic visitor view for downstream native state. Prefix-only ordinary
fallback and hook helpers recurse through the installed vtable, so
larger bases do not depend on the ordinary engine's dispatcher offset.
Borrowed bases cannot safely become owning visitor handles, and
native-mutated fields use interior mutability.
Expose borrowed ABI access and one unsafe raw hook entry point so
downstream visitor bases can call registered hooks without another
adapter.
---
rust/tvm-ffi/src/any.rs | 4 +-
rust/tvm-ffi/src/extra/structural_visit.rs | 500 ++++++++++++++---------
rust/tvm-ffi/src/lib.rs | 4 +-
rust/tvm-ffi/tests/test_structural_visit_base.rs | 374 +++++++++++++++++
4 files changed, 688 insertions(+), 194 deletions(-)
diff --git a/rust/tvm-ffi/src/any.rs b/rust/tvm-ffi/src/any.rs
index f565b86f..47c97b79 100644
--- a/rust/tvm-ffi/src/any.rs
+++ b/rust/tvm-ffi/src/any.rs
@@ -79,8 +79,10 @@ impl<'a> AnyView<'a> {
self.data.type_index
}
+ /// Borrow the raw ABI representation.
+ /// Copying it does not retain referenced data or extend its lifetime.
#[inline(always)]
- pub(crate) fn as_raw_ffi_any(&self) -> &TVMFFIAny {
+ pub fn as_raw_ffi_any(&self) -> &TVMFFIAny {
&self.data
}
diff --git a/rust/tvm-ffi/src/extra/structural_visit.rs
b/rust/tvm-ffi/src/extra/structural_visit.rs
index 152dfdcc..adf812c1 100644
--- a/rust/tvm-ffi/src/extra/structural_visit.rs
+++ b/rust/tvm-ffi/src/extra/structural_visit.rs
@@ -67,6 +67,7 @@
//
// Local Perf note comments explain individual implementation choices.
+use std::cell::UnsafeCell;
use std::marker::PhantomData;
use super::marker::{Borrowed, Owned};
@@ -121,7 +122,7 @@ struct StructuralVisitorVTable {
pub struct StructuralVisitorObj {
base: Object,
vtable: *const StructuralVisitorVTable,
- def_region_mode: i32,
+ def_region_mode: UnsafeCell<i32>,
}
/// Native `ffi.VisitInterrupt` object containing the interruption payload.
@@ -151,7 +152,7 @@ const _: () = {
impl StructuralVisitorObj {
const TYPE_KEY: &'static str = "ffi.StructuralVisitor";
- fn type_index() -> i32 {
+ fn native_type_index() -> i32 {
static TYPE_INDEX: ColdInitLazyLock<i32> = ColdInitLazyLock::new(||
unsafe {
let key =
TVMFFIByteArray::from_str(StructuralVisitorObj::TYPE_KEY);
let mut type_index = 0;
@@ -168,11 +169,79 @@ impl StructuralVisitorObj {
}
}
-// The dispatcher is a raw reborrow handle; its storage belongs to the outer
call.
-// The runtime contains only the fixed ABI base and the dispatcher reborrow.
+/// Native state and default traversal for [`StructuralVisitEngine`].
+/// The engine owns allocation, scoped dispatch and results.
+///
+/// # Safety
+///
+/// `Self` must have a complete initialized C layout with
[`StructuralVisitorObj`]
+/// at offset zero, matching its registered native type and every field hooks
access.
+/// Native-mutated fields require interior mutability; shared references must
not
+/// freeze them during recursion. Hooks must preserve the vtable and valid
region state.
+///
+/// The base must not implement [`crate::ObjectCore`] or otherwise allow
callers
+/// to retain the runtime. Hooks may access the runtime and borrowed traversal
+/// state only synchronously, on the calling thread.
+///
+/// `default_visit` returns one owning ABI None/Error/VisitInterrupt and
recurses only
+/// through the installed vtable, without a whole-base mutable reference
spanning
+/// recursion. Failures use that result carrier; panics must not cross native
callbacks.
+pub unsafe trait StructuralVisitorBase: Sized {
+ /// Initialize the complete base before the engine installs its runtime
header.
+ fn new() -> Result<Self>;
+
+ /// Native type registered for this complete base layout.
+ fn type_index() -> i32;
+
+ /// Descend from a borrowed value without dispatching that value again.
+ ///
+ /// # Safety
+ /// `runtime` is this live initialized base with a matching installed
vtable.
+ /// The input and runtime remain borrowed for this synchronous call. The
+ /// caller has published the current exclusive dispatcher reborrow.
+ unsafe fn default_visit(runtime: *mut Self, value: AnyView<'_>) ->
TVMFFIAny;
+}
+
+// SAFETY: this prefix exactly matches the ordinary native visitor. Its mutable
+// region is interior mutable, and it exposes no safe ownership conversion.
+unsafe impl StructuralVisitorBase for StructuralVisitorObj {
+ #[inline(always)]
+ fn new() -> Result<Self> {
+ Ok(Self::default())
+ }
+
+ fn type_index() -> i32 {
+ Self::native_type_index()
+ }
+
+ #[inline(always)]
+ unsafe fn default_visit(runtime: *mut Self, value: AnyView<'_>) ->
TVMFFIAny {
+ Self::default_visit_raw(runtime, value)
+ }
+}
+
+/// Scoped Rust visitor runtime with a statically selected native base.
+///
+/// Enter through [`IntoVisitor::visit_root`]. The engine owns the complete
base
+/// and the dispatcher reborrow handle, while the dispatcher stays in the outer
+/// call. Engines and their owning handles are never exposed to callbacks.
+///
+/// A downstream layer selects its base in a thin entry point; its handlers use
+/// `StructuralVisitorView<'_, D, Base>` for recursion and base-state
inspection:
+///
+/// ```
+/// use tvm_ffi::{AnyView, IntoVisitor, Result, StructuralVisitorBase,
VisitInterrupt};
+///
+/// fn visit_with_base<'a, Base: StructuralVisitorBase, Marker>(
+/// root: impl Into<AnyView<'a>>,
+/// visitor: impl IntoVisitor<Marker, Base>,
+/// ) -> Result<Option<VisitInterrupt>> {
+/// visitor.visit_root(root.into())
+/// }
+/// ```
#[repr(C)]
-pub(super) struct StructuralVisitEngine {
- base: StructuralVisitorObj,
+pub struct StructuralVisitEngine<Base = StructuralVisitorObj> {
+ base: Base,
dispatcher: *mut c_void,
}
@@ -182,7 +251,7 @@ impl Default for StructuralVisitorObj {
Self {
base: Object::new(),
vtable: std::ptr::null(),
- def_region_mode: DefRegionKind::None as i32,
+ def_region_mode: UnsafeCell::new(DefRegionKind::None as i32),
}
}
}
@@ -250,16 +319,42 @@ impl VisitInterrupt {
/// Recursive visiting and default child descent for a dispatcher.
///
/// Pass the handler's `self` to visit methods to recurse with the same state.
-pub struct StructuralVisitorView<'a, D> {
+pub struct StructuralVisitorView<'a, D, Base = StructuralVisitorObj> {
runtime: StructuralVisitorHandle,
- _scope: PhantomData<(&'a mut (), fn(&mut D), Rc<()>)>,
+ _scope: PhantomData<(&'a mut (), fn(&mut D) -> Base, Rc<()>)>,
}
-impl<D> StructuralVisitorView<'_, D> {
+impl<D, Base: StructuralVisitorBase> StructuralVisitorView<'_, D, Base> {
+ /// Inspect the base state without retaining the scoped visitor runtime.
+ ///
+ /// The reference cannot outlive its view:
+ ///
+ /// ```compile_fail
+ /// use tvm_ffi::{StructuralVisitorObj, StructuralVisitorView};
+ /// fn retain<D>(view: &StructuralVisitorView<'_, D>) -> &'static
StructuralVisitorObj {
+ /// view.base()
+ /// }
+ /// ```
+ ///
+ /// The prefix cannot be converted into an owning FFI value:
+ ///
+ /// ```compile_fail
+ /// use tvm_ffi::{Any, StructuralVisitorView};
+ /// fn retain<D>(view: &StructuralVisitorView<'_, D>) -> Any {
+ /// Any::from(view.base())
+ /// }
+ /// ```
+ #[inline(always)]
+ pub fn base(&self) -> &Base {
+ // SAFETY: the unsafe base contract permits shared inspection,
including
+ // native mutation through interior-mutable fields during recursion.
+ unsafe { &*self.runtime.cast::<Base>() }
+ }
+
#[inline(always)]
pub fn def_region_kind(&self) -> DefRegionKind {
// SAFETY: engine scopes and trusted ABI hooks store valid enum values.
- unsafe { std::mem::transmute((*self.runtime).def_region_mode) }
+ unsafe { std::mem::transmute(*(*self.runtime).def_region_mode.get()) }
}
/// Run operations under a scoped definition region.
@@ -270,7 +365,7 @@ impl<D> StructuralVisitorView<'_, D> {
kind: DefRegionKind,
callback: impl FnOnce(&mut Self) -> R,
) -> R {
- StructuralVisitEngine::with_visitor_def_region(self.runtime, kind, ||
callback(self))
+ StructuralVisitorObj::with_def_region_kind(self.runtime, kind, ||
callback(self))
}
/// Visit a value through the dispatcher.
@@ -283,11 +378,13 @@ impl<D> StructuralVisitorView<'_, D> {
// SAFETY: the view ties this runtime to D; the check establishes the
// instance, and publishing uses the caller's actual exclusive
reborrow.
unsafe {
-
visit_result_from_any(Any::from_raw_ffi_any(StructuralVisitEngine::visit_raw(
- self.runtime,
- dispatch,
- *value.as_raw_ffi_any(),
- )))
+ visit_result_from_any(Any::from_raw_ffi_any(
+ StructuralVisitEngine::<Base>::visit_raw(
+ self.runtime,
+ dispatch,
+ *value.as_raw_ffi_any(),
+ ),
+ ))
}
}
@@ -305,10 +402,10 @@ impl<D> StructuralVisitorView<'_, D> {
// SAFETY: type/layout and scope come from the private view
constructor;
// the supplied borrow must identify this runtime's dispatcher
instance.
unsafe {
- let context =
StructuralVisitEngine::checked_dispatcher(self.runtime, dispatch);
+ let context =
StructuralVisitEngine::<Base>::checked_dispatcher(self.runtime, dispatch);
let value: AnyView<'input> = value.into();
visit_result_from_any(Any::from_raw_ffi_any(
- StructuralVisitEngine::default_visit_raw(
+ StructuralVisitEngine::<Base>::default_visit_raw(
self.runtime,
context,
*value.as_raw_ffi_any(),
@@ -319,22 +416,22 @@ impl<D> StructuralVisitorView<'_, D> {
}
/// Conversion into a dispatcher-driven visitor. Closures belong to
structural_walk.
-pub trait IntoVisitor<Marker> {
+pub trait IntoVisitor<Marker, Base = StructuralVisitorObj> {
#[doc(hidden)]
fn visit_root(self, root: AnyView<'_>) -> Result<Option<VisitInterrupt>>;
}
-impl<D> IntoVisitor<Owned> for D
+impl<D, Base: StructuralVisitorBase> IntoVisitor<Owned, Base> for D
where
for<'s, 'v, 'c, 'r> &'s mut D: crate::Dispatcher<
- (AnyView<'v>, &'c mut StructuralVisitorView<'r, D>),
+ (AnyView<'v>, &'c mut StructuralVisitorView<'r, D, Base>),
Option<Result<Option<VisitInterrupt>>>,
>,
{
#[inline(always)]
fn visit_root(mut self, root: AnyView<'_>) ->
Result<Option<VisitInterrupt>> {
visit_result_from_any(unsafe {
- Any::from_raw_ffi_any(StructuralVisitEngine::run_visitor_raw::<D>(
+
Any::from_raw_ffi_any(StructuralVisitEngine::<Base>::run_visitor_raw::<D>(
*root.as_raw_ffi_any(),
&mut self,
))
@@ -342,17 +439,17 @@ where
}
}
-impl<D> IntoVisitor<Borrowed> for &mut D
+impl<D, Base: StructuralVisitorBase> IntoVisitor<Borrowed, Base> for &mut D
where
for<'s, 'v, 'c, 'r> &'s mut D: crate::Dispatcher<
- (AnyView<'v>, &'c mut StructuralVisitorView<'r, D>),
+ (AnyView<'v>, &'c mut StructuralVisitorView<'r, D, Base>),
Option<Result<Option<VisitInterrupt>>>,
>,
{
#[inline(always)]
fn visit_root(self, root: AnyView<'_>) -> Result<Option<VisitInterrupt>> {
visit_result_from_any(unsafe {
- Any::from_raw_ffi_any(StructuralVisitEngine::run_visitor_raw::<D>(
+
Any::from_raw_ffi_any(StructuralVisitEngine::<Base>::run_visitor_raw::<D>(
*root.as_raw_ffi_any(),
self,
))
@@ -430,7 +527,7 @@ fn visit_result_raw(result: Result<Option<VisitInterrupt>>,
raw: TVMFFIAny) -> T
Ok(Some(interrupt)) => unsafe {
Any::into_raw_ffi_any(interrupt.into()) },
Err(error) => {
core::hint::cold_path();
- StructuralVisitEngine::with_visit_context_raw(
+ StructuralVisitorObj::with_visit_context_raw(
unsafe { Any::into_raw_ffi_any(error.into()) },
raw,
)
@@ -555,7 +652,7 @@ unsafe fn visit_field_level<B>(
}
//
-----------------------------------------------------------------------------
-// StructuralVisitEngine implementation
+// Prefix-only default traversal and native ABI operations
//
-----------------------------------------------------------------------------
// Perf note: Raw carriers avoid materializing an owning Any on the success
path.
@@ -565,7 +662,21 @@ unsafe fn visit_field_level<B>(
// Raw bits provide no ownership guard: each `_raw` result carries one owned
ABI
// value. Forward it exactly once or import it into Any before any operation
// that can unwind or discard it, including destruction of an observer.
-impl StructuralVisitEngine {
+impl StructuralVisitorObj {
+ /// Ordinary registered-hook or reflected-field descent through a visitor
prefix.
+ /// Children reenter the installed vtable, including a specialized base's
vtable.
+ /// Returns one owning None, Error or VisitInterrupt ABI value.
+ ///
+ /// # Safety
+ /// The prefix and input must remain valid for synchronous descent. The
caller
+ /// must publish the current dispatcher reborrow and uphold the native-hook
+ /// lifetime, layout and ownership contract of [`StructuralVisitorBase`].
This
+ /// function accesses only the prefix, never a Rust engine's dispatcher
slot.
+ #[inline(always)]
+ pub unsafe fn default_visit_raw(runtime: *mut Self, value: AnyView<'_>) ->
TVMFFIAny {
+ Self::visit_children_raw(runtime, *value.as_raw_ffi_any())
+ }
+
/// Consume owning `result` from descent, adding borrowed `value` context
on Error.
/// Internal callers supply valid tokens and keep the input live during
annotation;
/// nonerrors transfer unchanged, and Error ownership is guarded through
formatting.
@@ -593,16 +704,6 @@ impl StructuralVisitEngine {
}
}
- /// Recover the Rust engine whose first field is this ABI base.
- ///
- /// # Safety
- /// The handle must belong to the live, synchronous Rust traversal that
created
- /// this exact repr(C) engine. An arbitrary native visitor is not such an
engine.
- #[inline(always)]
- unsafe fn from_handle(handle: StructuralVisitorHandle) -> *mut Self {
- handle.cast()
- }
-
/// Descend through the native hook or reflected fields using this runtime.
///
/// The dispatcher field must already hold the caller's actual reborrow.
@@ -669,7 +770,7 @@ impl StructuralVisitEngine {
(*metadata).structural_eq_hash_kind
}
};
- let kind = std::mem::transmute::<i32,
DefRegionKind>((*runtime).def_region_mode);
+ let kind = std::mem::transmute::<i32,
DefRegionKind>(*(*runtime).def_region_mode.get());
let def_region_kind = free_var_child_region(kind, seq_hash_kind);
let object = unsafe { value.data_union.v_obj } as *mut u8;
let halted = unsafe {
@@ -714,13 +815,162 @@ impl StructuralVisitEngine {
let borrowed = AnyView::from(&child);
let child_region = field_def_region(field, inherited_region);
with_field_error_context(
- Self::with_visitor_def_region(runtime, child_region, || {
+ Self::with_def_region_kind(runtime, child_region, || {
Any::from_raw_ffi_any(Self::call_visitor_raw(runtime,
*borrowed.as_raw_ffi_any()))
}),
field.name.as_str(),
)
}
+ /// Invoke the live visitor vtable on borrowed `value`, returning one
owning token.
+ /// Internal callers keep runtime/input valid and publish the dispatcher
reborrow.
+ /// None bypasses dispatch; native callbacks report failures without
unwinding.
+ #[inline(always)]
+ fn call_visitor_raw(visitor: StructuralVisitorHandle, value: TVMFFIAny) ->
TVMFFIAny {
+ if value.type_index == TVMFFITypeIndex::kTVMFFINone as i32 {
+ core::hint::cold_path();
+ return TVMFFIAny::new();
+ }
+ unsafe {
+ let callback = (*(*visitor).vtable).visit;
+ callback(visitor, AnyView::from_raw_ffi_any(value))
+ }
+ }
+
+ /// Invoke a native or packed hook with borrowed `visitor`, `value` and
`hook`.
+ /// Return one owning None/Error/VisitInterrupt; packed results are
checked.
+ ///
+ /// # Safety
+ /// The runtime and both borrowed inputs must be valid and live for this
call;
+ /// the inputs transfer no ownership. Publish the current exclusive
dispatcher
+ /// reborrow first. A native hook must be a non-null function with the
visit ABI
+ /// accepting this runtime's complete layout. All hooks must obey
+ /// [`StructuralVisitorBase`]'s synchronous, same-thread, non-escaping
contract.
+ #[inline(always)]
+ pub unsafe fn call_structural_visit_hook_raw(
+ visitor: *mut Self,
+ value: TVMFFIAny,
+ hook: TVMFFIAny,
+ ) -> TVMFFIAny {
+ unsafe {
+ if hook.type_index == TVMFFITypeIndex::kTVMFFIOpaquePtr as i32 {
+ let pointer = hook.data_union.v_ptr;
+ // Native pointer registration is an unsafe contract: a
non-null
+ // callback with this ABI, borrowed runtime lifetime and
owning result.
+ let hook: FStructuralVisit = std::mem::transmute(pointer);
+ hook(visitor, AnyView::from_raw_ffi_any(value))
+ } else {
+ core::hint::cold_path();
+ Self::call_structural_visit_hook_slow_raw(
+ visitor,
+ value.type_index,
+ value.small_str_len,
+ value.data_union,
+ hook.type_index,
+ hook.small_str_len,
+ hook.data_union,
+ )
+ }
+ }
+ }
+
+ /// Reconstruct borrowed value/hook from their scalar fields for a packed
call.
+ /// Internal callers keep `visitor` and both inputs live; no input
transfers ownership.
+ /// Validate the owning packed result and release temporary owners before
exporting
+ /// one raw token. Private C-unwind preserves Rust cleanup and AArch64
register
+ /// returns without changing the public or native callback ABI.
+ // Keep packed calls and malformed registrations out of the native-hook
hot path.
+ // Outline uncommon packed-call scratch to keep native visit frames small.
+ // Match the native raw-result ABI (registers on AArch64), without adding
+ // an abort boundary around Rust packed-call cleanup.
+ #[cold]
+ #[inline(never)]
+ extern "C-unwind" fn call_structural_visit_hook_slow_raw(
+ visitor: StructuralVisitorHandle,
+ value_type_index: i32,
+ value_small_str_len: u32,
+ value_data: TVMFFIAnyDataUnion,
+ hook_type_index: i32,
+ hook_small_str_len: u32,
+ hook_data: TVMFFIAnyDataUnion,
+ ) -> TVMFFIAny {
+ // Construct addressable packed-call arguments only on this cold path.
+ let value = TVMFFIAny {
+ type_index: value_type_index,
+ small_str_len: value_small_str_len,
+ data_union: value_data,
+ };
+ let hook = TVMFFIAny {
+ type_index: hook_type_index,
+ small_str_len: hook_small_str_len,
+ data_union: hook_data,
+ };
+ // Packed hooks (including Python class methods) can return any
well-formed
+ // Any through safe APIs. Their result tag needs validation before
decoding.
+ let result = unsafe {
+
Function::try_from(AnyView::from_raw_ffi_any(hook)).and_then(|function| {
+ // Borrow the live runtime for this synchronous hook; it may
not retain it.
+ let object = visitor.cast::<TVMFFIObject>();
+ let mut runtime = TVMFFIAny::new();
+ runtime.type_index = (*object).type_index;
+ runtime.data_union.v_obj = object;
+ function.call_packed(&[
+ AnyView::from_raw_ffi_any(runtime),
+ AnyView::from_raw_ffi_any(value),
+ ])
+ })
+ };
+ let result = match result {
+ Ok(value) => checked_hook_result(value),
+ Err(error) => error.into(),
+ };
+ unsafe { Any::into_raw_ffi_any(result) }
+ }
+
+ /// Run `callback` under requested `kind` on live borrowed `visitor`,
preserving
+ /// Pattern precedence. Restore prior region on return or Rust unwind.
Internal
+ /// callers keep the runtime live and retain no reference to its region
field.
+ #[inline(always)]
+ fn with_def_region_kind<T>(
+ visitor: StructuralVisitorHandle,
+ kind: DefRegionKind,
+ callback: impl FnOnce() -> T,
+ ) -> T {
+ unsafe {
+ let previous = *(*visitor).def_region_mode.get();
+ if previous == kind as i32 || previous == DefRegionKind::Pattern
as i32 {
+ // Inherited descent does not change state; Pattern keeps
precedence.
+ return callback();
+ }
+ *(*visitor).def_region_mode.get() = kind as i32;
+ struct Restore {
+ visitor: StructuralVisitorHandle,
+ previous: i32,
+ }
+ impl Drop for Restore {
+ #[inline(always)]
+ fn drop(&mut self) {
+ unsafe { *(*self.visitor).def_region_mode.get() =
self.previous };
+ }
+ }
+ let _restore = Restore { visitor, previous };
+ callback()
+ }
+ }
+}
+
+// Generic runtime: only this implementation projects the dispatcher after
Base.
+impl<Base: StructuralVisitorBase> StructuralVisitEngine<Base> {
+ /// Recover the Rust engine whose first field is this ABI base.
+ ///
+ /// # Safety
+ /// The handle must belong to the live, synchronous Rust traversal that
created
+ /// this exact repr(C) engine. An arbitrary native visitor is not such an
engine.
+ #[inline(always)]
+ unsafe fn from_handle(handle: StructuralVisitorHandle) -> *mut Self {
+ handle.cast()
+ }
+
/// Publish the current exclusive `dispatch` reborrow and visit borrowed
`raw`.
/// Return one owning raw None/interrupt/Error token.
///
@@ -735,7 +985,7 @@ impl StructuralVisitEngine {
) -> TVMFFIAny {
let context = Self::checked_dispatcher(runtime, dispatch);
(*Self::from_handle(runtime)).dispatcher = context;
- Self::call_visitor_raw(runtime, raw)
+ StructuralVisitorObj::call_visitor_raw(runtime, raw)
}
/// Check runtime identity and return the current `dispatch` reborrow
pointer.
@@ -764,7 +1014,7 @@ impl StructuralVisitEngine {
fn vtable<D>() -> &'static StructuralVisitorVTable
where
for<'s, 'v, 'c, 'r> &'s mut D: crate::Dispatcher<
- (AnyView<'v>, &'c mut StructuralVisitorView<'r, D>),
+ (AnyView<'v>, &'c mut StructuralVisitorView<'r, D, Base>),
Option<Result<Option<VisitInterrupt>>>,
>,
{
@@ -786,7 +1036,7 @@ impl StructuralVisitEngine {
) -> TVMFFIAny
where
for<'s, 'v, 'c, 'r> &'s mut D: crate::Dispatcher<
- (AnyView<'v>, &'c mut StructuralVisitorView<'r, D>),
+ (AnyView<'v>, &'c mut StructuralVisitorView<'r, D, Base>),
Option<Result<Option<VisitInterrupt>>>,
>,
{
@@ -822,7 +1072,7 @@ impl StructuralVisitEngine {
#[inline(always)]
unsafe fn visitor_context<'a, D>(
runtime: StructuralVisitorHandle,
- ) -> StructuralVisitorView<'a, D> {
+ ) -> StructuralVisitorView<'a, D, Base> {
StructuralVisitorView {
runtime,
_scope: PhantomData,
@@ -844,7 +1094,10 @@ impl StructuralVisitEngine {
// Context is the supplied exclusive reborrow. Publish it before
descent;
// the handle-only scanner never creates or uses a competing D
reference.
(*Self::from_handle(runtime)).dispatcher = context;
- Self::with_visit_context_raw(Self::visit_children_raw(runtime, raw),
raw)
+ StructuralVisitorObj::with_visit_context_raw(
+ Base::default_visit(runtime.cast(),
AnyView::from_raw_ffi_any(raw)),
+ raw,
+ )
}
/// Run the public visit/walk adapter with borrowed `root` and exclusive
`dispatch`.
@@ -854,17 +1107,21 @@ impl StructuralVisitEngine {
pub(super) fn run_visitor_raw<D>(root: TVMFFIAny, dispatch: &mut D) ->
TVMFFIAny
where
for<'s, 'v, 'c, 'r> &'s mut D: crate::Dispatcher<
- (AnyView<'v>, &'c mut StructuralVisitorView<'r, D>),
+ (AnyView<'v>, &'c mut StructuralVisitorView<'r, D, Base>),
Option<Result<Option<VisitInterrupt>>>,
>,
{
- let type_index = StructuralVisitorObj::type_index();
+ let base = match Base::new() {
+ Ok(base) => base,
+ Err(error) => return unsafe { Any::into_raw_ffi_any(error.into())
},
+ };
+ let type_index = Base::type_index();
// The managed runtime may borrow its dispatcher only for this call.
// Trusted hooks release all temporary handles before the owner is
dropped.
let mut engine = unsafe {
ObjectArc::<Object>::new_opaque_unchecked(
StructuralVisitEngine {
- base: StructuralVisitorObj::default(),
+ base,
dispatcher: dispatcher_context(dispatch),
},
type_index,
@@ -875,154 +1132,15 @@ impl StructuralVisitEngine {
// Only raw field projections cross recursion; no whole-engine borrow
survives.
unsafe {
(*handle).vtable = Self::vtable::<D>();
- (*handle).def_region_mode = DefRegionKind::None as i32;
+ *(*handle).def_region_mode.get() = DefRegionKind::None as i32;
}
// Keep the result owned while releasing the runtime, including on
unwind.
- let result = unsafe {
Any::from_raw_ffi_any(Self::call_visitor_raw(handle, root)) };
+ let result =
+ unsafe {
Any::from_raw_ffi_any(StructuralVisitorObj::call_visitor_raw(handle, root)) };
// Trusted hooks have released every temporary strong/weak runtime
reference.
drop(engine);
unsafe { Any::into_raw_ffi_any(result) }
}
-
- /// Invoke the live visitor vtable on borrowed `raw`, returning one owning
token.
- /// Internal callers keep runtime/input valid and publish the dispatcher
reborrow.
- /// None bypasses dispatch; native callbacks report failures without
unwinding.
- #[inline(always)]
- fn call_visitor_raw(visitor: StructuralVisitorHandle, raw: TVMFFIAny) ->
TVMFFIAny {
- if raw.type_index == TVMFFITypeIndex::kTVMFFINone as i32 {
- core::hint::cold_path();
- return TVMFFIAny::new();
- }
- unsafe {
- let callback = (*(*visitor).vtable).visit;
- callback(visitor, AnyView::from_raw_ffi_any(raw))
- }
- }
-
- /// Invoke registered borrowed `attr` on live `visitor` and borrowed `raw`.
- /// Internal callers uphold hook registration and scoped lifetimes. Return
one
- /// owning token, using packed validation when the attribute is not a
native pointer.
- #[inline(always)]
- fn call_structural_visit_hook_raw(
- visitor: StructuralVisitorHandle,
- raw: TVMFFIAny,
- attr: TVMFFIAny,
- ) -> TVMFFIAny {
- unsafe {
- if attr.type_index == TVMFFITypeIndex::kTVMFFIOpaquePtr as i32 {
- let pointer = attr.data_union.v_ptr;
- // Native pointer registration is an unsafe contract: a
non-null
- // callback with this ABI, borrowed runtime lifetime and
owning result.
- let hook: FStructuralVisit = std::mem::transmute(pointer);
- hook(visitor, AnyView::from_raw_ffi_any(raw))
- } else {
- core::hint::cold_path();
- Self::call_structural_visit_hook_slow_raw(
- visitor,
- raw.type_index,
- raw.small_str_len,
- raw.data_union,
- attr.type_index,
- attr.small_str_len,
- attr.data_union,
- )
- }
- }
- }
-
- /// Reconstruct borrowed value/attribute from their scalar fields for a
packed call.
- /// Internal callers keep `visitor` and both inputs live; no input
transfers ownership.
- /// Validate the owning packed result and release temporary owners before
exporting
- /// one raw token. Private C-unwind preserves Rust cleanup and AArch64
register
- /// returns without changing the public or native callback ABI.
- // Keep packed calls and malformed registrations out of the native-hook
hot path.
- // Outline uncommon packed-call scratch to keep native visit frames small.
- // Match the native raw-result ABI (registers on AArch64), without adding
- // an abort boundary around Rust packed-call cleanup.
- #[cold]
- #[inline(never)]
- extern "C-unwind" fn call_structural_visit_hook_slow_raw(
- visitor: StructuralVisitorHandle,
- value_type_index: i32,
- value_small_str_len: u32,
- value_data: TVMFFIAnyDataUnion,
- attr_type_index: i32,
- attr_small_str_len: u32,
- attr_data: TVMFFIAnyDataUnion,
- ) -> TVMFFIAny {
- // Construct addressable packed-call arguments only on this cold path.
- let raw = TVMFFIAny {
- type_index: value_type_index,
- small_str_len: value_small_str_len,
- data_union: value_data,
- };
- let attr = TVMFFIAny {
- type_index: attr_type_index,
- small_str_len: attr_small_str_len,
- data_union: attr_data,
- };
- // Packed hooks (including Python class methods) can return any
well-formed
- // Any through safe APIs. Their result tag needs validation before
decoding.
- let result = unsafe {
-
Function::try_from(AnyView::from_raw_ffi_any(attr)).and_then(|function| {
- function.call_packed(&[
- Self::borrowed_visitor_view(visitor),
- AnyView::from_raw_ffi_any(raw),
- ])
- })
- };
- let result = match result {
- Ok(value) => checked_hook_result(value),
- Err(error) => error.into(),
- };
- unsafe { Any::into_raw_ffi_any(result) }
- }
-
- /// Borrow `visitor` as an object-valued packed-call argument without
retaining it.
- ///
- /// # Safety
- /// The handle must point to a live visitor object and remain valid for
the returned
- /// view lifetime; the view may not escape the synchronous invocation.
- #[inline(always)]
- unsafe fn borrowed_visitor_view<'a>(visitor: StructuralVisitorHandle) ->
AnyView<'a> {
- let object = visitor.cast::<TVMFFIObject>();
- let mut raw = TVMFFIAny::new();
- raw.type_index = (*object).type_index;
- raw.small_str_len = 0;
- raw.data_union.v_obj = object;
- AnyView::from_raw_ffi_any(raw)
- }
-
- /// Run `callback` under requested `kind` on live borrowed `visitor`,
preserving
- /// Pattern precedence. Restore prior region on return or Rust unwind.
Internal
- /// callers keep the runtime live and retain no reference to its region
field.
- #[inline(always)]
- fn with_visitor_def_region<T>(
- visitor: StructuralVisitorHandle,
- kind: DefRegionKind,
- callback: impl FnOnce() -> T,
- ) -> T {
- unsafe {
- let previous = (*visitor).def_region_mode;
- if previous == kind as i32 || previous == DefRegionKind::Pattern
as i32 {
- // Inherited descent does not change state; Pattern keeps
precedence.
- return callback();
- }
- (*visitor).def_region_mode = kind as i32;
- struct Restore {
- visitor: StructuralVisitorHandle,
- previous: i32,
- }
- impl Drop for Restore {
- #[inline(always)]
- fn drop(&mut self) {
- unsafe { (*self.visitor).def_region_mode = self.previous };
- }
- }
- let _restore = Restore { visitor, previous };
- callback()
- }
- }
}
//
-----------------------------------------------------------------------------
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 495e0995..8336cb2e 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -60,8 +60,8 @@ pub use crate::extra::structural_mutate::{
StructuralMutatorView, StructuralVarRemap,
};
pub use crate::extra::structural_visit::{
- structural_visit, DefRegionKind, IntoVisitor, StructuralVisitorObj,
StructuralVisitorView,
- VisitInterrupt, VisitInterruptObj,
+ structural_visit, DefRegionKind, IntoVisitor, StructuralVisitEngine,
StructuralVisitorBase,
+ StructuralVisitorObj, StructuralVisitorView, VisitInterrupt,
VisitInterruptObj,
};
pub use crate::extra::structural_walk::{
structural_walk, IntoWalkResult, IntoWalker, WalkChainLink, WalkOrder,
WalkResult,
diff --git a/rust/tvm-ffi/tests/test_structural_visit_base.rs
b/rust/tvm-ffi/tests/test_structural_visit_base.rs
new file mode 100644
index 00000000..d385dad9
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_structural_visit_base.rs
@@ -0,0 +1,374 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use std::cell::{Cell, RefCell};
+use std::sync::LazyLock;
+use tvm_ffi::tvm_ffi_sys::{
+ TVMFFIAny, TVMFFIByteArray, TVMFFIGetTypeInfo, TVMFFIObject,
TVMFFITypeGetOrAllocIndex,
+ TVMFFITypeMetadata, TVMFFITypeRegisterMetadata,
+};
+use tvm_ffi::{
+ dispatch, get_type_attr, structural_visit, Any, AnyView, Array,
DefRegionKind, Dispatcher,
+ Error, Function, IntoVisitor, Result, StructuralVisitEngine,
StructuralVisitorBase,
+ StructuralVisitorObj, StructuralVisitorView, VisitInterrupt, RUNTIME_ERROR,
+};
+
+thread_local! {
+ static DROPPED: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
+ static FAIL_INIT: Cell<bool> = const { Cell::new(false) };
+ static BASE_RESULT: RefCell<Option<Result<Option<VisitInterrupt>>>> =
const { RefCell::new(None) };
+}
+
+// This base deliberately moves the engine dispatcher well beyond the ordinary
+// prefix. Its private fields have a registered native size but no owning
wrapper.
+#[repr(C)]
+struct CountingBase {
+ prefix: StructuralVisitorObj,
+ cookie: [u64; 8],
+ defaults: Cell<usize>,
+}
+
+impl Drop for CountingBase {
+ fn drop(&mut self) {
+ assert_eq!(self.cookie, [0xfeed_dead_beef; 8]);
+ DROPPED.with_borrow_mut(|drops| drops.push(self.defaults.get()));
+ }
+}
+
+// SAFETY: the C prefix is at offset zero, the complete size is registered, and
+// the only mutable field uses Cell. No safe API exposes an owning runtime. The
+// reused ordinary hooks obey the synchronous visitor contract and use its
vtable.
+unsafe impl StructuralVisitorBase for CountingBase {
+ fn new() -> Result<Self> {
+ if FAIL_INIT.get() {
+ return Err(Error::new(RUNTIME_ERROR, "base initialization failed",
""));
+ }
+ Ok(Self {
+ prefix: StructuralVisitorObj::default(),
+ cookie: [0xfeed_dead_beef; 8],
+ defaults: Cell::new(0),
+ })
+ }
+
+ fn type_index() -> i32 {
+ static INDEX: LazyLock<i32> = LazyLock::new(|| unsafe {
+ let parent = StructuralVisitorObj::type_index();
+ let depth = (*TVMFFIGetTypeInfo(parent)).type_depth + 1;
+ let index = TVMFFITypeGetOrAllocIndex(
+ &TVMFFIByteArray::from_str("test.CountingVisitor"),
+ -1,
+ depth,
+ 0,
+ 0,
+ parent,
+ );
+ assert!(index >= 0);
+ assert_eq!(
+ TVMFFITypeRegisterMetadata(
+ index,
+ &TVMFFITypeMetadata {
+ doc: TVMFFIByteArray::from_str(""),
+ creator: None,
+ total_size: std::mem::size_of::<CountingBase>() as i32,
+ structural_eq_hash_kind: 0,
+ }
+ ),
+ 0
+ );
+ index
+ });
+ *INDEX
+ }
+
+ unsafe fn default_visit(runtime: *mut Self, value: AnyView<'_>) ->
TVMFFIAny {
+ assert_eq!(
+ (*runtime.cast::<TVMFFIObject>()).type_index,
+ Self::type_index()
+ );
+ (*runtime).defaults.set((*runtime).defaults.get() + 1);
+ // A default with observable behavior: redirect 10 to children 11 and
12.
+ if value.try_as::<i64>() == Some(10) {
+ let children = Array::new(vec![11_i64, 12]);
+ return StructuralVisitorObj::default_visit_raw(runtime.cast(),
(&children).into());
+ }
+ if value.try_as::<i64>() == Some(-1) {
+ let result = BASE_RESULT.with_borrow_mut(Option::take).unwrap();
+ return Any::into_raw_ffi_any(match result {
+ Ok(None) => Any::new(),
+ Ok(Some(interrupt)) => interrupt.into(),
+ Err(error) => error.into(),
+ });
+ }
+ // Exercise the shared resolved-hook entry as well as ordinary
fallback.
+ if value.try_as::<Array<Any>>().is_some() {
+ let hook = get_type_attr(value.type_index(),
"__s_visit__").unwrap();
+ return StructuralVisitorObj::call_structural_visit_hook_raw(
+ runtime.cast(),
+ *value.as_raw_ffi_any(),
+ *AnyView::from(&hook).as_raw_ffi_any(),
+ );
+ }
+ StructuralVisitorObj::default_visit_raw(runtime.cast(), value)
+ }
+}
+
+type View<'a, D> = StructuralVisitorView<'a, D, CountingBase>;
+
+fn visit<'a, M>(
+ root: impl Into<AnyView<'a>>,
+ visitor: impl IntoVisitor<M, CountingBase>,
+) -> Result<Option<VisitInterrupt>> {
+ visitor.visit_root(root.into())
+}
+
+#[derive(Default)]
+struct Trace {
+ leaves: Vec<(i64, DefRegionKind)>,
+ defaults: Vec<usize>,
+}
+
+#[dispatch(visit, (AnyView<'_>, &mut View<'_, Self>),
Result<Option<VisitInterrupt>>)]
+impl Trace {
+ fn visit_integer(
+ &mut self,
+ value: i64,
+ visitor: &mut View<'_, Self>,
+ ) -> Result<Option<VisitInterrupt>> {
+ self.leaves.push((value, visitor.def_region_kind()));
+ self.defaults.push(visitor.base().defaults.get());
+ match value {
+ 0 => visitor.with_def_region_kind(DefRegionKind::Pattern, |view| {
+ let base = view.base();
+ let before = base.defaults.get();
+ let result = view.default_visit(self, &10_i64);
+ assert_eq!(base.defaults.get(), before + 1);
+ assert_eq!(base.cookie, [0xfeed_dead_beef; 8]);
+ assert_eq!(view.def_region_kind(), DefRegionKind::Pattern);
+ result
+ }),
+ 1 => {
+ visitor.visit(self, &Array::new(vec![2_i64, 3]))?;
+ // Resume the parent dispatcher after native recursion, then
recurse again.
+ self.leaves.push((4, visitor.def_region_kind()));
+ visitor.visit(self, &5_i64)
+ }
+ _ => Ok(None),
+ }
+ }
+}
+
+#[test]
+fn larger_base_defaults_recurse_and_restore_dispatcher_and_region() {
+ DROPPED.with_borrow_mut(Vec::clear);
+ assert!(
+ std::mem::size_of::<StructuralVisitEngine<CountingBase>>()
+ > std::mem::size_of::<StructuralVisitEngine>()
+ );
+ assert_eq!(
+ std::mem::size_of::<View<'_, Trace>>(),
+ std::mem::size_of::<usize>()
+ );
+ let root = Array::new(vec![0_i64, 1, 6]);
+ let mut trace = Trace::default();
+ assert!(visit(&root, &mut trace).unwrap().is_none());
+ use DefRegionKind::{None as Use, Pattern};
+ assert_eq!(
+ trace.leaves,
+ [
+ (0, Use),
+ (11, Pattern),
+ (12, Pattern),
+ (1, Use),
+ (2, Use),
+ (3, Use),
+ (4, Use),
+ (5, Use),
+ (6, Use)
+ ]
+ );
+ assert_eq!(trace.defaults, [1, 2, 2, 2, 3, 3, 3, 3]);
+ DROPPED.with_borrow(|drops| assert_eq!(*drops, [3]));
+}
+
+#[test]
+fn unmatched_scalar_uses_selected_default_and_owned_dispatcher() {
+ struct Empty;
+ impl<Dummy> Dispatcher<Dummy, Option<Result<Option<VisitInterrupt>>>> for
&mut Empty {
+ fn dispatch_tuple(self, _: Dummy) ->
Option<Result<Option<VisitInterrupt>>> {
+ None
+ }
+ }
+ DROPPED.with_borrow_mut(Vec::clear);
+ assert!(visit(&10_i64, Empty).unwrap().is_none());
+ DROPPED.with_borrow(|drops| assert_eq!(*drops, [3]));
+ assert!(structural_visit(&Array::new(vec![1_i64]), Empty)
+ .unwrap()
+ .is_none());
+ assert!(structural_visit(&Array::new(vec![1_i64]), &mut Empty)
+ .unwrap()
+ .is_none());
+}
+
+#[test]
+fn ordinary_reflected_fallback_uses_specialized_vtable() {
+ assert_eq!(
+ unsafe { tvm_ffi::tvm_ffi_sys::TVMFFITestingDummyTarget() },
+ 0
+ );
+ let root = Function::get_global("testing.make_visit_region_graph")
+ .unwrap()
+ .call_tuple((false,))
+ .unwrap();
+ let mut trace = Trace::default();
+ assert!(visit(&root, &mut trace).unwrap().is_none());
+ assert!(trace.leaves.iter().any(|&(value, _)| value == 2));
+ assert!(trace.leaves.iter().any(|&(value, _)| value == 3));
+}
+
+#[test]
+fn actual_dispatcher_identity_is_checked_before_recursive_publication() {
+ struct Nonzero(usize);
+ impl<'a, 'b, 'c>
+ Dispatcher<(AnyView<'a>, &'b mut View<'c, Nonzero>),
Option<Result<Option<VisitInterrupt>>>>
+ for &mut Nonzero
+ {
+ fn dispatch_tuple(
+ self,
+ (_, view): (AnyView<'a>, &'b mut View<'c, Nonzero>),
+ ) -> Option<Result<Option<VisitInterrupt>>> {
+ let mut other = Nonzero(1);
+ let result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ view.default_visit(&mut other, &1_i64)
+ }));
+ assert!(result.is_err());
+ self.0 += 1;
+ Some(Ok(None))
+ }
+ }
+ let mut visitor = Nonzero(0);
+ visit(&1_i64, &mut visitor).unwrap();
+ assert_eq!(visitor.0, 1);
+}
+
+#[test]
+fn base_failure_and_owned_results_release_exactly_once() {
+ struct Unmatched;
+ impl<Args> Dispatcher<Args, Option<Result<Option<VisitInterrupt>>>> for
&mut Unmatched {
+ fn dispatch_tuple(self, _: Args) ->
Option<Result<Option<VisitInterrupt>>> {
+ None
+ }
+ }
+ DROPPED.with_borrow_mut(Vec::clear);
+ FAIL_INIT.set(true);
+ assert!(visit(&1_i64, Unmatched)
+ .err()
+ .unwrap()
+ .to_string()
+ .contains("initialization failed"));
+ FAIL_INIT.set(false);
+ DROPPED.with_borrow(|drops| assert!(drops.is_empty()));
+
+ let payload = Any::from(Array::new(vec![42_i64]));
+ BASE_RESULT
+ .with_borrow_mut(|slot| *slot =
Some(Ok(Some(VisitInterrupt::with(payload.clone())))));
+ let result = visit(&Array::new(vec![-1_i64, 99]), Unmatched)
+ .unwrap()
+ .unwrap();
+ assert_eq!(payload.debug_strong_count(), Some(2));
+ DROPPED.with_borrow(|drops| assert_eq!(*drops, [2]));
+ drop(result);
+ assert_eq!(payload.debug_strong_count(), Some(1));
+
+ 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::new(vec![-1_i64, 99]), Unmatched)
+ .err()
+ .unwrap();
+ assert!(result.to_string().contains("base descent failed"));
+ drop(result);
+ assert_eq!(owner.debug_strong_count(), Some(1));
+ DROPPED.with_borrow(|drops| assert_eq!(*drops, [2, 2]));
+}
+
+#[test]
+fn
callback_outcomes_preserve_reborrows_and_owners_through_specialized_defaults() {
+ struct Halt {
+ result: Option<Result<Option<VisitInterrupt>>>,
+ seen: Vec<i64>,
+ }
+ #[dispatch(visit, (AnyView<'_>, &mut View<'_, Self>),
Result<Option<VisitInterrupt>>)]
+ impl Halt {
+ fn visit_integer(
+ &mut self,
+ value: i64,
+ view: &mut View<'_, Self>,
+ ) -> Result<Option<VisitInterrupt>> {
+ self.seen.push(value);
+ match value {
+ 10 => {
+ let result =
view.with_def_region_kind(DefRegionKind::Pattern, |view| {
+ view.default_visit(self, &10_i64)
+ });
+ assert_eq!(view.def_region_kind(), DefRegionKind::None);
+ // The parent borrow must be usable after either halted
descent.
+ view.visit(self, &99_i64)?;
+ result
+ }
+ 11 => {
+ assert_eq!(view.def_region_kind(), DefRegionKind::Pattern);
+ self.result.take().unwrap()
+ }
+ 99 => Ok(None),
+ _ => panic!("descent continued after the callback halted it"),
+ }
+ }
+ }
+ for fail in [false, true] {
+ DROPPED.with_borrow_mut(Vec::clear);
+ let payload = Any::from(Array::new(vec![42_i64]));
+ let error = Error::new(RUNTIME_ERROR, "callback failed", "");
+ let error_owner = Any::from(error.clone());
+ let mut visitor = Halt {
+ result: Some(if fail {
+ Err(error)
+ } else {
+ drop(error);
+ Ok(Some(VisitInterrupt::with(payload.clone())))
+ }),
+ seen: vec![],
+ };
+ let result = visit(&Array::new(vec![10_i64, 100]), &mut visitor);
+ assert_eq!(visitor.seen, [10, 11, 99]);
+ DROPPED.with_borrow(|drops| assert_eq!(*drops, [2]));
+ if fail {
+ assert!(result
+ .as_ref()
+ .err()
+ .unwrap()
+ .to_string()
+ .contains("callback failed"));
+ } else {
+ assert_eq!(payload.debug_strong_count(), Some(2));
+ }
+ drop(result);
+ assert_eq!(payload.debug_strong_count(), Some(1));
+ assert_eq!(error_owner.debug_strong_count(), Some(1));
+ }
+}