I'm going to show you how to use slab to send handles from rust to python and back to rust using an arena allocator called slab
.
tl;dr
With slab
, we allocate types into a chunk of memory that has been pre-allocated and get an id back. It's really just an index into the chunk of memory, indexed by the size of the type we are using.
We have to have a "global" (static in some way) chuck of this memory to handle the FFI nature of it.
The example code from this article is available here: https://github.com/graysonarts/uniffi-slab-example
Background
On my youtube series "Growing up Rust", I'm building a personal CRM in Rust with a Swift frontend. I'm using CQRS
and an event-driven architecture with the least amount of swift as possible. I'm using UniFFI
to generate the bindings for swift (and in this example python)
To accomplish this, I need the ability to create commands and then submit the command to the core rust model for processing.
I'm handling this part of it by an enum called PersonCommand
which handles all the different types of changes that can happen to a person. I want to treat the PersonCommand
as completely opaque to the swift code. It just needs to know how to create the necessary commands and submit them to the run queue.
Because I want to batch up commands at the dialog boundary, I need swift to be able to hold onto the Command before it's processed, but it doesn't need access to anything inside of the command, so I want it to be opaque. UniFFI
doesn't support this out of the box (that I could find, let me know if I'm wrong!).
The key pieces.
I'm not going to cover setting up UniFFI
so make sure you check their documentation if you need help there.
The global slab
In order for slab to be global, we need to make it a static variable. There are other patterns you could use for this, but this is the simplest.
static COMMAND_SLAB: Lazy<Mutex<Slab<OpaqueType>>>
= Lazy::new(|| Mutex::new(Slab::new()));
Lazy
is from the once_cell
crate. I tried to use the std::cell::LazyCell
but I couldn't get it working and is not a standard feature yet, so I'd rather bring in once_cell
which does work well.
We need the Mutex
because the slab may be accessed from multiple threads. Other kinds of locks would work like a RwLock
but again, Mutex
is simple and straightforward. Your mileage may vary depending on your use case and workload. In a UI application, all commands are generated as a result of user actions, so we should be okay.
The handle type alias
I decided to use a type alias for code clarity, but it's just a u32
. Technically, slab returns a usize
but that can't be marshalled across the FFI, so we cast it to a u32. Adjust as necessary. It might be safer to serialize and deserialize to a string using serde
or some other crate.
type OpaqueHandle = u32;
The factory method
Because it's an opaque type to our FFI, we need to have a function that returns the handle for specific instance of the type.
fn ffi_make_opaque(number: u32) -> OpaqueHandle {
COMMAND_SLAB.lock().unwrap().insert(OpaqueType {
...
}) as OpaqueHandle
}
Accepting the handle
Now if we need to send the handle back into rust, we would do something like this
let slab = COMMAND_SLAB.lock().unwrap();
let command = slab.get(handle as usize).unwrap();
println!("Processing handle: {:?}", command));
In this case, we need to bind the slab to a local variable to control the lifetime, and then we get the command from the slab and do something with it.
Ideally, this function would get the command and then call it's execute function.
Alternative Approaches
If we wanted, we could get the raw pointer to the type and pass that around as a value between the different languages, but I found that it was a messier and more error prone way to handle it than to use slab
.
Top comments (0)