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

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

Third-party foreign language bindings

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:

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

Add uniffi as a dependency and build-dependency

In your crate's Cargo.toml add:

[dependencies]
uniffi = { version = "[latest-version]" }

[build-dependencies]
uniffi = { version = "[latest-version]", features = [ "build" ] }

UniFFI has not reached version 1.0 yet. Versions are typically specified as 0.[minor-version].

Build your crate as a cdylib

Ensure your crate builds as a cdylib by adding

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

to your crate's Cargo.toml.

Note: You also need to add staticlib crate type if you target iOS.

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! See the Built-in Types chapter for more information on mapping types between Rust and UDL.

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!

Note It's also possible to use Rust procmacros to describe your interface and you can avoid UDL files entirely if you choose. Unfortunately the docs aren't quite as good for that yet though.

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 as both a dependency and build-dependency. Enable the build feature for the build-dependencies. This adds the runtime support code that powers UniFFI and build-time support for generating the Rust scaffolding code.

[dependencies]
uniffi = "0.XX.0"

[build-dependencies]
uniffi = { version = "0.XX.0", features = ["build"] }

As noted in Describing the interface, UniFFI currently supports two methods of interface definitions: UDL files and proc macros. If you are using only proc macros, you can skip some boilerplate in your crate setup as well.

Setup for crates using UDL

Crates using UDL need a build.rs file next to Cargo.toml. This uses uniffi to generate the Rust scaffolding code.

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

Lastly, we include the generated scaffolding code in our lib.rs using this handy macro:

uniffi::include_scaffolding!("math");

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

Setup for crates using only proc macros

If you are only using proc macros, you can skip build.rs entirely! All you need to do is add this to the top of lib.rs NOTE: This function takes an optional parameter, the namespace used by the component. If not specified, the crate name will be used as the namespace.

uniffi::setup_scaffolding!();

⚠ Warning ⚠ Do not call both uniffi::setup_scaffolding!() and uniffi::include_scaffolding!!() in the same crate.

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.

Creating the bindgen binary

First, make sure you have installed all the prerequisites.

Ideally you would then run the uniffi-bindgen binary from the uniffi crate to generate your bindings. However, this is only available with Cargo nightly. To work around this, you need to create a binary in your project that does the same thing.

Add the following to your Cargo.toml:

[[bin]]
# This can be whatever name makes sense for your project, but the rest of this tutorial assumes uniffi-bindgen.
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"

Create uniffi-bindgen.rs:

fn main() {
    uniffi::uniffi_bindgen_main()
}

You can now run uniffi-bindgen from your project using cargo run --features=uniffi/cli --bin uniffi-bindgen [args]

Multi-crate workspaces

If your project consists of multiple crates in a Cargo workspace, then the process outlined above would require you creating a binary for each crate that uses UniFFI. You can avoid this by creating a separate crate for running uniffi-bindgen:

  • Name the crate uniffi-bindgen
  • Add this dependency to Cargo.toml: uniffi = {version = "0.XX.0", features = ["cli"] }
  • Follow the steps from the previous section to add the uniffi-bindgen binary target

Then your can run uniffi-bindgen from any create in your project using cargo run -p uniffi-bindgen [args]

