UniFFI

UniFFI is a tool that automatically generates foreign-language bindings targeting Rust libraries. The repository can be found on github. It fits in the practice of consolidating business logic in a single Rust library while targeting multiple platforms, making it simpler to develop and maintain a cross-platform codebase.
Note that this tool will not help you ship a Rust library to these platforms, but simply not have to write bindings code by hand [0].

Design

UniFFI requires to write an Interface Definition Language (based on WebIDL) file describing the methods and data structures available to the targeted languages.
This .udl (UniFFI Definition Language) file, whose definitions must match with the exposed Rust code, is then used to generate Rust scaffolding code and foreign-languages bindings. This process can take place either during the build process or be manually initiated by the developer.

uniffi diagram

Supported languages

  • Kotlin
  • Swift
  • Python
  • Ruby

What?

We're interested in building re-useable components for sync- and storage-related browser functionality - things like storing and syncing passwords, working with bookmarks and signing in to your Firefox Account.

We want to write the code for these components once, in Rust. We want to easily re-use these components from all the different languages and on all the different platforms for which we build browsers, which currently includes JavaScript for PCs, Kotlin for Android, and Swift for iOS.

And of course, we want to do this in a way that's convenient, maintainable, and difficult to mess up.

How?

In an aspirational world, we could get this kind of easy cross-language interop for free using wasm_bindgen and webassembly interface types - imagine writing an API in Rust, annotating it with some #[wasm_bindgen] macros, compiling it into a webassembly bundle, and being able to import and use that bundle from any target language, complete with a rich high-level API!

That kind of tooling is not available to shipping applications today, but that doesn't mean we can't take a small step in that general direction while the Rust and Wasm ecosystem continues to evolve.

Using UniFFI, you can:

  • Implement your software component as a cdylib crate in Rust; let's say the code is in ./src/lib.rs.
  • Specify the desired component API using an Interface Definition Language (specifically, a variant of WebIDL) in a separate file like ./src/lib.udl.
  • Run uniffi-bindgen scaffolding ./src/lib.udl to generate a bunch of boilerplate rust code that exposes this API as a C-compatible FFI layer, and include it as part of your crate.
  • cargo build your crate as normal to produce a shared library.
  • Run uniffi-bindgen generate ./src/lib.udl -l kotlin to generate a Kotlin library that can load your shared library and expose it to Kotlin code using your nice high-level component API!
    • Or -l swift or -l python to produce bindings for other languages.

Why?

There are plenty of potential ways to solve this problem, and the one that's right for us might not be right for you. You can read a little more about the considerations and trade-offs that lead to the current approach in our Architecture Decision Records, starting with this motivational document.

Why Not?

We hope UniFFI will be useful to you! But if you're considering it for your project then here are some tradeoffs you should keep in mind:

  • UniFFI makes it easy to produce "good enough" bindings into Rust from several different target languages. If you want to call Rust code from just one foreign language, there may be a language-specific bindings tool that provides more features and better performance.
  • The project is evolving fast, because it's being developed concurrently with its primary consumers. You should be prepared for frequent releases and non-trivial API churn (but we'll use semantic versioning to signal breaking changes).
  • UniFFI doesn't provide an end-to-end packaging solution for your Rust code and its generated bindings. For example, it'll help you generate Kotlin bindings to call into Rust, but it won't help you with compiling the Rust code to run on Android or with packaging the bindings into an .aar bundle (but it might be useful as a building-block for such a tool!).

We also have a list of design principles that might help you decide whether this project is a good fit for your needs.

Getting started

Say your company has a simple math crate with the following lib.rs:


#![allow(unused)]
fn main() {
fn add(a: u32, b: u32) -> u32 {
    a + b
}
}

And top brass would like you to expose this business-critical operation to Kotlin and Swift.
Don't panic! We will show you how do that using UniFFI.

Prerequisites

The uniffi-bindgen cli tool

Install the uniffi-bindgen binary on your system using:

cargo install uniffi_bindgen

You can see what it can do with uniffi-bindgen --help, but let's leave it aside for now.

Running from a source checkout

It's also possible to run uniffi-bindgen from a source checkout of uniffi - this might be useful if you are experimenting with changes to uniffi and want to test them out.

In this case, just use cargo run in the uniffi_bindgen crate directory. For example, from the root of the uniffi-rs repo, execute:

% cd uniffi_bindgen/src
% cargo run -- --help

and you will see the help output from running uniffi-bindgen locally. Refer to the docs for cargo run for more information and options.

Build your crate as a cdylib

Ensure your crate builds as a cdylib by adding

crate-type = ["cdylib"]
name = "<library name>"

to your crate's Cargo.toml.

The UDL file

We describe in a UDL (a type of IDL, Interface Definition Language) file what is exposed and available to foreign-language bindings. In this case, we are only playing with primitive types (u32) and not custom data structures but we still want to expose the add method.
Let's create a math.udl file in the math crate's src/ folder:

namespace math {
  u32 add(u32 a, u32 b);
};

Here you can note multiple things:

  • The namespace directive: it will be the name of your Kotlin/Swift package. It must be present in any udl file, even if there ain't any exposed function (e.g. namespace foo {}).
  • The add function is in the namespace block. That's because on the Rust side it is a top-level function, we will see later how to to handle methods.
  • Rust's u32 is also UDL's u32, but it is not always true! (TODO table correspondance)

Note: If any of the things you expose in the udl file do not have an equivalent in your Rust crate, you will get a hard error. Try changing the u32 result type to u64 and see what happens!

Rust scaffolding

Rust scaffolding code

Now we generate some Rust helper code to make the add method available to foreign-language bindings.

First, add uniffi to your crate dependencies; this is the runtime support code that powers UniFFI's serialization of data types across languages:

[dependencies]
uniffi = "0.8"

Important note: the uniffi version must be the same as the uniffi-bindgen command-line tool installed on your system.

Then let's add uniffi_build to your build dependencies: it generates the Rust scaffolding code that exposes our Rust functions as a C-compatible FFI layer.

[build-dependencies]
uniffi_build = "0.8"

Then create a build.rs file next to Cargo.toml that will use uniffi_build:

fn main() {
    uniffi_build::generate_scaffolding("./src/math.udl").unwrap();
}

Note: This is the equivalent of calling (and does it under the hood) uniffi-bindgen scaffolding src/math.udl --out-dir <OUT_DIR>.

