Understanding RefAny
Introduction
RefAny is a type-erased, atomically reference-counted smart pointer with
runtime borrow checking. Conceptually Arc<RefCell<dyn Any>>, but
#[repr(C)] so the same value moves across the C ABI into Python, C++,
and other bindings. It is the only piece of state plumbing the framework
imposes on you - every piece of callback data, dataset data, timer data, thread data,
and component backreference is a RefAny.
extern crate azul_core;
use azul_core::refany::RefAny;
// plain Rust struct - doesn't necessarily have to be repr(C)
struct Counter {
value: i64
}
// internally uses compiler internals to store the "RefAny" TypeId,
// to uniquely identify the type. Additionally stores metadata such as
// name, JSON serializing callbacks (if applicable), memory size, etc.
// for introspection / debugging
let mut data = RefAny::new(Counter {
value: 0
});
let mut clone = data.clone(); // cheap — bumps a refcount
// refmut "c" object created - bumps refcount
// call only succeeds if no borrow exists
if let Some(mut c) = clone.downcast_mut::<Counter>() {
c.value += 1;
// refmut "c" object dropped - refcount decreased
}
assert_eq!(data.downcast_ref::<Counter>().unwrap().value, 1);
What RefAny stores
RefAny heap-allocates the value once and stores a pointer to it
alongside metadata (type id, type name, destructor, layout, atomic
counters) in a separate heap allocation, the RefCountInner. Every clone
of a RefAny shares both allocations.
Fields on RefCountInner:
_internal_ptr: Pointer to the users heap-allocated valuenum_copies: Number ofRefAnyinstances sharing the datanum_refs: Active shared borrows (Ref<T>)num_mutable_refs: Active mutable borrows (RefMut<T>)type_id/type_name: Runtime type identity for safe downcastscustom_destructor:extern "C" fn(*mut c_void)— runsT::drop_internal_layout_*: Size and alignment captured for deallocation
See core/src/refany.rs for explicit documentation around safety / soundness.
Constructing RefAny
extern crate azul_core;
use azul_core::refany::RefAny;
struct AppData {
user: String,
click_count: u32
}
let data = RefAny::new(AppData {
user: "alice".to_string(),
click_count: 0,
});
RefAny::new<T> records TypeId::of::<T>(), allocates with
Layout::from_size_align(size_of::<T>(), align_of::<T>()), copies the
value onto the heap, and mem::forgets the original to skip its destructor:
the destructor stored in RefCountInner will run later when the last reference
drops. T must be 'static (cannot store references with lifetimes in a RefAny
because the runtime cannot enforce a static lifetime through the C ABI.
Cloning
extern crate azul_core;
use azul_core::refany::RefAny;
let data = RefAny::new(0u32);
let a = data.clone();
let b = data.clone();
assert_eq!(a.get_ref_count(), 3); // original + 2 clones
Clone atomically increments num_copies with SeqCst ordering and
assigns the clone a unique instance_id. No data is copied. A clone
is a few atomic instructions — you can pass RefAnys freely.
Borrowing the inner value
extern crate azul_core;
use azul_core::refany::RefAny;
let mut data = RefAny::new(42i32);
{
let r = data.downcast_ref::<i32>().unwrap(); // shared borrow
assert_eq!(*r, 42);
} // borrow released here
if let Some(mut m) = data.downcast_mut::<i32>() { // mutable borrow
*m = 100;
}
downcast_ref<U>(&mut self)returnsOption<Ref<'_, U>>: fails on wrong type, or a mutable borrow is livedowncast_mut<U>(&mut self)returnsOption<RefMut<'_, U>>: fails on wrong type, or any borrow is livereplace_contents(new_value)returnsbool(success): returns false borrow is live
For debugging, RefAny contains other functions such as get_type_name() and
get_ref_count() to debug why a downcast is failing.
Both downcast_* functions take &mut self so the compiler prevents two
borrows from the same RefAny value. Borrows from different clones of the
same data are guarded by the runtime counters and the Ref / RefMut RAII
guards.
Returning Option instead of panicking is deliberate. A failed downcast
reports a mismatch a callback can react to (return Update::DoNothing)
instead of aborting the process.
Dataset vs Callback RefAny
use azul_core::dom::{Dom, EventFilter, HoverEventFilter};
use azul_core::callbacks::{CallbackInfo, Update};
use azul_core::refany::RefAny;
struct Counter { value: i64 }
struct MyCustomStruct { foo: u32 }
extern "C" fn on_click(mut data: RefAny, info: CallbackInfo) -> Update {
match data.downcast_mut::<Counter>() {
Some(c) => c.value += 1,
None => return Update::DoNothing,
}
let storage = info.get_dataset(info.get_hit_node_id())
.and_then(data.downcast_mut::<MyCustomStruct>());
match storage {
Some(MyCustomStruct { foo }) => println!("{foo}"),
None => return Update::DoNothing,
}
Update::RefreshDom
}
fn build() -> Dom {
let state = RefAny::new(Counter { value: 0 });
Dom::div().with_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
state,
on_click,
)
.with_dataset(RefAny::new(MyCustomStruct {
foo: 0,
}))
}
Dom::with_callback stores the RefAny alongside the callback.
When the event fires, the framework clones the RefAny (cheap),
invokes the extern "C" callback with that clone, and drops the
clone after the callback returns. The original RefAny remains
attached to the node for the next event.
Dom::with_dataset attaches a RefAny without
a callback — useful when child callbacks need to navigate to data
attached to an ancestor.
Memory and threading
RefAny is Send and Sync. The data is heap-allocated and the counters
are AtomicUsize with SeqCst ordering, so a RefAny can be moved or shared
into a Task or Thread. The runtime borrow checker is per-RefAny value,
not per-data — concurrent mutable downcasts on clones of the same RefAny
race because the check-then-increment in downcast_mut is not atomic.
For exclusive mutation across clones, use replace_contents instead, which
uses compare_exchange on num_mutable_refs.
Deallocation is automatic. When the last RefAny clone drops:
RefCount::drop(core/src/refany.rs:185) seesnum_copies == 1.- It reclaims
RefCountInnerviaBox::from_raw. - It runs
custom_destructoron the data pointer (executesT::drop). - It calls
deallocwith the saved layout to release the bytes.
Borrow guards (Ref, RefMut) clone the RefCount, so they keep the
data alive even if the original RefAny is dropped. A guard outliving
its parent is rare but legal.
Common pitfalls
RefMut still holding a reference
downcast_mut holds a runtime mutable borrow until its RefMut guard
drops. If you call a user-supplied callback while still holding it and
the callback re-enters your component (touches the same RefAny), the
inner downcast returns None. The pattern in the example above —
extracting whatever you need into local variables, calling drop(me)
explicitly, then dispatching — keeps the borrow window minimal.
let mut me = data.downcast_mut::<NumberInput>().unwrap();
let parent = me.on_number.as_option().unwrap().clone(); // clone the RefAny
let cb = me.on_number_cb.unwrap();
drop(me); // release the borrow
cb(parent, info, value) // safe to re-enter
Can't store references
A struct that borrows from another value cannot go through RefAny::new.
Either own the data or wrap the borrowed source in a RefAny and clone
that into the child.
Wrong type id
downcast_ref::<Foo>() on a RefAny constructed from Bar returns None,
not a panic. Check the result.
Borrow leaks across threads
A Ref<T> sent to another thread keeps the read borrow alive there;
mutations on any clone block until it returns. Prefer cloning the RefAny
itself across threads and taking borrows locally.
Cycles
Two structs holding RefAny clones of each other will never drop.
Use one direction of backreference only — children point at parents,
never the reverse.
Coming Up Next
- Document Object Model — The Dom tree - node types, hierarchy, and CSS
- Datasets — Attaching state to a node for navigation and per-instance state
- Events — Callbacks, event filters, and how state triggers relayout