Use generate --library to generate foreign bindings by using a cdylib file built for your library. This flag was added in UniFFI 0.24 and can be more convenient than specifying the UDL file -- especially when multiple UniFFI-ed crates are built together in one library. The plan is to make library mode the default in a future UniFFI version, and it is highly recommended to specify the flag for now (because some features simply don't work otherwise).

Taking example/arithmetic as an example, you can generate the bindings with:

cargo build --release
cargo run --bin uniffi-bindgen generate --library target/release/libarithmetical.so --language kotlin --out-dir out

Then check out the out directory.

When using library mode, if multiple crates get built into the library that use UniFFI, all will have bindings generated for them.

Library mode comes with some extra requirements:

  • It must be run from within the cargo workspace of your project
  • Each crate must use exactly 1 UDL file when compiling the Rust library. However, crates can have multiple UDL files as long as they ensure only one is used for any particular build, e.g. by using feature flags.
  • Rust sources must use uniffi::include_scaffolding! to include the scaffolding code.

Running uniffi-bindgen with a single UDL file

Use the generate command to generate bindings by specifying a UDL file.

Kotlin

From the example/arithmetic directory, run:

cargo run --bin uniffi-bindgen generate src/arithmetic.udl --language kotlin

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

Swift

Run

cargo run --bin uniffi-bindgen generate src/arithmetic.udl --language swift

then check out src/arithmetic.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
Vec<u8>bytesDifferent from sequence<u8> only in foreign type mappings
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<K, V>record<K, T>
()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

enum Animal {
    Dog,
    Cat,
}

Can be exposed in the UDL file with:

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

Enums with fields

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:

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. However, procmacros support more flexible enums.

#[derive(uniffi::Enum)]
pub enum MyEnum {
    None,
    Str(String),
    All { s: String, i: i64 }
}

Remote, non-exhaustive enums

One corner case is an enum that's:

In this case, UniFFI needs to generate a default arm when matching against the enum variants, or else a compile error will be generated. Use the [NonExhaustive] attribute to handle this case:

[Enum]
[NonExhaustive]
interface Message {
  Send(u32 from, u32 to, string contents);
  Quit();
};

Note: since UniFFI generates a default arm, if you leave out a variant, or if the upstream crate adds a new variant, this won't be caught at compile time. Any attempt to pass that variant across the FFI will result in a panic.

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:

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:

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:

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:

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:

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 {
    // ...
}

Async

Async functions can be exposed using the [Async] attribute:

namespace Example {
    [Async]
    string async_hello();
}

See the Async/Future support section for details.

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:

#[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 associated data as fields on the exception, use this syntax:

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

Interfaces as errors

It's possible to use an interface (ie, a rust struct impl or a dyn Trait) as an error; the thrown object will have methods instead of fields. This can be particularly useful when working with anyhow style errors, where an enum can't easily represent certain errors.

In your UDL:

namespace error {
  [Throws=MyError]
  void bail(string message);
}

[Traits=(Debug)]
interface MyError {
  string message();
};

and Rust:

#[derive(Debug, thiserror::Error)]
#[error("{e:?}")] // default message is from anyhow.
pub struct MyError {
    e: anyhow::Error,
}

impl MyError {
    fn message(&self) -> String> { self.to_string() }
}

impl From<anyhow::Error> for MyError {
    fn from(e: anyhow::Error) -> Self {
        Self { e }
    }
}

You can't yet use anyhow directly in your exposed functions - you need a wrapper:

fn oops() -> Result<(), Arc<MyError>> {
    let e = anyhow::Error::msg("oops");
    Err(Arc::new(e.into()))
}

then in Python:

try:
  oops()
except MyError as e:
  print("oops", e.message())

This works for procmacros too - just derive or export the types.

#[derive(Debug, uniffi::Error)]
pub struct MyError { ... }
#[uniffi::export]
impl MyError { ... }
#[uniffi::export]
fn oops(e: String) -> Result<(), Arc<MyError>> { ... }

See our tests this feature.

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:

struct TodoList {
    items: RwLock<Vec<String>>
}

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

    fn add_item(&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.

Exposing Traits as interfaces

It's possible to have UniFFI expose a Rust trait as an interface by specifying a Trait attribute.

For example, in the UDL file you might specify:

[Trait]
interface Button {
    string name();
};

With the following Rust implementation:

pub trait Button: Send + Sync {
    fn name(&self) -> String;
}

struct StopButton {}

impl Button for StopButton  {
    fn name(&self) -> String {
        "stop".to_string()
    }
}

Uniffi explicitly checks all interfaces are Send + Sync - there's a ui-test which demonstrates obscure rust compiler errors when it's not true. Traits however need to explicitly add those bindings.

References to traits are passed around like normal interface objects - in an Arc<>. For example, this UDL:

namespace traits {
    sequence<Button> get_buttons();
    Button press(Button button);
};

would have these signatures in Rust:

fn get_buttons() -> Vec<Arc<dyn Button>> { ... }
fn press(button: Arc<dyn Button>) -> Arc<dyn Button> { ... }

Foreign implementations

Use the WithForeign attribute to allow traits to also be implemented on the foreign side passed into Rust, for example:

[Trait, WithForeign]
interface Button {
    string name();
};
class PyButton(uniffi_module.Button):
    def name(self):
        return "PyButton"

uniffi_module.press(PyButton())

Note: This is currently only supported on Python, Kotlin, and Swift.

Traits construction

Because any number of structs may implement a trait, they don't have constructors.

Traits example

See the "traits" example for more.

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);
    // This alternate constructor is async.
    [Async, Name=new_async]
    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.

Constructors can be async, although support for async primary constructors in bindings is minimal.

Exposing methods from standard Rust traits

Rust has a number of general purpose traits which add functionality to objects, such as Debug, Display, etc. It's possible to tell UniFFI that your object implements these traits and to generate FFI functions to expose them to consumers. Bindings may then optionally generate special methods on the object.

For example, consider the following example:

[Traits=(Debug)]
interface TodoList {
    ...
}

and the following Rust code:

#[derive(Debug)]
struct TodoList {
   ...
}

(or using proc-macros)

#[derive(Debug, uniffi::Object)]
#[uniffi::export(Debug)]
struct TodoList {
   ...
}

This will cause the Python bindings to generate a __repr__ method that returns the value implemented by the Debug trait. Not all bindings support generating special methods, so they may be ignored. It is your responsibility to implement the trait on your objects; UniFFI will attempt to generate a meaningful error if you do not.

The list of supported traits is hard-coded in UniFFI's internals, and at time of writing is Debug, Display, Eq and Hash.

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();

    // Create a list of lists, one for each item this one
    sequence<TodoList> split();
};

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.

When returning interface objects, UniFFI supports both Rust functions that wrap the value in an Arc<> and ones that don't. This only applies if the interface type is returned directly:

impl TodoList {
    // When the foreign function/method returns `TodoList`, the Rust code can return either `TodoList` or `Arc<TodoList>`.
    fn duplicate(&self) -> TodoList {
        TodoList {
            items: RwLock::new(self.items.read().unwrap().clone())
        }
    }

    // However, if TodoList is nested inside another type then `Arc<TodoList>` is required
    fn split(&self) -> Vec<Arc<TodoList>> {
        self.items.read()
            .iter()
            .map(|i| Arc::new(TodoList::from_item(i.clone()))
            .collect()
    }
}

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>:

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);
    ...
};
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);
    ...
};
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:

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:

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:

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.

Callback interfaces

Callback interfaces are a special implementation of Rust traits implemented by foreign languages.

These are described in both UDL and proc-macros as an explicit "callback interface". They are (soft) deprecated, remain now for backwards compatibility, but probably should be avoided.

This document describes the differences from regular traits.

Defining a callback

If you must define a callback in UDL it would look like:

callback interface Keychain {
  // as described in the foreign traits docs...
};

procmacros support it too, but just don't use it :)

Box and Send + Sync?

Traits defined purely for callbacks probably don't technically need to be Sync in Rust, but they conceptually are, just outside of Rust's view.

That is, the methods of the foreign class must be safe to call from multiple threads at once, but Rust can not enforce this in the foreign code.

Rust signature differences

Consider the examples in Rust traits implemented by foreign languages.