Lastly, we include the generated scaffolding code in our lib.rs. If you've used the default build settings then this can be done using a handy macro:


#![allow(unused)]
fn main() {
uniffi_macros::include_scaffolding!("math");
}

If you have generated the scaffolding in a custom location, use the standard include! macro to include the generated file by name, like this:


#![allow(unused)]
fn main() {
include!(concat!(env!("OUT_DIR"), "/math.uniffi.rs"));
}

Note: The file name is always <namespace>.uniffi.rs.

Great! add is ready to see the outside world!

Avoiding version mismatches between uniffi core and uniffi-bindgen

The process above has one significant problem - things start to fall apart if the version of the uniffi core (ie, the version specified in [dependencies]) and the version of uniffi-bindgen (ie, the version installed by cargo install) get out of date - and often these problems are not detected until runtime when the generated rust code actually runs.

The uniffi_build crate supports an alternative workflow via the builtin-bindgen feature. If this feature is enabled, then the uniffi_build crate takes a runtime dependency on the uniffi_bindgen crate - effectively building and running the uniffi-bindgen tool as your crate is being compiled. The uniffi-bindgen tool doesn't need to be installed if this feature is enabled.

The downside of this is that it drives up the build time for your crate (as uniffi-bindgen needs to be built as well), so it's not the default.

To enable this, the [build-dependencies] of your Cargo.toml might look like:

[build-dependencies]
uniffi_build = {version = "0.8", features = [ "builtin-bindgen" ]}

Your build.rs script and everything else should remain the same, but now whatever version of uniffi is specified will be used to perform the (now slightly slower) build.

Rust scaffolding code from a local uniffi

Note: This section is only for people who want to make changes to uniffi itself. If you just want to use uniffi as released you should ignore this section.

The techniques above don't work very well when you are making changes to uniffi itself and want to see how those changes impact your project - there's no released version of uniffi you can reference.

To support this use-case, you can leverage Cargo's support for local dependencies and the builtin-bindgen feature described above. You should:

  • Change the [dependencies] section of your Cargo.toml to point to a local checkout of uniffi core.

  • Change the [build-dependencies] section of your Cargo.toml to point to a local checkout of uniffi_build and enable the builtin-bindgen feature.

For example, you will probably end up with Cargo.toml looking something like:

