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 value
  • num_copies: Number of RefAny instances sharing the data
  • num_refs: Active shared borrows (Ref<T>)
  • num_mutable_refs: Active mutable borrows (RefMut<T>)
  • type_id / type_name: Runtime type identity for safe downcasts
  • custom_destructor: extern "C" fn(*mut c_void) — runs T::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) returns Option<Ref<'_, U>>: fails on wrong type, or a mutable borrow is live
  • downcast_mut<U>(&mut self) returns Option<RefMut<'_, U>>: fails on wrong type, or any borrow is live
  • replace_contents(new_value) returns bool (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:

  1. RefCount::drop (core/src/refany.rs:185) sees num_copies == 1.
  2. It reclaims RefCountInner via Box::from_raw.
  3. It runs custom_destructor on the data pointer (executes T::drop).
  4. It calls dealloc with 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

Back to guide index