If the traits in question are defined as a "callback" interface, the Arc<dyn Keychain> types would actually be Box<dyn Keychain> - eg, the Rust implementation of the Authenticator constructor would be fn new(keychain: Box<dyn Keychain>) -> Self instead of the Arc<>.

External types

External types are types implemented by UniFFI but outside of this UDL file.

They are similar to, but different from custom types which wrap UniFFI primitive types.

But like custom types, external types are all declared using a typedef with attributes giving more detail.

Types in another crate

There's a whole page about that!

Types from procmacros in this crate.

If your crate has types defined via #[uniffi::export] etc you can make them available to the UDL file in your own crate via a typedef with a [Rust=] attribute. Eg, your Rust might have:

#[derive(uniffi::Record)]
pub struct One {
    inner: i32,
}

you can use it in your UDL:

[Rust="record"]
typedef extern One;

namespace app {
    // use the procmacro type.
    One get_one(One? one);
}

Supported values:

  • "enum", "trait", "callback", "trait_with_foreign"
  • For records, either "record" or "dictionary"
  • For objects, either "object" or "interface"

Declaring External Types

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;
};

Inside another crate, consuming_crate, you'd 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;
};

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

use demo_crate::DemoDict;

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

uniffi::include_scaffolding!("consuming_crate");

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

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

External interface and trait types

If the external type is an Interface, then use the [ExternalInterface] attribute instead of [External]:

[ExternalInterface="demo_crate"]
typedef extern DemoInterface;

similarly for traits: use [ExternalTrait].

External procmacro types

The above examples assume the external types were defined via UDL. If they were defined by procmacros, you need different attribute names:

  • if DemoDict is implemented by a procmacro in demo_crate, you'd use [ExternalExport=...]
  • for DemoInterface you'd use [ExternalInterfaceExport=...]

For types defined by procmacros in this crate, see the attribute [Rust=...]

Foreign bindings

The foreign bindings will also need to know how to access the external type, which varies slightly for each language:

Kotlin

For Kotlin, "library mode" generation with generate --library [path-to-cdylib] is recommended when using external types. If you use generate [udl-path] then the generated code needs to know how to import the external types from the Kotlin module that corresponds to the Rust crate. By default, UniFFI assumes that the Kotlin module name matches the Rust crate name, but this can be configured in uniffi.toml with an entry like this:

[bindings.kotlin.external_packages]
# Map the crate names from [External={name}] into Kotlin package names
rust-crate-name = "kotlin.package.name"

Swift

For Swift, you must compile all generated .swift files together in a single module since the generate code expects that it can access external types without importing them.

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:

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 implementation for Handle would look something like:

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 ExampleError {
    "InvalidHandle"
};

namespace errors_example {
    take_handle_1(Handle handle);

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

and the following Rust:

#[derive(Debug, thiserror::Error)]
pub enum ExampleError {
    #[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 throw an ExampleError 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;

Docstrings

UDL file supports docstring comments. The comments are emitted in generated bindings without any transformations. What you see in UDL is what you get in generated bindings. The only change made to UDL comments are the comment syntax specific to each language. Docstrings can be used for most declarations in UDL file. Docstrings are parsed as AST nodes, so incorrectly placed docstrings will generate parse errors. Docstrings in UDL are comments prefixed with ///.

Docstrings in UDL

/// The list of supported capitalization options
enum Capitalization {
    /// Lowercase, i.e. `hello, world!`
    Lower,

    /// Uppercase, i.e. `Hello, World!`
    Upper
};

namespace example {
    /// Return a greeting message, using `capitalization` for capitalization
    string hello_world(Capitalization capitalization);
}

Docstrings in generated Kotlin bindings

/**
 * The list of supported capitalization options
 */
enum class Capitalization {
    /**
     * Lowercase, i.e. `hello, world!`
     */
    LOWER,

    /**
     * Uppercase, i.e. `Hello, World!`
     */
    UPPER;
}

/**
 * Return a greeting message, using `capitalization` for capitalization
 */
fun `helloWorld`(`capitalization`: Capitalization): String { .. }

Docstrings in generated Swift bindings

/**
 * The list of supported capitalization options
 */
public enum Capitalization {
    /**
     * Lowercase, i.e. `hello, world!`
     */
    case lower

    /**
     * Uppercase, i.e. `Hello, World!`
     */
    case upper
}

/**
 * Return a greeting message, using `capitalization` for capitalization
 */
public func helloWorld(capitalization: Capitalization) -> String;

Docstrings in generated Python bindings

class Capitalization(enum.Enum):
    """The list of supported capitalization options"""

    LOWER = 1
    """Lowercase, i.e. `hello, world!`"""