[dependencies]
uniffi = { path = "path/to/uniffi-rs/uniffi }
...
[build-dependencies]
uniffi_build = { path = "path/to/uniffi-rs/uniffi_build, features=["builtin-bindgen"] }

Note that path/to/uniffi-rs should be the path to the root of the uniffi source tree - ie, the 2 path specs above point to different sub-directories under the uniffi root.

Libraries that depend on UniFFI components

Suppose you want to create a shared library that includes one or more components using UniFFI. The typical way to achieve this is to create a new crate that depends on the component crates. However, this can run into rust-lang#50007. Under certain circumstances, the scaffolding functions that the component crates export do not get re-exported by the dependent crate.

Use the uniffi_reexport_scaffolding! macro to work around this issue. If your library depends on foo_component, then add foo_component::uniffi_reexport_scaffolding!(); to your lib.rs file and UniFFI will add workaround code that forces the functions to be re-exported.

Each scaffolding function contains a hash that's derived from the UDL file. This avoids name collisions when combining multiple UniFFI components into one library.

Foreign-language bindings

As stated in the Overview, this library and tutorial does not cover how to ship a Rust library on mobile, but how to generate bindings for it, so this section will only cover that.

First, make sure you have installed all the prerequisites - particularly, installing uniffi-bindgen (or alternatively, understanding how to run it from the source tree)

Kotlin

Run

uniffi-bindgen generate src/math.udl --language kotlin

then have a look at src/uniffi/math/math.kt

Swift

Run

uniffi-bindgen generate src/math.udl --language swift

then check out src/math.swift

Note that these commands could be integrated as part of your gradle/XCode build process.

This is it, you have an MVP integration of UniFFI in your project.

The UDL file

This file defines which functions, methods and types are exposed to the foreign-language bindings.

namespace sprites {
  Point translate([ByRef] Point position, Vector direction);
};

dictionary Point {
  double x;
  double y;
};

dictionary Vector {
  double dx;
  double dy;
};

interface Sprite {
  constructor(Point? initial_position);
  Point get_position();
  void move_to(Point position);
  void move_by(Vector direction);
};

Namespace

Every UDL file must have a namespace block:

namespace math {
  double exp(double a);
};

It serves multiple purposes:

  • It identifies the name of the generated Rust scaffolding file <namespace>.uniffi.rs.
  • It identifies the package name of the generated foreign-language bindings (e.g. uniffi.<namespace> in Kotlin)
  • It also contains all top-level functions that get exposed to foreign-language bindings.

Built-in types

The following built-in types can be passed as arguments/returned by Rust methods:

Rust typeUDL typeNotes
boolboolean
u8/i8..u64/i64u8/i8..u64/i64
f32float
f64double
Stringstring
SystemTimetimestampPrecision may be lost when converting to Python and Swift types
Duration durationPrecision may be lost when converting to Python and Swift types
&T[ByRef] TThis works for &str and &[T]
Option<T>T?
Vec<T>sequence<T>
HashMap<String, T>record<DOMString, T>Only string keys are supported
()voidEmpty return
Result<T, E>N/ASee Errors section

And of course you can use your own types, which is covered in the following sections.

Enumerations

An enumeration defined in Rust code as


#![allow(unused)]
fn main() {
enum Animal {
    Dog,
    Cat,
}
}

Can be exposed in the UDL file with:

enum Animal {
  "Dog",
  "Cat",
};

Enumerations with associated data require a different syntax, due to the limitations of using WebIDL as the basis for UniFFI's interface language. An enum like this in Rust:


#![allow(unused)]
fn main() {
enum IpAddr {
  V4 {q1: u8, q2: u8, q3: u8, q4: u8},
  V6 {addr: string},
}
}

Can be exposed in the UDL file with:

[Enum]
interface IpAddr {
  V4(u8 q1, u8 q2, u8 q3, u8 q4);
  V6(string addr);
};

Only enums with named fields are supported by this syntax.

Structs/Dictionaries

Dictionaries are how UniFFI represents structured data. They consist of one of more named fields, each of which holds a value of a particular type. Think of them like a Rust struct without any methods.

A Rust struct like this:


#![allow(unused)]
fn main() {
struct TodoEntry {
    done: bool,
    due_date: u64,
    text: String,
}
}

Can be exposed via UniFFI using UDL like this:

dictionary TodoEntry {
    boolean done;
    u64 due_date;
    string text;
};

The fields in a dictionary can be of almost any type, including objects or other dictionaries. The current limitations are:

  • They cannot recursively contain another instance of the same dictionary type.
  • They cannot contain references to callback interfaces.

Fields holding Object References

If a dictionary contains a field whose type is an interface, then that field will hold a reference to an underlying instance of a Rust struct. The Rust code for working with such fields must store them as an Arc in order to help properly manage the lifetime of the instance. So if the UDL interface looked like this:

interface User {
    // Some sort of "user" object that can own todo items
};

dictionary TodoEntry {
    User owner;
    string text;
}

Then the corresponding Rust code would need to look like this:


#![allow(unused)]
fn main() {
struct TodoEntry {
    owner: std::sync::Arc<User>,
    text: String,
}
}

Depending on the language, the foreign-language bindings may also need to be aware of these embedded references. For example in Kotlin, each Object instance must be explicitly destroyed to avoid leaking the underlying memory, and this also applies to Objects stored in record fields.

You can read more about managing object references in the section on interfaces.

Default values for fields

Fields can be specified with a default value:

dictionary TodoEntry {
    boolean done = false;
    string text;
};

The corresponding generated Kotlin code would be equivalent to:

data class TodoEntry (
    var done: Boolean = false,
    var text: String
)  {
    // ...
}

This works for Swift and Python targets too. If not set otherwise the default value for a field is used when constructing the Rust struct.

Optional fields and default values

Fields can be made optional using a T? type.

dictionary TodoEntry {
    boolean done;
    string? text;
};

The corresponding Rust struct would need to look like this:


#![allow(unused)]
fn main() {
struct TodoEntry {
    done: bool,
    text: Option<String>,
}
}

The corresponding generated Kotlin code would be equivalent to:

data class TodoEntry (
    var done: Boolean,
    var text: String?
)  {
    // ...
}

Optional fields can also be set to a default null value:

dictionary TodoEntry {
    boolean done;
    string? text = null;
};

The corresponding generated Kotlin code would be equivalent to:

data class TodoEntry (
    var done: Boolean,
    var text: String? = null
)  {
    // ...
}

This works for Swift and Python targets too.

Functions

All top-level functions get exposed through the UDL's namespace block. For example, if the crate's lib.rs file contains:


#![allow(unused)]
fn main() {
fn hello_world() -> String {
    "Hello World!".to_owned()
}
}

The UDL file will look like:

namespace Example {
    string hello_world();
}

Optional arguments & default values

Function arguments can be marked optional with a default value specified.

In the UDL file:

namespace Example {
    string hello_name(optional string name = "world");
}

The Rust code will take a required argument:


#![allow(unused)]
fn main() {
fn hello_name(name: String) -> String {
    format!("Hello {}", name)
}
}

The generated foreign-language bindings will use function parameters with default values. This works for the Kotlin, Swift and Python targets.

For example the generated Kotlin code will be equivalent to:

fun helloName(name: String = "world" ): String {
    // ...
}

Throwing errors

It is often the case that a function does not return T in Rust but Result<T, E> to reflect that it is fallible.
For UniFFI to expose this error, your error type (E) must be an enum and implement std::error::Error (thiserror works!).

Here's how you would write a Rust failible function and how you'd expose it in UDL:


#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
enum ArithmeticError {
    #[error("Integer overflow on an operation with {a} and {b}")]
    IntegerOverflow { a: u64, b: u64 },
}

fn add(a: u64, b: u64) -> Result<u64, ArithmeticError> {
    a.checked_add(b).ok_or(ArithmeticError::IntegerOverflow { a, b })
}
}

And in UDL:

[Error]
enum ArithmeticError {
  "IntegerOverflow",
};


namespace arithmetic {
  [Throws=ArithmeticError]
  u64 add(u64 a, u64 b);
}

On the other side (Kotlin, Swift etc.), a proper exception will be thrown if Result::is_err() is true.

If you want to expose the assocated data as fields on the exception, use this syntax:

[Enum]
interface ArithmeticError {
  IntegerOverflow(u64 a, u64 b);
};

Interfaces/Objects

Interfaces are represented in the Rust world as a struct with an impl block containing methods. In the Kotlin or Swift world, it's a class.

Because Objects are passed by reference and Dictionaries by value, in the UniFFI world it is impossible to be both an Object and a Dictionary.

The following Rust code:


#![allow(unused)]
fn main() {
struct TodoList {
    items: RwLock<Vec<String>>
}

impl TodoList {
    fn new() -> Self {
        TodoList {
            items: RwLock::<Vec::new())
        }
    }

    fn add_item(&mut self, todo: String) {
        self.items.write().unwrap().push(todo);
    }

    fn get_items(&self) -> Vec<String> {
        self.items.read().unwrap().clone()
    }
}
}

would be exposed using:

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

By convention, the constructor() calls the Rust's new() method.

Conceptually, these interface objects are live Rust structs that have a proxy object on the foreign language side; calling any methods on them, including a constructor or destructor results in the corresponding methods being called in Rust. If you do not specify a constructor the bindings will be unable to create the interface directly.

UniFFI will generate these proxies with an interface or protocol to help with testing in the foreign-language code. For example in Kotlin, the TodoList would generate:

interface TodoListInterface {
    fun addItem(todo: String)
    fun getItems(): List<String>
}

class TodoList : TodoListInterface {
   // implementations to call the Rust code.
}

When working with these objects, it may be helpful to always pass the interface or protocol, but construct the concrete implementation. For example in Swift:

let todoList = TodoList()
todoList.addItem(todo: "Write documentation")
display(list: todoList)

func display(list: TodoListProtocol) {
    let items = list.getItems()
    items.forEach {
        print($0)
    }
}

Following this pattern will make it easier for you to provide mock implementation of the Rust-based objects for testing.

Alternate Named Constructors

In addition to the default constructor connected to the ::new() method, you can specify alternate named constructors to create object instances in different ways. Each such constructor must be given an explicit name, provided in the UDL with the [Name] attribute like so:

