SidecarBox<T> - RAII registration
pub struct SidecarBox<T: AdaptiveInstance> {
handle: SidecarHandle,
inner: Box<T>,
}
impl<T: AdaptiveInstance> SidecarBox<T> {
pub fn new(value: T) -> Self;
pub fn id(&self) -> InstanceId;
pub fn stats(&self) -> Option<InstanceStats>;
}
impl<T: AdaptiveInstance> std::ops::Deref for SidecarBox<T> {
type Target = T;
}SidecarBox::new(primitive) is the one-call way to register a
primitive with the global sidecar and get a value that:
- Derefs to the primitive.
sb.method()callsT::method(). - Tracks its registration.
sb.id()returns theInstanceId;sb.stats()returns a snapshot of the currentInstanceStats. - Auto-unregisters on
Drop. The internalSidecarHandledrops before the innerBox<T>, blocking on any in-flight scan cycle so the sidecar cannot see freed memory.
Field order is load-bearing
pub struct SidecarBox<T: AdaptiveInstance> {
// ORDER MATTERS: handle drops before inner.
handle: SidecarHandle,
inner: Box<T>,
}Rust drops struct fields in declaration order. The handle’s Drop
calls sidecar.unregister(id), which:
- Removes the slot from the registry (subsequent scan iterations skip it).
- Blocks until any currently-executing scan iteration finishes.
By the time handle’s drop returns, no scan thread holds a pointer
into the instance. Then inner: Box<T> drops, freeing the
header / ring memory. Reversing the field order would let the box
drop while a scan thread was mid-read of the header → use-after-free.
How it captures pointers
pub fn new(value: T) -> Self {
let inner = Box::new(value);
let header = NonNull::from(inner.header());
let ring = NonNull::from(inner.ring());
let instance_ref: &dyn AdaptiveInstance = &*inner;
let instance_ptr: *const dyn AdaptiveInstance = instance_ref;
let instance = unsafe {
NonNull::new_unchecked(instance_ptr as *mut dyn AdaptiveInstance)
};
let policy = inner.make_policy();
let sidecar = global();
let id = unsafe { sidecar.register_raw(header, ring, Some(instance), policy) };
Self {
handle: SidecarHandle { id, sidecar },
inner,
}
}The Box::new allocation gives the inner value a stable heap
address. NonNull::from(&field) then captures stable interior
pointers. The Box lifetime keeps those interior pointers valid
for as long as the SidecarBox lives.
Deref makes the wrapper transparent
let sb = SidecarBox::new(SharedHashMap::<u32, u32>::create("/tmp/m.bin", 1024).unwrap());
sb.insert(42, 4242).unwrap(); // -> SharedHashMap::insert
sb.get(&42); // -> SharedHashMap::get
sb.ring(); // -> AdaptiveInstance::ring via SharedHashMap implCalling sb.method() resolves via deref to T’s inherent method
or trait method. AdaptiveInstance itself must be in scope for the
trait methods (header(), ring(), make_policy(),
apply_migration()) to be callable via method syntax.
When SidecarBox doesn’t fit
SidecarBox<T> owns the primitive. For ownership patterns that
need Arc<T> instead (multiple owners, cross-thread sharing
without Send/Sync on the box), use Sidecar::register_raw
directly. The lifetime invariant then moves to the caller: call
Sidecar::unregister(id) before the last Arc drop.
See also
AdaptiveInstance- the traitTmust implement.- Sidecar registry
- what
register_rawandunregisterdo internally. - Compose primitives via
SidecarBoxhow-to for end-to-end patterns.