    UPPER = 2
    """Uppercase, i.e. `Hello, World!`"""

def hello_world(capitalization: "Capitalization") -> "str":
    """Return a greeting message, using `capitalization` for capitalization"""
    ..

Procedural Macros: Attributes and Derives

UniFFI allows you to define your function signatures and type definitions directly in your Rust code, avoiding the need to duplicate them in a UDL file and so avoiding the possibility for the two to get out of sync. This mechanism is based on Procedural Macros (proc-macros), specifically the attribute and derive macros.

You can have this mechanism extract some kinds of definitions out of your Rust code, in addition to what is declared in the UDL file. However, you have to make sure that the UDL file is still valid on its own: All types referenced in fields, parameter and return types in UDL must also be declared in UDL.

Further, using this capability probably means you still need to refer to the UDL documentation, because at this time, that documentation tends to conflate the UniFFI type model and the description of how foreign bindings use that type model. For example, the documentation for a UDL interface describes both how it is defined in UDL and how Swift and Kotlin might use that interface. The latter is relevant even if you define the interface using proc-macros instead of in UDL.

⚠ Warning ⚠ This facility is relatively new, so things may change often. However, this remains true for all of UniFFI, so proceed with caution and the knowledge that things may break in the future.

Build workflow

Be sure to use library mode when using UniFFI proc-macros (See the Foreign language bindings docs for more info).

If your crate's API is declared using only proc-macros and not UDL files, call the uniffi::setup_scaffolding macro at the top of your source code:

uniffi::setup_scaffolding!();

⚠ Warning ⚠ Do not call both uniffi::setup_scaffolding!() and uniffi::include_scaffolding!!() in the same crate.

The #[uniffi::export] attribute

The most important proc-macro is the export attribute. It can be used on functions, impl blocks, and trait definitions to make UniFFI aware of them.

#[uniffi::export]
fn hello_ffi() {
    println!("Hello from Rust!");
}

// Corresponding UDL:
//
// interface MyObject {};
#[derive(uniffi::Object)] 
struct MyObject {
    // ...
}

#[uniffi::export]
impl MyObject {
    // Constructors need to be annotated as such.
    // The return value can be either `Self` or `Arc<Self>`
    // It is treated as the primary constructor, so in most languages this is invoked with
    `MyObject()`.
    #[uniffi::constructor]
    fn new(argument: String) -> Arc<Self> {
        // ...
    }

    // Constructors with different names are also supported, usually invoked
    // as `MyObject.named()` (depending on the target language)
    #[uniffi::constructor]
    fn named() -> Arc<Self> {
        // ...
    }

    // All functions that are not constructors must have a `self` argument
    fn method_a(&self) {
        // ...
    }

    // Returning objects is also supported, either as `Self` or `Arc<Self>`
    fn method_b(self: Arc<Self>) {
        // ...
    }
}

// Corresponding UDL:
// [Trait]
// interface MyTrait {};
#[uniffi::export]
trait MyTrait {
    // ...
}

// Corresponding UDL:
// [Trait, WithForeign]
// interface MyTrait {};
#[uniffi::export(with_foreign)]
trait MyTrait {
    // ...
}

All owned builtin types and user-defined types can be used as arguments and return types.

Arguments and receivers can also be references to these types, for example:

// Input data types as references
#[uniffi::export]
fn process_data(a: &MyRecord, b: &MyEnum, c: Option<&MyRecord>) {
    ...
}

#[uniffi::export]
impl Foo {
  // Methods can take a `&self`, which will be borrowed from `Arc<Self>`
  fn some_method(&self) {
    ...
  }
}

// Input foo as an Arc and bar as a reference
fn call_both(foo: Arc<Foo>, bar: &Foo) {
  foo.some_method();
  bar.some_method();
}

The one restriction is that the reference must be visible in the function signature. This wouldn't work:

type MyFooRef = &'static Foo;

// ERROR: UniFFI won't recognize that the `foo` argument is a reference.
#[uniffi::export]
fn do_something(foo: MyFooRef) {
}

Default values

Exported functions/methods can have default values using the default argument of the attribute macro that wraps them. default inputs a comma-separated list of [name]=[value] items.

#[uniffi::export(default(text = " ", max_splits = None))]
pub fn split(
    text: String,
    sep: String,
    max_splits: Option<u32>,
) -> Vec<String> {
  ...
}

#[derive(uniffi::Object)]
pub struct TextSplitter { ... }

#[uniffi::export]
impl TextSplitter {
    #[uniffi::constructor(default(ignore_unicode_errors = false))]
    fn new(ignore_unicode_errors: boolean) -> Self {
        ...
    }

    #[uniffi::method(default(text = " ", max_splits = None))]
    fn split(
        text: String,
        sep: String,
        max_splits: Option<u32>,
    ) -> Vec<String> {
      ...
    }
}

Supported default values:

  • String, integer, float, and boolean literals
  • [] for empty Vecs
  • Option<T> allows either None or Some(T)

Renaming functions, methods and constructors

A single exported function can specify an alternate name to be used by the bindings by specifying a name attribute.

#[uniffi::export(name = "something")]
fn do_something() {
}

will be exposed to foreign bindings as a namespace function something()

You can also rename constructors and methods:

#[uniffi::export]
impl Something {
    // Set this as the default constructor by naming it `new`
    #[uniffi::constructor(name = "new")]
    fn make_new() -> Arc<Self> { ... }