interface TodoList {
    // The default constructor makes an empty list.
    constructor();
    // This alternate constructor makes a new TodoList from a list of string items.
    [Name=new_from_items]
    constructor(sequence<string> items)
    ...

For each alternate constructor, UniFFI will expose an appropriate static-method, class-method or similar in the foreign language binding, and will connect it to the Rust method of the same name on the underlying Rust struct.

Managing Shared References

To the foreign-language consumer, UniFFI object instances are designed to behave as much like regular language objects as possible. They can be freely passed as arguments or returned as values, like this:

interface TodoList {
    ...

    // Copy the items from another TodoList into this one.
    void import_items(TodoList other);

    // Make a copy of this TodoList as a new instance.
    TodoList duplicate();
};

To ensure that this is safe, UniFFI allocates every object instance on the heap using Arc, Rust's built-in smart pointer type for managing shared references at runtime.

The use of Arc is transparent to the foreign-language code, but sometimes shows up in the function signatures of the underlying Rust code. For example, the Rust code implementing the TodoList::duplicate method would need to explicitly return an Arc<TodoList>, since UniFFI doesn't know whether it will be returning a new object or an existing one:


#![allow(unused)]
fn main() {
impl TodoList {
    fn duplicate(&self) -> Arc<TodoList> {
        Arc::new(TodoList {
            items: RwLock::new(self.items.read().unwrap().clone())
        })
    }
}
}

By default, object instances passed as function arguments will also be passed as an Arc<T>, so the Rust implementation of TodoList::import_items would also need to accept an Arc<TodoList>:


#![allow(unused)]
fn main() {
impl TodoList {
    fn import_items(&self, other: Arc<TodoList>) {
        self.items.write().unwrap().append(other.get_items());
    }
}
}

If the Rust code does not need an owned reference to the Arc, you can use the [ByRef] UDL attribute to signal that a function accepts a borrowed reference:

interface TodoList {
    ...
    //                  +-- indicate that we only need to borrow the other list
    //                  V
    void import_items([ByRef] TodoList other);
    ...
};

#![allow(unused)]
fn main() {
impl TodoList {
    //                              +-- don't need to care about the `Arc` here
    //                              V
    fn import_items(&self, other: &TodoList) {
        self.items.write().unwrap().append(other.get_items());
    }
}
}

Conversely, if the Rust code explicitly wants to deal with an Arc<T> in the special case of the self parameter, it can signal this using the [Self=ByArc] UDL attribute on the method:

interface TodoList {
    ...
    // +-- indicate that we want the `Arc` containing `self`
    // V
    [Self=ByArc]
    void import_items(TodoList other);
    ...
};

#![allow(unused)]
fn main() {
impl TodoList {
    // `Arc`s everywhere! --+-----------------+
    //                      V                 V
    fn import_items(self: Arc<Self>, other: Arc<TodoList>) {
        self.items.write().unwrap().append(other.get_items());
    }
}
}

You can read more about the technical details in the docs on the internal details of managing object references.

Concurrent Access

Since interfaces represent mutable data, UniFFI has to take extra care to uphold Rust's safety guarantees around shared and mutable references. The foreign-language code may attempt to operate on an interface instance from multiple threads, and it's important that this not violate Rust's assumption that there is at most a single mutable reference to a struct at any point in time.

UniFFI enforces this by requiring that the Rust implementation of an interface be Sync+Send, and you will get a compile-time error if your implementation does not satisfy this requirement. For example, consider a small "counter" object declared like so:

interface Counter {
    constructor();
    void increment();
    u64 get();
};

For this to be safe, the underlying Rust struct must adhere to certain restrictions, and UniFFI's generated Rust scaffolding will emit compile-time errors if it does not.

The Rust struct must not expose any methods that take &mut self. The following implementation of the Counter interface will fail to compile because it relies on mutable references:


#![allow(unused)]
fn main() {
struct Counter {
    value: u64
}

impl Counter {
    fn new() -> Self {
        Self { value: 0 }
    }

    // No mutable references to self allowed in UniFFI interfaces.
    fn increment(&mut self) {
        self.value = self.value + 1;
    }

    fn get(&self) -> u64 {
        self.value
    }
}
}

Implementations can instead use Rust's "interior mutability" pattern. However, they must do so in a way that is both Sync and Send, since the foreign-language code may operate on the instance from multiple threads. The following implementation of the Counter interface will fail to compile because RefCell is not Sync:


#![allow(unused)]
fn main() {
struct Counter {
    value: RefCell<u64>
}

impl Counter {
    fn new() -> Self {
        // `RefCell` is not `Sync`, so neither is `Counter`.
        Self { value: RefCell::new(0) }
    }

    fn increment(&self) {
        let mut value = self.value.borrow_mut();
        *value = *value + 1;
    }

    fn get(&self) -> u64 {
        *self.value.borrow()
    }
}
}

This version uses an AtomicU64 for interior mutability, which is both Sync and Send and hence will compile successfully:


#![allow(unused)]
fn main() {
struct Counter {
    value: AtomicU64
}

impl Counter {
    fn new() -> Self {
        Self { value: AtomicU64::new(0) }
    }

    fn increment(&self) {
        self.value.fetch_add(1, Ordering::SeqCst);
    }

    fn get(&self) -> u64 {
        self.value.load(Ordering::SeqCst)
    }
}
}

You can read more about the technical details in the docs on the internal details of managing object references.

External types

Note: The facility described in this document is not yet available for all foreign language bindings.

UniFFI supports refering to types defined outside of the UDL file. These types must be either:

