Skip to content

Managing Object References

UniFFI interfaces represent instances of objects that have methods and contain state. One of Rust's core innovations is its ability to provide compile-time guarantees about working with such instances, including:

  • Ensuring that each instance has a unique owner responsible for disposing of it.
  • Ensuring that there is only a single writer or multiple readers of an object active at any point in the program.
  • Guarding against data races.

The very nature of the problems UniFFI tries to solve is that calls may come from foreign languages on any thread, outside of the control of Rust's ownership system. UniFFI itself tries to take a hands-off approach as much as possible and depends on the Rust compiler itself to uphold safety guarantees, without assuming that foreign-language callers will be "well behaved".

Concurrency

UniFFI's hands-off approach means that all object instances exposed by UniFFI must be safe to access concurrently. In Rust terminology, they must be Send+Sync and must be useable without taking any &mut references.

Typically this will mean that the Rust implementation of an object uses some of Rust's data structures for thread-safe interior mutability, such as a Mutex or RwLock or the types from std::atomic. The precise details are completely up to the author of the component - as much as possible, UniFFI tries to stay out of your way, simply requiring that the object implementation is Send+Sync and letting the Rust compiler ensure that this is so.

Lifetimes

In order to allow for instances to be used as flexibly as possible from foreign-language code, UniFFI wraps all object instances in an Arc and leverages their reference-count based lifetimes, allowing UniFFI to largely stay out of handling lifetimes entirely for these objects.

When constructing a new object, UniFFI is able to add the Arc automatically, because it knows that the return type of the Rust constructor must be a new uniquely-owned struct of the corresponding type.

When you want to return object instances from functions or methods, or store object instances as fields in records, the underlying Rust code will need to work with Arc<T> directly, to ensure that the code behaves in the way that UniFFI expects.

When accepting instances as arguments, the underlying Rust code can choose to accept it as an Arc<T> or as the underlying struct T, as there are different use-cases for each scenario.

For example, given a interface definition like this:

interface TodoList {
    constructor();
    void add_item(string todo);
    sequence<string> get_items();
};

On the Rust side of the generated bindings: - The instance constructor will create an instance of the corresponding TodoList Rust struct - The owned value is wrapped in an Arc<> - The Arc<> is lowered into the foreign code using Arc::into_raw and returned as an object pointer.

This is the "arc to pointer" dance. Note that this has "leaked" the Arc<> reference out of Rusts ownership system and given it to the foreign-language code. The foreign-language code must pass that pointer back into Rust in order to free it, or our instance will leak.

When invoking a method on the instance: - The foreign-language code passes the raw pointer back to the Rust code, conceptually passing a "borrow" of the Arc<> to the Rust scaffolding. - The Rust side calls Arc::from_raw to convert the pointer into an an Arc<> - It wraps the Arc in std::mem::ManuallyDrop<>, which we never actually drop. This is because the Rust side is borrowing the Arc and shouldn't run the destructor and decrement the reference count. - The Arc<> is cloned and passed to the Rust code

Finally, when the foreign-language code frees the instance, it passes the raw pointer a special destructor function so that the Rust code can drop that initial reference (and if that happens to be the final reference, the Rust object will be dropped.). This simply calls Arc::from_raw, then lets the value drop.

Passing instances as arguments and returning them as values works similarly, except that UniFFI does not automatically wrap/unwrap the containing Arc.

To see this in action, use cargo expand to see the exact generated code.