    // Expose this as `obj.something()`
    #[uniffi::method(name = "something")]
    fn do_something(&self) { }
}

The uniffi::Record derive

The Record derive macro exposes a struct with named fields over FFI. All types that are supported as parameter and return types by #[uniffi::export] are also supported as field types here.

It is permitted to use this macro on a type that is also defined in the UDL file, as long as all field types are UniFFI builtin types; user-defined types might be allowed in the future. You also have to maintain a consistent field order between the Rust and UDL files (otherwise compilation will fail).

#[derive(uniffi::Record)]
pub struct MyRecord {
    pub field_a: String,
    pub field_b: Option<Arc<MyObject>>,
    // Fields can have a default values
    #[uniffi(default = "hello")]
    pub greeting: String,
    #[uniffi(default = true)]
    pub some_flag: bool,
}

The uniffi::Enum derive

The Enum derive macro works much like the Record derive macro. Any fields inside variants must be named. All types that are supported as parameter and return types by #[uniffi::export] are also supported as field types.

It is permitted to use this macro on a type that is also defined in the UDL file as long as the two definitions are equal in the names and ordering of variants and variant fields, and any field types inside variants are UniFFI builtin types; user-defined types might be allowed in the future.

#[derive(uniffi::Enum)]
pub enum MyEnum {
    Fieldless,
    WithFields {
        foo: u8,
        bar: Vec<i32>,
    },
    WithValue = 3,
}

Variant Discriminants

Variant discriminants are accepted by the macro but how they are used depends on the bindings.

For example this enum:

#[derive(uniffi::Enum)]
pub enum MyEnum {
    Foo = 3,
    Bar = 4,
}

would give you in Kotlin & Swift:

// kotlin
enum class MyEnum {
    FOO,
    BAR;
    companion object
}
// swift
public enum MyEnum {
    case foo
    case bar
}

which means you cannot use the platforms helpful methods like value or rawValue to get the underlying discriminants. Adding a repr will allow the type to be defined in the foreign bindings.

For example:

// added the repr(u8), also u16 -> u64 supported
#[repr(u8)]
#[derive(uniffi::Enum)]
pub enum MyEnum {
    Foo = 3,
    Bar = 4,
}

will now generate:

// kotlin
enum class MyEnum(val value: UByte) {
    FOO(3u),
    BAR(4u);
    companion object
}

// swift
public enum MyEnum : UInt8 {
    case foo = 3
    case bar = 4
}

The uniffi::Object derive

This derive can be used to replace an interface definition in UDL. Every object type must have either an interface definition in UDL or use this derive macro. However, #[uniffi::export] can be used on an impl block for an object type regardless of whether this derive is used. You can also mix and match, and define some method of an object via proc-macro while falling back to UDL for methods that are not supported by #[uniffi::export] yet; just make sure to use separate impl blocks:

// UDL file

interface Foo {
    void method_a();
};
// Rust file

// Not deriving uniffi::Object since it is defined in UDL
struct Foo {
    // ...
}

// Implementation of the method defined in UDL
impl Foo {
    fn method_a(&self) {
        // ...
    }
}

// Another impl block with an additional method
#[uniffi::export]
impl Foo {
    fn method_b(&self) {
        // ...
    }
}

The uniffi::custom_type and uniffi::custom_newtype macros

There are 2 macros available which allow procmacros to support "custom types" as described in the UDL documentation for Custom Types

The uniffi::custom_type! macro requires you to specify the name of the custom type, and the name of the builtin which implements this type. Use of this macro requires you to manually implement the UniffiCustomTypeConverter trait for for your type, as shown below.

pub struct Uuid {
    val: String,
}

// Use `Uuid` as a custom type, with `String` as the Builtin
uniffi::custom_type!(Uuid, String);

impl UniffiCustomTypeConverter for Uuid {
    type Builtin = String;

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

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

There's also a uniffi::custom_newtype! macro, designed for custom types which use the "new type" idiom. You still need to specify the type name and builtin type, but because UniFFI is able to make assumptions about how the type is laid out, UniffiCustomTypeConverter is implemented automatically.

uniffi::custom_newtype!(NewTypeHandle, i64);
pub struct NewtypeHandle(i64);

and that's it!

The uniffi::Error derive

The Error derive registers a type as an error and can be used on any enum that the Enum derive also accepts. By default, it exposes any variant fields to the foreign code. This type can then be used as the E in a Result<T, E> return type of an exported function or method. The generated foreign function for an exported function with a Result<T, E> return type will have the result's T as its return type and throw the error in case the Rust call returns Err(e).

#[derive(uniffi::Error)]
pub enum MyError {
    MissingInput,
    IndexOutOfBounds {
        index: u32,
        size: u32,
    }
    Generic {
        message: String,
    }
}

#[uniffi::export]
fn do_thing() -> Result<(), MyError> {
    // ...
}

You can also use the helper attribute #[uniffi(flat_error)] to expose just the variants but none of the fields. In this case the error will be serialized using Rust's ToString trait and will be accessible as the only field on each of the variants. For flat errors your variants can have unnamed fields, and the types of the fields don't need to implement any special traits.

#[derive(uniffi::Error)]
#[uniffi(flat_error)]
pub enum MyApiError {
    Http(reqwest::Error),
    Json(serde_json::Error),
}

// ToString is not usually implemented directly, but you get it for free by implementing Display.
// This impl could also be generated by a proc-macro, for example thiserror::Error.
impl std::fmt::Display for MyApiError {
    // ...
}

#[uniffi::export]
fn do_http_request() -> Result<(), MyApiError> {
    // ...
}

The #[uniffi::export(callback_interface)] attribute

#[uniffi::export(callback_interface)] can be used to export a callback interface definition. This allows the foreign bindings to implement the interface and pass an instance to the Rust code.

#[uniffi::export(callback_interface)]
pub trait Person {
    fn name() -> String;
    fn age() -> u32;
}

// Corresponding UDL:
// callback interface Person {
//     string name();
//     u32 age();
// }

Types from dependent crates

When using proc-macros, you can use types from dependent crates in your exported library, as long as the dependent crate annotates the type with one of the UniFFI derives. However, there are a couple exceptions:

Types from UDL-based dependent crates

If the dependent crate uses a UDL file to define their types, then you must invoke one of the uniffi::use_udl_*! macros, for example:

uniffi::use_udl_record!(dependent_crate, RecordType);
uniffi::use_udl_enum!(dependent_crate, EnumType);
uniffi::use_udl_error!(dependent_crate, ErrorType);
uniffi::use_udl_object!(dependent_crate, ObjectType);

Non-UniFFI types from dependent crates

If the dependent crate doesn't define the type in a UDL file or use one of the UniFFI derive macros, then it's currently not possible to use them in an proc-macro exported interface. However, we hope to fix this limitation soon.

Other limitations

In addition to the per-item limitations of the macros presented above, there is also currently a global restriction: You can only use the proc-macros inside a crate whose name is the same as the namespace in your UDL file. This restriction will be lifted in the future.

Async/Future support

UniFFI supports exposing async Rust functions over the FFI. It can convert a Rust Future/async fn to and from foreign native futures (async/await in Python/Swift, suspend fun in Kotlin etc.)

Check out the examples or the more terse and thorough fixtures.

Example

This is a short "async sleep()" example:

use std::time::Duration;
use async_std::future::{timeout, pending};

/// Async function that says something after a certain time.
#[uniffi::export]
pub async fn say_after(ms: u64, who: String) -> String {
    let never = pending::<()>();
    timeout(Duration::from_millis(ms), never).await.unwrap_err();
    format!("Hello, {who}!")
}

This can be called by the following Python code:

import asyncio
from uniffi_example_futures import *

async def main():
    print(await say_after(20, 'Alice'))

if __name__ == '__main__':
    asyncio.run(main())

Async functions can also be defined in UDL:

namespace example {
    [Async]
    string say_after(u64 ms, string who);
}

This code uses asyncio to drive the future to completion, while our exposed function is used with await.

In Rust Future terminology this means the foreign bindings supply the "executor" - think event-loop, or async runtime. In this example it's asyncio. There's no requirement for a Rust event loop.

There are some great API docs on the implementation that are well worth a read.

Exporting async trait methods

UniFFI is compatible with the async-trait crate and this can be used to export trait interfaces over the FFI.

When using UDL, wrap your trait with the #[async_trait] attribute. In the UDL, annotate all async methods with [Async]:

[Trait]
interface SayAfterTrait {
    [Async]
    string say_after(u16 ms, string who);
};

When using proc-macros, make sure to put #[uniffi::export] outside the #[async_trait] attribute:

#[uniffi::export]
#[async_trait::async_trait]
pub trait SayAfterTrait: Send + Sync {
    async fn say_after(&self, ms: u16, who: String) -> String;
}

Combining Rust and foreign async code

Traits with callback interface support that export async methods can be combined with async Rust code. See the async-api-client example for an example of this.

Python: uniffi_set_event_loop()

Python bindings export a function named uniffi_set_event_loop() which handles a corner case when integrating async Rust and Python code. uniffi_set_event_loop() is needed when Python async functions run outside of the eventloop, for example:

- Rust code is executing outside of the eventloop.  Some examples:
    - Rust code spawned its own thread
    - Python scheduled the Rust code using `EventLoop.run_in_executor`
- The Rust code calls a Python async callback method, using something like `pollster` to block
  on the async call.

In this case, we need an event loop to run the Python async function, but there's no eventloop set for the thread. Use uniffi_set_event_loop() to handle this case. It should be called before the Rust code makes the async call and passed an eventloop to use.

Generating bindings

Bindings is the term used for the code generates for foreign languages which integrate with Rust crates - that is, the generated Python, Swift or Kotlin code which drives the examples.

UniFFI comes with a uniffi_bindgen which generates these bindings. For introductory information, see Foreign Language Bindings in the tutorial

Customizing the binding generation.

Each of the bindings reads a file uniffi.toml in the root of a crate which supports various options which influence how the bindings are generated. Default options will be used if this file is missing.

--config option can be used to specify additional uniffi config file. This config is merged with the uniffi.toml config present in each crate, with its values taking precedence.

Each binding supports different options, so please see the documentation for each binding language.

Generating bindings

Bindings is the term used for the code generates for foreign languages which integrate with Rust crates - that is, the generated Python, Swift or Kotlin code which drives the examples.

UniFFI comes with a uniffi_bindgen which generates these bindings. For introductory information, see Foreign Language Bindings in the tutorial

Customizing the binding generation.

Each of the bindings reads a file uniffi.toml in the root of a crate which supports various options which influence how the bindings are generated. Default options will be used if this file is missing.

--config option can be used to specify additional uniffi config file. This config is merged with the uniffi.toml config present in each crate, with its values taking precedence.

Each binding supports different options, so please see the documentation for each binding language.

Foreign traits

UniFFI traits can be implemented by foreign code. This means traits implemented in Python/Swift/Kotlin etc can provide Rust code with capabilities not easily implemented in Rust, such as:

  • device APIs not directly available to Rust.
  • provide glue to clip together Rust components at runtime.
  • access shared resources and assets bundled with the app.

Example

To implement a Rust trait in a foreign language, you might:

1. Define a Rust trait

This toy example defines a way of Rust accessing a key-value store exposed by the host operating system (e.g. the key chain).

All methods of the Rust trait should return a Result<> with the error half being a compatible error type - see below for more on error handling.

For example:

pub trait Keychain: Send + Sync + Debug {
  fn get(&self, key: String) -> Result<Option<String>, KeyChainError>;
  fn put(&self, key: String, value: String) -> Result<(), KeyChainError>;
}

If you are using macros add #[uniffi::export(with_foreign)] above the trait. Otherwise define this trait in your UDL file:

[Trait, WithForeign]
interface Keychain {
    [Throws=KeyChainError]
    string? get(string key);

    [Throws=KeyChainError]
    void put(string key, string data);
};

The with_foreign / WithForeign attributes specify that you want to enable support for foreign implementations of that trait as well as Rust ones.

2. Allow it to be passed into Rust

Here, we define a new object with a constructor which takes a keychain.

interface Authenticator {
    constructor(Keychain keychain);
    void login();
};

In Rust we'd write:

struct Authenticator {
  keychain: Arc<dyn Keychain>,
}

impl Authenticator {
  pub fn new(keychain: Arc<dyn Keychain>) -> Self {
    Self { keychain }
  }