  1. A locally defined type which wraps a UniFFI primitive type.
  2. A "UniFFI compatible" type in another crate

Specifically, "UniFFI compatible" means either a type defined in udl in an external crate, or a type defined in another crate that satisfies (1).

These types are all declared using a typedef, with attributes specifying how the type is handled. See the links for details.

Declaring External Types

Note: The facility described in this document is not yet available for all foreign language bindings.

It is possible to use types defined by UniFFI in an external crate. For example, let's assume that you have an existing crate named demo_crate with the following UDL:

dictionary DemoDict {
  string string_val,
  boolean bool_val,
};

And further, assume that you have another crate called consuming-crate which would like to use this dictionary. Inside consuming-crate's UDL file you can reference DemoDict by using a typedef with an External attribute, as shown below.

[External="demo-crate"]
typedef extern DemoDict;

// Now define our own dictionary which references the imported type.
dictionary ConsumingDict {
  DemoDict demo_dict,
  boolean another_bool,
};

(Note the special type extern used on the typedef. It is not currently enforced that the literal extern is used, but we hope to enforce this later, so please use it!)

Inside consuming-crate's Rust code you must use that struct as normal - for example, consuming-crate's lib.rs might look like:


#![allow(unused)]
fn main() {
use demo_crate::DemoDict;

pub struct ConsumingDict {
    demo_dict: DemoDict,
    another_bool: bool,
}

include!(concat!(env!("OUT_DIR"), "/consuming_crate.uniffi.rs"));
}

Your Cargo.toml must reference the external crate as normal.

The External attribute can be specified on dictionaries, enums and errors.

Custom types

Custom types allow you to extend the UniFFI type system to support types from your Rust crate or 3rd party libraries. This relies on a builtin UDL type move data across the FFI, followed by a conversion to your custom type.

Custom types in the scaffolding code

Consider the following trivial Rust abstraction for a "handle" which wraps an integer:


#![allow(unused)]
fn main() {
pub struct Handle(i64);
}

You can use this type in your udl by declaring it via a typedef with a Custom attribute, defining the builtin type that it's based on.

[Custom]
typedef i64 Handle;

For this to work, your Rust code must also implement a special trait named UniffiCustomTypeConverter. This trait is generated by UniFFI and can be found in the generated Rust scaffolding - it is defined as:

trait UniffiCustomTypeConverter {
    type Builtin;

    fn into_custom(val: Self::Builtin) -> uniffi::Result<Self>
    where
        Self: Sized;
    fn from_custom(obj: Self) -> Self::Builtin;
}

where Builtin is the Rust type corresponding to the UniFFI builtin-type - i64 in the example above. Thus, the trait implemention for Handle would look something like:


#![allow(unused)]
fn main() {
impl UniffiCustomTypeConverter for Handle {
    type Builtin = i64;

    fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
        Ok(Handle(val))
    }

    fn from_custom(obj: Self) -> Self::Builtin {
        obj.0
    }
}
}

Because UniffiCustomTypeConverter is defined in each crate, this means you can use custom types even if they are not defined in your crate - see the 'custom_types' example which demonstrates url::Url as a custom type.

Error handling during conversion

You might have noticed that the into_custom function returns a uniffi::Result<Self> (which is an alias for anyhow::Result) and might be wondering what happens if you return an Err.

It depends on the context. In short:

  • If the value is being used as an argument to a function/constructor that does not return a Result (ie, does not have the throws attribute in the .udl), then the uniffi generated scaffolding code will panic!()

  • If the value is being used as an argument to a function/constructor that does return a Result (ie, does have a throws attribute in the .udl), then the uniffi generated scaffolding code will use anyhow::Error::downcast() to try and convert the failure into that declared error type and:

    • If that conversion succeeds, it will be used as the Err for the function.
    • If that conversion fails, it will panic()

Example

For example, consider the following UDL:

[Custom]
typedef i64 Handle;

[Error]
enum ExampleErrors {
    "InvalidHandle"
};

namespace errors_example {
    take_handle_1(Handle handle);

    [Throws=ExampleErrors]
    take_handle_2(Handle handle);
}

and the following Rust:


#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum ExampleErrors {
    #[error("The handle is invalid")]
    InvalidHandle,
}

impl UniffiCustomTypeConverter for ExampleHandle {
    type Builtin = i64;

    fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
        if (val == 0) {
            Err(ExampleErrors::InvalidHandle.into())
        else if (val == -1) {
            Err(SomeOtherError.into()) // SomeOtherError decl. not shown.
        } else {
            Ok(Handle(val))
        }
    }
    ...
}
}

The behavior of the generated scaffolding will be:

  • Calling take_handle_1 with a value of 0 or -1 will always panic.
  • Calling take_handle_2 with a value of 0 will return Err(ExampleErrors) exception
  • Calling take_handle_2 with a value of -1 will always panic.
  • All other values will return Ok(ExampleHandle)

Custom types in the bindings code

Note: The facility described in this document is not yet available for the Ruby bindings.

By default, the foreign bindings just see the builtin type - eg, the bindings will get an integer for the Handle.

However, custom types can also be converted on the bindings side. For example, a Url type could be configured to use the java.net.URL class in Kotlin by adding code like this to uniffi.toml:

[bindings.kotlin.custom_types.Url]
# Name of the type in the Kotlin code
type_name = "URL"
# Classes that need to be imported
imports = [ "java.net.URL" ]
# Expression to convert the builtin type the custom type.  In this example, `{}` will be replaced with the int value.
into_custom = "URL({})"
# Expression to convert the custom type to the builtin type.  In this example, `{}` will be replaced with the URL value.
from_custom = "{}.toString()"

Here's how the configuration works in uniffi.toml.

  • Create a [bindings.{language}.custom_types.{CustomTypeName}] table to enable a custom type on a bindings side. This has several subkeys:
    • type_name (Optional, Typed languages only): Type/class name for the custom type. Defaults to the type name used in the UDL. Note: The UDL type name will still be used in generated function signatures, however it will be defined as a typealias to this type.
    • into_custom: Expression to convert the UDL type to the custom type. {} will be replaced with the value of the UDL type.
    • from_custom: Expression to convert the custom type to the UDL type. {} will be replaced with the value of the custom type.
    • imports (Optional) list of modules to import for your into_custom/from_custom functions.

Using Custom Types from other crates

To use the Handle example above from another crate, these other crates just refer to the type as a regular External type - for example, another crate might use udl such as:

[External="crate_defining_handle_name"]
typedef extern Handle;

Integrating with Gradle

It is possible to generate Kotlin bindings at compile time for Kotlin Android projects. We'd like to make a gradle plugin for that, but until then you can add to your build.gradle the following:

android.libraryVariants.all { variant ->
    def t = tasks.register("generate${variant.name.capitalize()}UniFFIBindings", Exec) {
        workingDir "${project.projectDir}"
        // Runs the bindings generation, note that you must have uniffi-bindgen installed and in your PATH environment variable
        commandLine 'uniffi-bindgen', 'generate', '<PATH TO .udl FILE>', '--language', 'kotlin', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"
    }
    variant.javaCompileProvider.get().dependsOn(t)
    def sourceSet = variant.sourceSets.find { it.name == variant.name }
    sourceSet.java.srcDir new File(buildDir, "generated/source/uniffi/${variant.name}/java")
    // XXX: I've been trying to make this work but I can't, so the compiled bindings will show as "regular sources" in Android Studio.
    idea.module.generatedSourceDirs += file("${buildDir}/generated/source/uniffi/${variant.name}/java/uniffi")
}

