DEV Community

Cover image for Opaque Types for UniFFI
Grayson Hay
Grayson Hay

Posted on

Opaque Types for UniFFI

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.


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:


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()));
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)