How Leptos avoids cloning variables moved into closures?

I asked a question about explicit cloning and closures a few days ago and turns out there's a way around it.

As shown in the example bellow, the reactive-graph library of the Leptos framework managed to pass variables without the Copy trait such as String to closures without explicitly cloning them.

use reactive_graph::{owner::ArenaItem, signal::ArcRwSignal, traits::Read};

fn callback_mut(mut cb: impl FnMut() + 'static) { cb(); }

fn main() {

    let counter_arena = ArenaItem::<_>::new_with_storage(ArcRwSignal::new("example1".to_owned()));
    callback_mut(move || { println!("Arena 1: {}", counter_arena.try_get_value().unwrap().read().to_string()); });
    callback_mut(move || { println!("Arena 2: {}", counter_arena.try_get_value().unwrap().read().to_string()); });
}

Repo: https://github.com/LiveDuo/rust-reactive-graph-example/blob/master/src/main.rs

In their docs, Leptos says the following:

This is the innovation that allows Leptos to be usable as a Rust UI framework. Traditionally, managing UI state in Rust has been hard, because UI is all about shared mutability. (A simple counter button is enough to see the problem: You need both immutable access to set the text node showing the counter's value, and mutable access in the click handler, and every Rust UI framework is designed around the fact that Rust is designed to prevent exactly that!) Using something like an event handler in Rust traditionally relies on primitives for communicating via shared memory with interior mutability (Rc<RefCell<_>>, Arc<Mutex<_>>) or for shared memory by communicating via channels, either of which often requires explicit .clone()ing to be moved into an event listener. This is kind of fine, but also an enormous inconvenience.

Leptos has always used a form of arena allocation for signals instead. A signal itself is essentially an index into a data structure that's held elsewhere. It's a cheap-to-copy integer type that does not do reference counting on its own, so it can be copied around, moved into event listeners, etc. without explicit cloning.

While the docs kind of mention arena allocators there's no enough info to understand how it works and recreate an example locally without reactive-graph.

Does anyone knows how to build a minimal example without using another crate? Any good reading on arena allocators?

First of all, ArenaItem impls Copy. So it's not really that suprising.

Then new_with_storage effectively put the item into a thread_local and you are passing around the pointer.

The close analogue is that this works as well:


fn callback_mut(mut cb: impl FnMut() + 'static) { cb(); }

fn main() {
    let a = &*Box::leak(Box::new(String::from("hello")));
        
    callback_mut(move || println!("{a}"));
    callback_mut(move || println!("{a}"));
}

It's practially the same thing.

Thanks

I'd guess the thread_local is used for manual memory deallocations?

Another side note is that couldn't find the Box::leak in their source code. Here's my search Code search results · GitHub. Are there other ways to do it without Box::leak?

Ah, the Box::leak is just there to gain a static lifetime in the code snippet, it's not in the actual source code. Putting data into a thread local also makes it live forever (until you manually drop it ofc).

Got it now, cheers