The generated bindings should appear in the project sources in Android Studio.

Using experimental unsigned types

Unsigned integers in the defined API are translated to their equivalents in the foreign language binding, e.g. u32 becomes Kotlin's UInt type. See Built-in types.

However unsigned integer types are experimental in Kotlin versions prior to 1.5. As such they require explicit annotations to suppress warnings. Uniffi is trying to add these annotations where necessary, but currently misses some places, see PR #977 for details.

To suppress all warnings for experimental unsigned types add this to your project's build.gradle file:

allprojects {
   tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
        kotlinOptions {
            freeCompilerArgs += [
                "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes",
            ]
        }
    }
}

Update

As of PR #993, the Kotlin backend was refactored, and it became harder to support the @ExperimentalUnsignedTypes annotation. Uniffi's Android customers are rapidly moving toward Kotlin 1.5, so adding this compiler arg is no longer necessary.

JNA dependency

UniFFI relies on JNA for the ability to call native methods. JNA 5.7 or greater is required.

Set the dependency in your build.gradle:

dependencies {
    implementation "net.java.dev.jna:jna:5.7.0@aar"
}

Swift Bindings

UniFFI ships with production-quality support for generating Swift bindings. Concepts from the UDL file map into Swift as follows:

  • Primitive datatypes map to their obvious Swift counterpart, e.g. u32 becomes UInt32, string becomes String, etc.
  • An object interface declared as interface T is represented as a Swift protocol TProtocol and a concrete Swift class T that conforms to it. Having the protocol declared explicitly can be useful for mocking instances of the class in unittests.
  • A dictionary struct declared as dictionary T is represented as a Swift struct T with public mutable fields.
  • An enum declared enum T or [Enum] interface T is represented as a Swift enum T with appropriate variants.
  • Optional types are represented using Swift's builtin optional type syntax T?.
  • Sequences are represented as Swift arrays, and Maps as Swift dictionaries.
  • Errors are represented as Swift enums that conform to the Error protocol.
  • Function calls that have an associated error type are marked with throws, and hence must be called using one of Swift's try syntax variants.
  • Failing assertions, Rust panics, and other unexpected errors in the generated code are translated into a private enum conforming to the Error protocol.
    • If this happens inside a throwing Swift function, it can be caught and handled by a catch-all catch statement (but do so at your own risk, because it indicates that something has gone seriously wrong).
    • If this happens inside a non-throwing Swift function, it will be converted into a fatal Swift error that cannot be caught.

Conceptually, the generated bindings are split into two Swift modules, one for the low-level C FFI layer and one for the higher-level Swift bindings. For a UniFFI component named "example" we generate:

  • A C header file exampleFFI.h declaring the low-level structs and functions for calling into Rust, along with a corresponding exampleFFI.modulemap to expose them to Swift.
  • A Swift source file example.swift that imports the exampleFFI module and wraps it to provide the higher-level Swift API.

Splitting up the bindings in this way gives you flexibility over how both the Rust code and the Swift code are distributed to consumers. For example, you may choose to compile and distribute the Rust code for several UniFFI components as a single shared library in order to reduce the compiled code size, while distributing their Swift wrappers as individual modules.

For more technical details on how the bindings work internally, please see the module documentation

Configuration

The generated Swift module can be configured using a uniffi.toml configuration file.

Available options

Configuration nameDefaultDescription
cdylib_nameuniffi_{namespace}1The name of the compiled Rust library containing the FFI implementation.
module_name{namespace}1The name of the Swift module containing the high-level foreign-language bindings.
ffi_module_name{module_name}FFIThe name of the lower-level C module containing the FFI declarations.
ffi_module_filename{ffi_module_name}The filename stem for the lower-level C module containing the FFI declarations.
generate_module_maptrueWhether to generate a .modulemap file for the lower-level C module with FFI declarations.
omit_argument_labelsfalseWhether to omit argument labels in Swift function definitions.
1

namespace is the top-level namespace from your UDL file.

Example

[bindings.swift]
cdylib_name = "mycrate_ffi"
omit_argument_labels = true

Compiling a Swift module

Before you can import the generated Swift bindings as a module (say, to use them from your application, or to try them out using swift on the command-line) you first need to compile them into a Swift module.

To do so, you'll need both the generated .swift file and the corresponding .modulemap file, which tells Swift how to expose the underlying C FFI layer. Use swiftc to combine the cdylib from your Rust crate with the generated Swift bindings:

swiftc
    -module-name example                         # Name for resulting Swift module
    -emit-library -o libexample.dylib            # File to link with if using Swift REPL
    -emit-module -emit-module-path ./            # Output directory for resulting module
    -parse-as-library
    -L ./target/debug/                           # Directory containing compiled Rust crate
    -lexample                                    # Name of compiled Rust crate cdylib
    -Xcc -fmodule-map-file=exampleFFI.modulemap  # The modulemap file from above
    example.swift                                # The generated bindings file

This will produce an example.swiftmodule file that can be loaded by other Swift code or used from the Swift command-line REPL.

Integrating with XCode

It is possible to generate Swift bindings at compile time for XCode projects and incorporate them alongside hand-written Swift code to form a larger module. Broadly, you will need to:

  1. Add a build phase to compile the Rust crate into a static lib and link it into your framework.
  2. Add a build phase to run uniffi-bindgen and generate the Swift bindings.
  3. Include the generated bridging header into your overall bridging header.

There is also an example app in the UniFFI project repo that may be helpful.

Compiling the Rust crate.

Sorry, configuring XCode to compile the Rust crate into a staticlib is beyond the scope of this document. However you do so, make sure you include the resulting libexample.a file in the "Link Binary with Libraries" build phase for your framework.

This repository contains an example iOS app (at ./examples/app/ios) which may be useful for reference. It contains an xc-universal-binary.sh shell script which can invoke cargo with the necessary settings to produce a static library of Rust code.

Generating the bindings

In the "Build Rules" section of your config, add a rule to process .udl files using uniffi-bindgen. We recommend having it generate the output files somewhere in your source tree, rather than in XCode's default $DERIVED_FILE_DIR; this both helps with debugging the build output, and makes it easier to configure how the generated files are used.

  • Add a build rule processing files with names matching *.udl.
    • Use something like the following as the custom script:
      • $HOME/.cargo/bin/uniffi-bindgen generate $INPUT_FILE_PATH --language swift --out-dir $INPUT_FILE_DIR/Generated
    • Add both the .swift file and the generated bridging header as output files:
      • $(INPUT_FILE_DIR)/Generated/$(INPUT_FILE_BASE).swift
      • $(INPUT_FILE_DIR)/Generated/$(INPUT_FILE_BASE)FFI.h
  • Add your .udl file to the "Compile Sources" build phase for your framework, so that XCode will process it using the new build rule and will include the resulting outputs in the build.