  pub fn login(&self) {
    let username = self.keychain.get("username".into());
    let password = self.keychain.get("password".into());
  }
}

3. Create a foreign language implementation of the trait

Here's a Kotlin implementation:

class KotlinKeychain: Keychain {
    override fun get(key: String): String? {
        // … elide the implementation.
        return value
    }
    override fun put(key: String) {
        // … elide the implementation.
    }
}

…and Swift:

class SwiftKeychain: Keychain {
    func get(key: String) -> String? {
        // … elide the implementation.
        return value
    }
    func put(key: String) {
        // … elide the implementation.
    }
}

4. Pass the implementation to Rust

Again, in Kotlin

val authenticator = Authenticator(KotlinKeychain())
// later on:
authenticator.login()

and in Swift:

let authenticator = Authenticator(SwiftKeychain())
// later on:
authenticator.login()

Care is taken to ensure that things are cleaned up in the foreign language once all Rust references drop.

⚠️ Avoid cycles

Foreign trait implementations make it easy to create cycles between Rust and foreign objects causing memory leaks. For example a foreign implementation holding a reference to a Rust object which also holds a reference to the same foreign implementation.

UniFFI doesn't try to help here and there's no universal advice; take the usual precautions.

Error handling

We must handle foreign code failing, so all methods of the Rust trait should return a Result<> with a compatible error type otherwise these errors will panic.

Unexpected Error handling.

So long as your function returns a Result<>, it's possible for you to define how "unexpected" errors (ie, errors not directly covered by your Result<> type, panics, etc) are converted to your Result<>'s Err.

If your code defines a From<uniffi::UnexpectedUniFFICallbackError> impl for your error type, then those errors will be converted into your error type which will be returned to the Rust caller. If your code does not define this implementation the generated code will panic. In other words, you really should implement this!

See our callbacks example for more.

Configuration

The generated Kotlin modules can be configured using a uniffi.toml configuration file.

Available options

Configuration nameDefaultDescription
package_nameuniffiThe Kotlin package name - ie, the value used in the package statement at the top of generated files.
cdylib_nameuniffi_{namespace}1The name of the compiled Rust library containing the FFI implementation (not needed when using generate --library).
generate_immutable_recordsfalseWhether to generate records with immutable fields (val instead of var).
custom_typesA map which controls how custom types are exposed to Kotlin. See the custom types section of the manual
external_packagesA map of packages to be used for the specified external crates. The key is the Rust crate name, the value is the Kotlin package which will be used referring to types in that crate. See the external types section of the manual
androidfalseUsed to toggle on Android specific optimizations
android_cleanerandroidUse the android.system.SystemCleaner instead of java.lang.ref.Cleaner. Fallback in both instances is the one shipped with JNA.

Example

Custom types

# Assuming a Custom Type named URL using a String as the builtin.
[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.URI", "java.net.URL" ]
# Functions to convert between strings and URLs
into_custom = "URI({}).toURL()"
from_custom = "{}.toString()"

External types

[bindings.kotlin.external_packages]
# This specifies that external types from the crate `rust-crate-name` will be referred by by the package `"kotlin.package.name`.
rust-crate-name = "kotlin.package.name"

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.12.0 or greater is required.

Set the dependency in your build.gradle:

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

Coroutines dependency

UniFFI relies on kotlinx coroutines core for future and async support. Version 1.6 or greater is required.

Set the dependency in your build.gradle:

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
}

Kotlin Lifetimes

All interfaces exposed via Kotlin expose a public API for freeing the Kotlin wrapper object in lieu of reliable finalizers. This is done by making the "base class" for all such generated objects implement the Disposable and AutoCloseable interfaces.

As such, these wrappers all implement a close() method, which must be explicitly called to ensure the associated Rust resources are reclaimed.

The best way to arrange for this to be called at the right time is beyond the scope of this document; you should consult the official documentation for AutoClosable, but one common pattern is the Kotlin use function.

Nested objects

We also need to consider what happens when objects are contained in other objects. The current situation is:

  • Dictionaries that contain interfaces implement AutoClosable with their close() method closing any contained interfaces.

  • Enums can't currently contain interfaces.