You do not need to add the generated Swift code to the list of "Compile Sources" and should not attempt to compile it explicitly; XCode will figure out what it needs to do with this code based on it being generated from the Build Rule for your .udl file.

Including the bridging header

In the overall bridging header for your module, include the header file generated by UniFFI in the previous step:

#include "exampleFFI.h"

For this to work without complaint from XCode, you also need to add the generated header file as a Public header in the "Headers" build phase of your project (which is why it's useful to generate this file somewhere in your source tree, rather than in a temporary build directory).

Design Principles

These are some high-level points to consider when making changes to UniFFI (or when wondering why past changes were made in a particular way).

Prioritize Mozilla's short-term needs

The initial consumers of this tool are teams working on features for Mozilla's mobile browsers. While we try to make the tool generally useful, we'll invest first in things that are the most valuable to those teams, which are reflected in the points below.

Safety First

The generated bindings need to be safe by default. It should be impossible for foreign-language code to trigger undefined behaviour in Rust by calling the public API of the generated bindings, even if it is called in egregiously wrong or malicious ways. We will accept reduced performance in the interests of ensuring this safety.

(The meaning of "impossible" and "public API" will of course depend on the target language. For example, code in Python might mutate internal attributes of an object that are marked as private with a leading underscore, and there's not much we can do to guard against that.)

Where possible, we use Rust's typesystem to encode safety guarantees. If that's not possible then the generated Rust code may use unsafe and assume that the generated foreign-language code will uphold safety guarantees at runtime.

Example: We insist that all object instances exposed to foreign-language code be Sync and Send, so that they're safe to access regardless of the threading model of the calling code. We do not allow thread-safety guarantees to be deferred to assumptions about how the code is called.

Example: We do not allow returning any borrowed data from function calls, because we can't make any guarantees about when or how the foreign-language could access it.

Performance is a feature, but not a deal-breaker

Our initial use-cases are not performance-critical, and our team are not low-level Rust experts, so we're highly motivated to favour simplicity and maintainability over performance. Given the choice we will pick "simple but slow" over "fast but complicated".

However, we know that performance can degrade through thousands of tiny cuts, so we'll keep iterating towards the winning combination of "simple and fast" over time.

Example: Initial versions of the tool used opaque integer handles and explicit mutexes to manage object references, favouring simplicity (in the "we're confident this works as intended" sense) over performance. As we got more experience and confidence with the approach and tool we replaced handles with raw Arc pointers, which both simplified the code and removed some runtime overheads.

Violation: The tool currently passes structured data over the FFI by serializing it to a byte buffer, favouring ease of implementation and understanding over performance. This was fine as a starting point! However, we have not done any work to measure the performace impact or iterate towards something with lower overhead (such as using repr(C) structs).

Produce bindings that feel idiomatic for the target language

The generated bindings should feel idiomatic for their end users, and what feels idiomatic can differ between different target languages. Ideally consumers should not even realize that they're using bindings to Rust under the hood.

We'll accept extra complexity inside of UniFFI if it means producing bindings that are nicer for consumers to use.

Example: We case-convert names to match the accepted standards of the target language, so a method named do_the_thing in Rust might be called doTheThing in its Kotlin bindings.

Example: Object references try to integrate with the GC of the target language, so that holding a reference to a Rust struct feels like holding an ordinary object instance.

Violation: The Kotlin bindings have an explicit destroy method on object instances, because we haven't yet found a good way to integrate with the JVM's GC.

Empower users to debug and maintain the tool

To succeed long-term, we can't depend on a dedicated team of "UniFFI experts" for debugging and maintenance. The people using the tool need to be empowered to debug, maintain and develop it.

If you're using UniFFI-generated bindings and something doesn't work quite right, it should be possible for you to dig in to the generated foreign-language code, follow it through to the underlying Rust code, and work out what's going wrong without being an expert in Rust or UniFFI.

Example: We try to include comments in the generated code to help guide users who may be reading through it to debug some issue.

Violation: We don't have very good "overview" documentation on how each set of foreign-language bindings works, so someone trying to debug the Kotlin bindings would need to poke around in the generated code to try to build up a mental model of how it's supposed to work.

Violation: A lack of structure in our code-generation templates means that it's hard for non-experts to find and change the codegen logic for a particular piece of functionality.

Navigating the code

The code for UniFFI is organized into the following crates:

  • ./uniffi_bindgen: This is the source for the uniffi-bindgen executable and is where most of the logic for the UniFFI tool lives. Its contents include:
  • ./uniffi: This is a run-time support crate that is used by the generated Rust scaffolding. It controls how values of various types are passed back-and-forth over the FFI layer, by means of the FfiConverter trait.
  • ./uniffi_build: This is a small hook to run uniffi-bindgen from the build.rs script of a UniFFI component, in order to automatically generate the Rust scaffolding as part of its build process.
  • ./uniffi_macros: This contains some helper macros that UniFFI components can use to simplify loading the generated scaffolding, and executing foreign-language tests.
  • ./examples: This contains code examples that you can use to explore the code generation process.

Lifting, Lowering and Serialization

UniFFI is able to transfer rich data types back-and-forth between the Rust code and the foreign-language code via a process we refer to as "lowering" and "lifting".

Recall that UniFFI interoperates between different languages by defining a C-style FFI layer which operates in terms of primitive data types and plain functions. To transfer data from one side of this layer to the other, the sending side "lowers" the data from a language-specific data type into one of the primitive types supported by the FFI-layer functions, and the receiving side "lifts" that primitive type into its own language-specific data type.

Lifting and lowering simple types such as integers is done by directly casting the value to and from an appropriate type. For complex types such as optionals and records we currently implement lifting and lowering by serializing into a byte buffer, but this is an implementation detail that may change in future. (See ADR-0002 for the reasoning behind this choice).

As a concrete example, consider this interface for accumulating a list of integers:

namespace example {
  sequence<i32> add_to_list(i32 item);
}

Calling this function from foreign language code involves the following steps:

  1. The user-provided calling code invokes the add_to_list function that is exposed by the UniFFI-generated foreign language bindings, passing item as an appropriate language-native integer.
  2. The foreign language bindings lower each argument to a function call into something that can be passed over the C-style FFI. Since the item argument is a plain integer, it is lowered by casting to an int32_t.
  3. The foreign language bindings pass the lowered arguments to a C FFI function named like example_XYZ_add_to_list that is exposed by the UniFFI-generated Rust scaffolding.
  4. The Rust scaffolding lifts each argument received over the FFI into a native Rust type. Since item is a plain integer it is lifted by casting to a Rust i32.
  5. The Rust scaffolding passes the lifted arguments to the user-provided Rust code for the add_to_list function, which returns a Vec<i32>.
  6. The Rust scaffolding now needs to lower the return value in order to pass it back to the foreign language code. Since this is a complex data type, it is lowered by serializing the values into a byte buffer and returning the buffer pointer and length from the FFI function.
  7. The foreign language bindings receive the return value and need to lift it into an appropriate native data type. Since it is a complex data type, it is lifted by deserializing from the returned byte buffer into a language-native list of integers.

Lowered Types

UDL TypeRepresentation in the C FFI
i8/i16/i32/i64int8_t/int16_t/int32_t/int64_t
u8/u16/u32/u64uint8_t/uint16_t/uint32_t/uint64_t
f32/floatfloat
f64/doubledouble
booleanint8_t, either 0 or 1
stringRustBuffer struct pointing to utf8 bytes
timestampRustBuffer struct pointing to a i64 representing seconds and a u32 representing nanoseconds
durationRustBuffer struct pointing to a u64 representing seconds and a u32 representing nanoseconds
T?RustBuffer struct pointing to serialized bytes
sequence<T>RustBuffer struct pointing to serialized bytes
record<DOMString, T>RustBuffer struct pointing to serialized bytes
enum and [Enum] interfaceRustBuffer struct pointing to serialized bytes
dictionaryRustBuffer struct pointing to serialized bytes
interfacevoid* opaque pointer to object on the heap

Serialization Format

When serializing complex data types into a byte buffer, UniFFI uses an ad-hoc fixed-width format which is designed mainly for simplicity. The details of this format are internal only and may change between versions of UniFFI.

UDL TypeRepresentation in serialized bytes
i8/i16/i32/i64Fixed-width 1/2/4/8-byte signed integer, big-endian
u8/u16/u32/u64Fixed-width 1/2/4/8-byte unsigned integer, big-endian
f32/floatFixed-width 4-byte float, big-endian
f64/doubleFixed-width 8-byte double, big-endian
booleanFixed-width 1-byte signed integer, either 0 or 1
stringSerialized i32 length followed by utf-8 string bytes; no trailing null
T?If null, serialized boolean false; if non-null, serialized boolean true followed by serialized T
sequence<T>Serialized i32 item count followed by serialized items; each item is a serialized T
record<DOMString, T>Serialized i32 item count followed by serialized items; each item is a serialized string followed by a serialized T
enum and [Enum] interfaceSerialized i32 indicating variant, numbered in declaration order starting from 1, followed by the serialized values of the variant's fields in declaration order
dictionaryThe serialized value of each field, in declaration order
interfaceFixed-width 8-byte unsigned integer encoding a pointer to the object on the heap

Note that length fields in this format are serialized as signed integers despite the fact that they will always be non-negative. This is to help ease compatibility with JVM-based languages since the JVM uses signed 32-bit integers for its size fields internally.

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, wrap it in an Arc<> and return the Arc's raw pointer to the foreign language code:


#![allow(unused)]
fn main() {
pub extern "C" fn todolist_12ba_TodoList_new(
    err: &mut uniffi::deps::ffi_support::ExternError,
) -> *const std::os::raw::c_void /* *const TodoList */ {
    uniffi::deps::ffi_support::call_with_output(err, || {
        let _new = TodoList::new();
        let _arc = std::sync::Arc::new(_new);
        <std::sync::Arc<TodoList> as uniffi::FfiConverter>::lower(_arc)
    })
}
}

The UniFFI runtime implements lowering for object instances using Arc::into_raw:


#![allow(unused)]
fn main() {
unsafe impl<T: Sync + Send> FfiConverter for std::sync::Arc<T> {
    type FfiType = *const std::os::raw::c_void;
    fn lower(self) -> Self::FfiType {
        std::sync::Arc::into_raw(self) as Self::FfiType
    }
}
}

which does the "arc to pointer" dance for us. 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 turns it back into a cloned Arc<> which lives for the duration of the method call:


#![allow(unused)]
fn main() {
pub extern "C" fn todolist_12ba_TodoList_add_item(
    ptr: *const std::os::raw::c_void,
    todo: uniffi::RustBuffer,
    err: &mut uniffi::deps::ffi_support::ExternError,
) -> () {
    uniffi::deps::ffi_support::call_with_result(err, || -> Result<_, TodoError> {
        let _retval = TodoList::add_item(
          &<std::sync::Arc<TodoList> as uniffi::FfiConverter>::try_lift(ptr).unwrap(),
          <String as uniffi::FfiConverter>::try_lift(todo).unwrap())?,
        )
        Ok(_retval)
    })
}
}

The UniFFI runtime implements lifting for object instances using Arc::from_raw:


#![allow(unused)]
fn main() {
unsafe impl<T: Sync + Send> FfiConverter for std::sync::Arc<T> {
    type FfiType = *const std::os::raw::c_void;
    fn try_lift(v: Self::FfiType) -> Result<Self> {
        let v = v as *const T;
        // We musn't drop the `Arc<T>` that is owned by the foreign-language code.
        let foreign_arc = std::mem::ManuallyDrop::new(unsafe { Self::from_raw(v) });
        // Take a clone for our own use.
        Ok(std::sync::Arc::clone(&*foreign_arc))
    }
}

Notice that we take care to ensure the reference that is owned by the foreign-language code remains alive.

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.)


#![allow(unused)]
fn main() {
pub extern "C" fn ffi_todolist_12ba_TodoList_object_free(ptr: *const std::os::raw::c_void) {
    if let Err(e) = std::panic::catch_unwind(|| {
        assert!(!ptr.is_null());
        unsafe { std::sync::Arc::from_raw(ptr as *const TodoList) };
    }) {
        uniffi::deps::log::error!("ffi_todolist_12ba_TodoList_object_free panicked: {:?}", e);
    }
}
}

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