  • Lists/Maps don't implement AutoClosable; if you have a list/map of interfaces you need to close each one individually.

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, bytes becomes Data, 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

The configurations prefixed with experimental_ should be regarded as unstable and more likely to change than other configurations.

Configuration nameDefaultDescription
cdylib_nameuniffi_{namespace}1The name of the compiled Rust library containing the FFI implementation (not needed when using generate --library).
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.
generate_immutable_recordsfalseWhether to generate records with immutable fields (let instead of var).
experimental_sendable_value_typesfalseWhether to mark value types as `Sendable'.
custom_typesA map which controls how custom types are exposed to Swift. See the custom types section of the manual
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.

If you are creating an XCFramework with this code, make sure to rename the modulemap file to module.modulemap, the default value expected by Clang and XCFrameworks for exposing the C FFI library to Swift.

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

Configuration

The generated Python modules 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 (not needed when using generate --library).
custom_typesA map which controls how custom types are exposed to Python. See the custom types section of the manual
external_packagesA map which controls the package name used by external packages. See below for more.

External Packages

When you reference external modules, uniffi will generate statements like from module import Type in the referencing module. The external_packages configuration value allows you to specify how module is formed in such statements.

The value is a map, keyed by the crate-name and the value is the package name which will be used by Python for that crate. The default value is an empty map.

When looking up crate-name, the following behavior is implemented.

Default value

If no value for the crate is found, it is assumed that you will be packaging up your library as a simple Python package, so the statement will be of the form from .module import Type, where module is the namespace specified in that crate.

Note that this is invalid syntax unless the module lives in a package - attempting to use the module as a stand-alone module will fail. UniFFI just generates flat .py files; the packaging is up to you. Eg, a build process might create a directory, create an __init__.py file in that directory (maybe including from subpackage import *) and have uniffi-bindgen generate the bindings into this directory.

Specified value

If the crate-name is found in the map, the specified entry used as a package name, so the statement will be of the form from package.module import Type (again, where module is the namespace specified in that crate)

An exception is when the specified value is an empty string, in which case you will see from module import Type, so each generated module functions outside a package. This is used by some UniFFI tests to avoid the test code needing to create a Python package.

Examples

Custom Types

# Assuming a Custom Type named URL using a String as the builtin.
[bindings.python.custom_types.Url]
imports = ["urllib.parse"]
# Functions to convert between strings and the ParsedUrl class
into_custom = "urllib.parse.urlparse({})"
from_custom = "urllib.parse.urlunparse({})"

External Packages

[bindings.python.external_packages]
# An external type `Foo` in `crate-name` (which specifies a namespace of `my_module`) will be referenced via `from MyPackageName.my_module import Foo`
crate-name = "MyPackageName"

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 performance 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
bytesSame as sequence<u8>
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<string, 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<string, 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.

Code Generation and the FfiConverter trait

UniFFI needs to generate Rust code to lift/lower types. To help with this, we define the FfiConverter trait which contains the code to lift/lower/serialize a particular type.

The most straightforward approach would be to define FfiConverter on the type being lifted/lowered/serialized. However, this wouldn't work for remote types defined in 3rd-party crates because of the Rust orphan rules. For example, our crates can't implement FfiConverter on serde_json::Value because both the trait and the type are remote.

To work around this we do several things:

  • FfiConverter gets a generic type parameter. This type is basically arbitrary and doesn't affect the lowering/lifting/serialization process.
  • We generate a unit struct named UniFfiTag in the root of each UniFFIed crate.
  • Each crate uses the FfiConverter<crate::UniFfiTag> trait to lower/lift/serialize values for its scaffolding functions.

This allows us to work around the orphan rules when defining FfiConverter implementations.

  • UniFFI consumer crates can implement lifting/lowering/serializing types for their own scaffolding functions, for example impl FfiConverter<crate::UniFfiTag> for serde_json::Value. This is allowed since UniFfiTag is a local type.
  • The uniffi crate can implement lifting/lowering/serializing types for all scaffolding functions using a generic impl, for example impl<UT> FfiConverter<UT> for u8. "UT" is short for "UniFFI Tag"
  • We don't currently use this, but crates can also implement lifting/lowering/serializing their local types for all scaffolding functions using a similar generic impl (impl<UT> FfiConverter<UT> for MyLocalType).

For more details on the specifics of the "orphan rule" and why these are legal implementations, see the Rust Chalk Book

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.

Rendering Foreign Bindings

This document details the general system that UniFFI uses to render the foreign bindings code.

The Askama template engine

Our foreign bindings generation uses the Askama template rendering engine. Askama uses a compile-time macro system that allows the template code to use Rust types directly, calling their methods passing them to normal Rust functions.

The task of the templates is to render the ComponentInterface, which is the Rust representation of the UDL file, into a bindings source file. This mainly consists of rendering source code for each Type from the UDL.

Type matching

One of the main sources of complexity when generating the bindings is handling types. UniFFI supports a large number of types, each of which corresponds to a variant of the Type enum. At one point there was a fairly large number of "mega-match" functions, each one matching against all Type variants. This made the code difficult to understand, because the functionality for one kind of type was split up.

Our current system for handling this is to have exactly 2 matches against Type:

  • One match lives in the template code. We map each Type variant to a template file that renders definitions and helper code, including:
    • Class definitions for records, enums, and objects.
    • Base classes and helper classes, for example ObjectRuntime.kt contains shared functionality for all the Type::Object types.
    • The FfiConverter class definition. This handles lifting and lowering types across the FFI for the type.
    • Initialization functions
    • Importing dependencies
    • See Types.kt for an example.
  • The other match lives in the Rust code. We map each Type variant to a implementation of the CodeType trait that renders identifiers and names related to the type, including:

Why is the code organized like this? For a few reasons:

  • Defining Askama templates in Rust required a lot of boilerplate. When the Rust code was responsible for rendering the class definitions, helper classes, etc., it needed to define a lot of Askama template structs which lead to a lot of extra lines of code (see PR #1189)
  • It's easier to access global state from the template code. Since the Rust code only handles names and identifiers, it only needs access to the Type instance itself, not the ComponentInterface or the Config. This simplifies the Rust side of things (see PR #1191). Accessing the ComponentInterface and Config from the template code is easy, we simply define these as fields on the top-level template Struct then they are accessible from all child templates.
  • Putting logic in the template code makes it easier to implement external types. For example, at one point the logic to lift/lower a type lived in the Rust code as a function that generated the expression in the foreign language. However, it was not clear at all how to make this work for external types, it would probably require parsing multiple UDL files and managing multiple ComponentInterfaces. Putting the logic to lift/lower the type in the FfiConverter class simplifies this, because we can import the external FfiConverter class and use that. We only need to know the name of the FfiConverter class which is a simpler task.

Askama extensions

A couple parts of this system require us to "extend" the functionality of Askama (i.e. adding hacks to workaround its limitations).

Adding imports

We want our type template files to specify what needs to be imported, but we don't want it to render the import statements directly. The imports should be rendered at the top of the file and de-duped in case multiple types require the same import. We handle this by:

  • Defining a separate Askama template struct that loops over all types and renders the definition/helper code for them.
  • That struct also stores a BTreeSet that contains the needed import statements and has an add_import() method that the template code calls. Using a BTreeSet ensures the imports stay de-duped and sorted.
  • Rendering this template as a separate pass. The rendered string and the list of imports get passed to the main template which arranges for them to be placed in the correct location.

Including templates once

We want our type template files to render runtime code, but only once. For example, we only want to render ObjectRuntime.kt once, even if there are multiple Object types defined in the UDL file. To handle this the type template defines an include_once_check() method, which tests if we've included a file before. The template code then uses that to guard the Askama {% include %} statement. See Object.kt for an example