uniffi-rs is a suite of projects to allow Rust to be used from other languages. It was started at Mozilla to facilitate building cross-platform components in Rust which could be run on Android and iOS.

It has since grown to support for other languages not in use at Mozilla.

React Native Logo + Rust Logo

uniffi-bindgen-react-native is the project that houses the bindings generators for react-native.

It supports all language features that uniffi-rs supports, including:

  • calling functions from Typescript to Rust, synchronous and asynchronous.
  • calling functions from Rust to Typescript, synchronous and asynchronous.
  • objects with methods, including:
    • garbage collection integration.
    • uniffi traits
  • custom types

It contains tooling to generate bindings from Hermes via JSI, and to generate the code to create turbo-modules.

Warning

This project is still in early development, and should not yet be used in production.

Before you start

Better resources are available than this site for installing these dependencies.

Below are a list of the dependencies, and a non-comprehensive instructions on how to get them onto your system.

Install Rust

If Rust isn’t already installed on your system, you should install it as per the rust-lang.org install instructions.

This will add cargo and rustup to your path, which are the main entry points into Rust.

Install C++ tooling

These commands will add the tooling needed to compile and run the generated C++ code.

Optionally, clang-format can be installed to format the generated C++ code.

For MacOS, using homebrew:

brew install cmake ninja clang-format

For Debian flavoured Linux:

apt-get install cmake ninja clang-format

For generared Typescript, the existing prettier installation is detected and your configuration is used.

Android

Add the Android specific targets

This command adds the backends for the Rust compiler to emit machine code for different Android architectures.

rustup target add \
    aarch64-linux-android \
    armv7-linux-androideabi \
    i686-linux-android \
    x86_64-linux-android

Install cargo-ndk

This cargo extension handles all the environment configuration needed for successfully building libraries for Android from a Rust codebase, with support for generating the correct jniLibs directory structure.

cargo install cargo-ndk

iOS

Ensure xcodebuild is available

This command checks if Xcode command line tools are available, and if not, will start the installation process.

xcode-select --install

Add the iOS specific targets

This command adds the backends for the Rust compiler to emit machine code for different iOS architectures.

rustup target add \
    aarch64-apple-ios \
    aarch64-apple-ios-sim \
    x86_64-apple-ios

Step-by-step tutorial

This tutorial will get you started, by taking an existing Rust crate, and building a React Native library from it.

By the end of this tutorial you will:

  1. have a working turbo-module library,
  2. an example app, running in both Android and iOS,
  3. seen how to set up uniffi-bindgen-react-native for your library.

Step 1: Start with builder-bob

We first use create-react-native-library to generate our basic turbo-module library.

Known issue with builder-bob

create-react-native-library has changed a few things around, and so the following does not work yet with the latest version of create-react-native-library.

We can still work with a previous version of builder-bob for this tutorial.

npx create-react-native-library@0.35.1 my-rust-lib

The important bits are:

✔ What type of library do you want to develop? › Turbo module
✔ Which languages do you want to use? › C++ for Android & iOS

For following along, here are the rest of my answers.

✔ What is the name of the npm package? … react-native-my-rust-lib
✔ What is the description for the package? … My first React Native library in Rust
✔ What is the name of package author? … James Hugman
✔ What is the email address for the package author? … james@nospam.fm
✔ What is the URL for the package author? … https://github.com/jhugman
✔ What is the URL for the repository? … https://github.com/jhugman/react-native-my-rust-lib
✔ What type of library do you want to develop? › Turbo module
✔ Which languages do you want to use? › C++ for Android & iOS
✔ Project created successfully at my-rust-lib!

Most of the rest of the command line guide will be done within the directory this has just created.

cd my-rust-lib

Verify everything works before adding Rust:

For iOS:

yarn
(cd example/ios && pod install)
yarn example start

Then i for iOS.

After that has launched, then you can hit the a key to launch Android.

We should, if all has gone to plan, see Result = 21 on screen.

Step 2: Add uniffi-bindgen-react-native to the project

Using yarn add the uniffi-bindgen-react-native package to your project.

yarn add uniffi-bindgen-react-native

Pre-release

While this is before the first release, we’re installing straight from github.

yarn add uniffi-bindgen-react-native@https://github.com/jhugman/uniffi-bindgen-react-native

Opening package.json add the following:

  "scripts": {
+    "ubrn:ios":      "ubrn build ios     --config ubrn.config.yaml --and-generate && (cd example/ios && pod install)",
+    "ubrn:android":  "ubrn build android --config ubrn.config.yaml --and-generate",
+    "ubrn:checkout": "ubrn checkout      --config ubrn.config.yaml",
+    "ubrn:clean": "rm -Rf cpp/ android/src/main/java ios/ src/Native* src/generated/ src/index.ts*",
+    "postinstall": "yarn ubrn:checkout && yarn ubrn:android && yarn ubrn:ios",
    "example": "yarn workspace react-native-my-rust-lib-example",
    "test": "jest",
    "typecheck": "tsc",
    "lint": "eslint \"**/*.{js,ts,tsx}\"",
    "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
    "prepare": "bob build",
    "release": "release-it"
  },

You can call the config file whatever you want, I have called it ubrn.config.yaml in this example.

For now, let’s just clean the files out we don’t need:

yarn ubrn:clean

Hint

If you’re going to be using the uniffi-bindgen-react-native command direct from the command line, it may be worth setting up an alias. In bash you can do this:

alias ubrn=$(yarn ubrn --path)

There is a guide to the ubrn command here.

Pre-release

While this is before the first release, we’re installing straight from local node_modules.

After release, the C++ runtime will be published to Cocoa Pods.

Until then, you need to add the dependency to the app’s Podfile, in this case example/ios/Podfile:

  use_react_native!(
    :path => config[:reactNativePath],
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

+  # We need to specify this here in the app because we can't add a local dependency within
+  # the react-native-matrix-rust-sdk
+  pod 'uniffi-bindgen-react-native', :path => '../../node_modules/uniffi-bindgen-react-native'

Step 3: Create the ubrn.config.yaml file

Full documentation on how to configure your library can be found in the YAML configuration file page of this book.

For now, we just want to get started; let’s start with an existing Rust crate that has uniffi bindings.

---
name: MyRustLib
rust:
  repo: https://github.com/ianthetechie/uniffi-starter
  branch: main
  manifestPath: rust/foobar/Cargo.toml

Step 4: Checkout the Rust code

Now, you should be able to checkout the Rust into the library.

yarn ubrn:checkout

This will checkout the uniffi-starter repo into the rust_modules directory within your project.

Step 4: Build the Rust

Building for iOS will:

  1. Build the Rust crate for iOS, including the uniffi scaffolding in Rust.
  2. Build an xcframework for Xcode to pick up.
  3. Generate the typescript and C++ bindings between Hermes and the Rust.
  4. Generate the files to set up the JS -> Objective C -> C++ installation flow for the turbo-module.
  5. Re-run the Podfile in the example/ios directory so Xcode can see the C++ files.
yarn ubrn:ios

Building for Android will:

  1. Build the Rust crate for Android, including the uniffi scaffolding in Rust.
  2. Copy the files into the correct place in for gradlew to pick them up.
  3. Generate the files to set up the JS -> Java -> C++ installation flow for the turbo-module.
  4. Generate the files to make a turbo-module from the C++.
yarn ubrn:android

Hint

You can change the targets that get built by adding a comma separated list to the ubrn build android and ubrn build ios commands.

yarn ubrn:android --targets aarch64-linux-android,armv7-linux-androideabi

Step 5: Write an example app exercising the Rust API

Here, we’re editing the app file at example/src/App.tsx.

First we delete the starter code given to us by create-react-native-library:

import { StyleSheet, View, Text } from 'react-native';
-import { multiply } from 'react-native-my-rust-lib';
-
-const result = multiply(3, 7);

export default function App() {

Next, add the following lines in place of the lines we just deleted:

import { Calculator, type BinaryOperator, SafeAddition, ComputationResult } from '../../src';

// A Rust object
const calculator = new Calculator();
// A Rust object implementing the Rust trait BinaryOperator
const addOp = new SafeAddition();

// A Typescript class, implementing BinaryOperator
class SafeMultiply implements BinaryOperator {
  perform(lhs: bigint, rhs: bigint): bigint {
    return lhs * rhs;
  }
}
const multOp = new SafeMultiply();

// bigints
const three = 3n;
const seven = 7n;

// Perform the calculation, and to get an object
// representing the computation result.
const computation: ComputationResult = calculator
  .calculate(addOp, three, three)
  .calculateMore(multOp, seven)
  .lastResult()!;

// Unpack the bigint value into a string.
const result = computation.value.toString();

Step 6: Run the example app

Now you can run the apps on Android and iOS:

yarn example start

As with the starter app from create-react-native-library, there is very little to look at.

We should, if all has gone to plan, see Result = 42 on screen.

Step 7: Make changes in the Rust

We can edit the Rust, in this case in rust_modules/uniffi-starter/rust/foobar/src/lib.rs.

If you’re already familiar with Rust, you will notice that there is very little unusual about this file, apart from a few uniffi proc macros scattered here or there.

If you’re not familiar with Rust, you might add a function to the Rust:

#![allow(unused)]
fn main() {
#[uniffi::export]
pub fn greet(who: String) -> String {
  format!("Hello, {who}!")
}
}

Then run either yarn ubrn:ios or yarn ubrn:android.

Once either of those are run, you should be able to import the greet function into App.tsx.

Appendix: the Rust

The Rust library is presented here for comparison with the App.tsx above.

All credit should go to the author, ianthetechie.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::time::{Duration, Instant};
// You must call this once
uniffi::setup_scaffolding!();

// What follows is an intentionally ridiculous whirlwind tour of how you'd expose a complex API to UniFFI.

#[derive(Debug, PartialEq, uniffi::Enum)]
pub enum ComputationState {
    /// Initial state with no value computed
    Init,
    Computed {
        result: ComputationResult
    },
}

#[derive(Copy, Clone, Debug, PartialEq, uniffi::Record)]
pub struct ComputationResult {
    pub value: i64,
    pub computation_time: Duration,
}

#[derive(Debug, PartialEq, thiserror::Error, uniffi::Error)]
pub enum ComputationError {
    #[error("Division by zero is not allowed.")]
    DivisionByZero,
    #[error("Result overflowed the numeric type bounds.")]
    Overflow,
    #[error("There is no existing computation state, so you cannot perform this operation.")]
    IllegalComputationWithInitState,
}

/// A binary operator that performs some mathematical operation with two numbers.
#[uniffi::export(with_foreign)]
pub trait BinaryOperator: Send + Sync {
    fn perform(&self, lhs: i64, rhs: i64) -> Result<i64, ComputationError>;
}

/// A somewhat silly demonstration of functional core/imperative shell in the form of a calculator with arbitrary operators.
///
/// Operations return a new calculator with updated internal state reflecting the computation.
#[derive(PartialEq, Debug, uniffi::Object)]
pub struct Calculator {
    state: ComputationState,
}

#[uniffi::export]
impl Calculator {
    #[uniffi::constructor]
    pub fn new() -> Self {
        Self {
            state: ComputationState::Init
        }
    }

    pub fn last_result(&self) -> Option<ComputationResult> {
        match self.state {
            ComputationState::Init => None,
            ComputationState::Computed { result } => Some(result)
        }
    }

    /// Performs a calculation using the supplied binary operator and operands.
    pub fn calculate(&self, op: Arc<dyn BinaryOperator>, lhs: i64, rhs: i64) -> Result<Calculator, ComputationError> {
        let start = Instant::now();
        let value = op.perform(lhs, rhs)?;

        Ok(Calculator {
            state: ComputationState::Computed {
                result: ComputationResult {
                    value,
                    computation_time: start.elapsed()
                }
            }
        })
    }

    /// Performs a calculation using the supplied binary operator, the last computation result, and the supplied operand.
    ///
    /// The supplied operand will be the right-hand side in the mathematical operation.
    pub fn calculate_more(&self, op: Arc<dyn BinaryOperator>, rhs: i64) -> Result<Calculator, ComputationError> {
        let ComputationState::Computed { result } = &self.state else {
            return Err(ComputationError::IllegalComputationWithInitState);
        };

        let start = Instant::now();
        let value = op.perform(result.value, rhs)?;

        Ok(Calculator {
            state: ComputationState::Computed {
                result: ComputationResult {
                    value,
                    computation_time: start.elapsed()
                }
            }
        })
    }
}

#[derive(uniffi::Object)]
struct SafeAddition {}

// Makes it easy to construct from foreign code
#[uniffi::export]
impl SafeAddition {
    #[uniffi::constructor]
    fn new() -> Self {
        SafeAddition {}
    }
}

#[uniffi::export]
impl BinaryOperator for SafeAddition {
    fn perform(&self, lhs: i64, rhs: i64) -> Result<i64, ComputationError> {
        lhs.checked_add(rhs).ok_or(ComputationError::Overflow)
    }
}

}

For usage in Rust on how to use uniffi’s proc-macros, see the uniffi-rs book for Procedural Macros: Attributes and Derives.

This section is about how the generated Typescript maps onto the Rust idioms available.

A useful way of organizing this is via the types that can be passed across the FFI.

Simple scalar types

RustTypescript
Unsigned integersu8, u16, u32numberPositive numbers only
Signed integersi8, i16, i32number
Floating pointf32, f64number
64 bit integersu64, i64bigintMDN
StringsStringstringUTF-8 encoded

Other simple types

RustTypescript
Byte arrayVec<u8>ArrayBufferMDN
Timestampstd::time::SystemTimeDatealiased to UniffiTimestamp
Durationstd::time::Durationnumber msaliased to UniffiDuration

Structural types

RustTypescript
OptionalOption<T>T | undefined
SequencesVec<T>Array<T>Max length is 2**31 - 1
MapsHashMap<K, V>
BTreeMap<K, V>
Map<K, V>Max length is 2**31 - 1

Enumerated types

RustTypescript
EnumsenumenumFlat enums
Tagged Union Types)enumTagged unionsEnums with properties
Error enumsenumsError

Struct types

RustTypescript
Objectsstruct Foo {}class Fooclass objects with methods
Recordsstruct Bar {}type Bar = {}objects without methods
Error objectsstruct Baz {}Errorobject is a property of the Error

Objects

Objects are structs with methods. They are passed-by-reference across the FFI.

#![allow(unused)]
fn main() {
#[derive(uniffi::Object)]
struct MyObject {
    my_property: u32,
}

#[uniffi::export]
impl MyObject {
    fn new(num: u32) -> Self {
        Self {
            my_property: num,
        }
    }

    #[uniffi::constructor(name = "create")]
    fn secondary_constructor(num: u32) -> Self {
        Self::new(num)
    }

    fn my_method(&self) -> String {
        format!("my property is {}", self.my_property)
    }
}
}

This produces Typescript with the following shape:

interface MyObjectInterface {
    myMethod(): string;
}

class MyObject implements MyObjectInterace {
    public constructor(num: number) {
        // …
        // call into the `new` function.
    }

    public static create(num: number): MyObjectInterface {
        // … secondary constructor
        // call into `secondary_constructor` function.
    }

    myMethod(): string {
        // call into the `my_method` method.
    }
}

Object interfaces

A supporting interface is constructed for each object, with the naming pattern: ${OBJECT_NAME}Interface.

This is used for return values and arguments elsewhere in the generated code.

e.g. a Rust function, my_object that returns a MyObject is written in Typescript as:

function myObject(): MyObjectInterface

This is to support mocking of Rust objects.

Uniffi traits

Implementing the following traits in Rust causes the corresponding methods to be generated in Typescript:

TraitTypescript methodReturn
DisplaytoString()string
DebugtoDebugString()string
Eqequals(other)boolean
HashhashCode()bigint

Garbage collection

When the object is garbage collected, the Rust native peer is dropped.

If the Rust object needs to be explicitly dropped, use the uniffiDestroy() method.

This will cause the reference to the object to be freed. If this is the last reference to be freed, then the object itself is dropped.

Hermes Garbage Collection and Rust Drop

In Rust, the compile-time memory management is fairly sophisticated: ownership and borrowing is a first class concept and a whole subsystem of the compilation process is called the borrow-checker. When an structure’s ownership is not passed on, then it is dropped. When dropped, if it implements the Drop trait, the drop function can be run. In addition, any members of a dropped object will also be dropped.

In this manner, resources can be closed and memory can be reclaimed. Rust uses the Resource Acquisition Is Initialization idiom, and its opposite: resource reclamation is deinitialization.

In Javascript, there is a garbage collector. More concretely it uses a mark-and-sweep garbage collector.

At the boundary between the Rust code and Javascript code, Uniffi has to worry about marrying the two programming models.

The Javascript programmer is handling a Javascript object which is facade onto a Rust object. In the literature this is known as a native-peer. For the JS programmer, the mental model would be that once the object becomes unreachable, then sometime in the future, the GC will reclaim the memory.

But for the Rust, things do not get dropped, and cleanup operations don’t get done.

There are several possible approaches:

  1. Let the Javascript programmer explicitly tell Rust when they are done with a native peer. This is least convenient for the programmer: if they forget to do this, then a potential memory leak occurs.
  2. Somehow persuade the garbage collector to tell Rust that something has fallen out of usage. This is convenient for the programmer, but the GC is not guaranteed to be run.
  3. Do both: get the GC to do easy things automatically, to avoid memory leaks, but also provide explicit API to destroy the native peer.

Current status

Garbage collected objects trigger a drop call into Rust

The simplest route for this would be to use a FinalizationRegistry.

Unfortunately, this is not yet supported by hermes. (New issue)

Instead, for every Javascript object constructor called, we call create a DestructibleObject in C++, that is represented in Javascript but has a C++ destructor.

At the end of this [C++] object’s lifetime, the destructor is called.

The assumptions here are:

  1. GC reclaims the memory through destruction of the C++ object
  2. the same C++ is used throughout the JS lifetime, i.e. memory compaction doesn’t exist, or if it does, then objects are moved rather than cloned.

Additionally, we observe that:

  1. Garbage collection may happen later than you think, if at all; especially in short running tests or apps.
  2. Garbage collection may happen sooner than you think, especially in Release.
  3. If your Rust object depends on a drop function being called, then you should call its uniffiDestroy method before losing it.

Explicit API for destroying the native peer

For every object, there is a uniffiDestroy method. This can be called more than once. Once it is called, calling any methods on that object results in an error.

To make calling this more automatic, in some circumstances it may be useful to use the uniffiUse method:

const result = new MyObject().uniffiUse((obj) => {
    obj.callSomeMethod();
    return obj.callAnotherMethod();
});

Future work

If there is any movement on hermes’ FinalizationRegistry support) we may well consider moving to this method.

Records

Uniffi records are data objects whose fields are serialized and passed over the FFI i.e. pass by value. They do not have methods.

In UDL, they may be specified with the dictionary keyword:

dictionary MyRecord {
  string mandatory_property;
  string defaulted_property = "Specified by UDL or Rust";
};

Alternatively, they are specified using a Rust proc-macro:

#![allow(unused)]
fn main() {
#[derive(uniffi::Record)]
struct MyRecord {
    mandatory_property: String,
    #[uniffi(default = "Specified by UDL or Rust")]
    defaulted_property: String,
}
}

They are implemented as bare objects in Javascript, with a type declaration in Typescript.

type MyRecord = {
    mandatoryProperty: string,
    defaultedProperty: string,
};

Using this scheme alone however, Typescript cannot represent the default values provided by the UDL, or Rust.

To correct this, uniffi-bindgen-react-native generates a companion factory object.

const MyRecord = {
    create(fields: Missing<MyRecord>) { … },
    defaults(): Partial<MyRecord> { … },
    new: create // a synonym for `create`.
};

The Missing<MyRecord> type above is a little bit hand-wavy, but it’s defined as the union of non-defaulted fields, and the partial of the defaulted fields.

So, continuing with our example, the factory will be minimally happy with:

const myRecord = MyRecord.create({
    mandatoryProperty: "Specified in Typescript"
});

assert(myRecord.mandatoryProperty === "Specified in Typescript");
assert(myRecord.defaultProperty === "Specified by UDL or Rust");

Enums without properties

Enums with variants that have no properties are said to be “flat enums”.

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
enum MyAnimal {
    Cat,
    Dog,
}
}

These are represented by a similar enum in Typescript:

enum MyAnimal {
    Cat,
    Dog,
}

Constructing these in Typescript is done as usual:

const dog = MyAnimal.Dog;
const cat = MyAnimal.Cat;

Enums with properties

Rust enums variants optionally have properties. These may be name or unnamed.

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
enum MyShape {
  Point,
  Circle(f64),
  Rectangle { length: f64, width: f64, colour: String },
}
}

These may be constructed like so:

#![allow(unused)]
fn main() {
let p = MyShape::Point;
let c = MyShape::Circle(2.0);
let r = MyShape::Rectangle { length: 1.0, width: 1.0, colour: "blue".to_string(), };
}

These can be used in pattern matching, for example:

#![allow(unused)]
fn main() {
fn area(shape: MyShape) -> f64 {
  match shape {
    MyShape::Point => 0.0,
    MyShape::Circle(radius) => PI * radius * radius,
    MyShape::Rectangle { length, width, .. } => length * width,
  }
}
}

Such enums are in all sorts of places in Rust: Option, Result and Errors all use this language feature.

In Typescript, we don’t have enums with properties, but we can simulate them:

enum MyShape_Tags { Point, Circle, Rectangle };
type MyShape =
    { tag: MyShape_Tags.Point } |
    { tag: MyShape_Tags.Circle, inner: [number] } |
    { tag: MyShape_Tags.Rectangle, inner: { length: number, width: number, colour: string }};

In order to make them easier to construct, a helper object containing sealed classes implementing the tag/inner:

const point = new MyShape.Point();
const circle = new MyShape.Circle(2.0);
const rectangle = new MyShape.Circle({ length: 1.0, width: 1.0, colour: "blue" });

These are arranged so that the Typescript compiler can derive the types when you match on the tag:

function area(shape: MyShape): number {
    switch (shape.tag) {
        case MyShape_Tags.Point:
            return 0.0;
        case MyShape_Tags.Circle: {
            const [radius] = shape.inner;
            return Math.PI * radius ** 2;
        }
        case MyShape_Tags.Rectangle: {
            const [length, width] = shape.inner;
            return length * width;
        }
    }
}

instanceOf methods

Both the enum and each variant have instanceOf methods. These may be useful when you don’t need to match/switch on all variants in the Enum.

function colour(shape: MyShape): string | undefined {
    if (MyShape.Rectangle.instanceOf(shape)) {
        // We know what the type inner is.
        return shape.inner.colour;
    }
    return undefined;
}

Tip

Adding one or more properties to one or more variants moves these flat enums to being “non-flat”, as above.

To help switch between the two, the classes representing the variants have a static method new. For example, adding a property to the MyAnimal enum above:

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
enum MyAnimal {
    Cat,
    Dog(String),
}
}

This would mean changing the typescript construction to:

const dog = new MyAnimal.Dog("Fido");
const cat = new MyAnimal.Cat();

The variants each have a static new method to have a smaller diff:

const dog = MyAnimal.Dog.new("Fido");
const cat = MyAnimal.Cat.new();

Enums with explicit discriminants

Both Rust and Typescript allow you to specify discriminants to enum variants. As in other bindings for uniffi-rs, this is suppoorted by uniffi-bindgen-react-native. For example,

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

will cause this Typescript to be generated:

enum MyEnum {
    Foo = 3,
    Bar = 4,
}

Errors

In Javascript, errors are thrown when an error condition is found.

When calling code which can throw, it is good practice to wrap that code in a try/catch block:

try {
    const result = divide(42, 0); // throws
} catch (e: any) {
    // do something with the error.
}

In other languages, e.g. Java or Swift, the method that can throw must declare it on the method signature. e.g.

In Java:

float divide(float top, float bottom) throws MathException {}

while in Swift:

func divide(top: Float, bottom: Float) throws -> Float {}

In Rust, instead of throwing with try/catch, a method returns a Result enum.

#![allow(unused)]
fn main() {
#[derive(uniffi::Error)]
pub enum MathError {
    DivideByZero,
    NumberOverflow,
}

#[uniffi::export]
fn divide(top: f64, bottom: f64) -> Result<f64, MathError> {
    if bottom == 0.0 {
        Err(MathError::DivideByZero)
    } else {
        Ok(top / bottom)
    }
}
}

Enums as Errors

Notice that MathError is not itself a special kind of object. In idiomatic Rust, this is usually an enum.

uniffi-bindgen-react-native converts these types of enums-as-errors in to JS Errors. Due to a limitation in babel, subclasses of Error do not evaluate instanceof as expected. For this reason, each variant has its own instanceOf static method.

try {
    divide(x, y);
} catch (e: any) {
    if (MathError.instanceOf(e)) {
        e instanceof Error; // true
        e instanceof MathError; // false
    }

    if (MathError.DivideByZero.instanceOf(e)) {
        // handle divide by zero
    }
}

Such enums as errors, without properties also have a companion _Tags enum.

Using a switch on the error’s tag property is a convenient way of handling all cases:

try {
    divide(x, y);
} catch (e: any) {
    if (MathError.instanceOf(e)) {
        switch (e.tag) {
            case MathError_Tags.DivideByZero: {
                // handle divide by zero
                break;
            }
            case MathError_Tahs.NumberOverflow: {
                // handle overflow
                break;
            }
        }
    }
}

Enums with properties as Errors

Enums-as-errors may also have properties. These are exactly the same as other enums with properties, except they subclass Error.

e.g.

#![allow(unused)]
fn main() {
enum MyRequestError {
    UrlParsing(String),
    Timeout { timeout: u32 },
    ConnectionLost,
}

#[uniffi::export]
fn make_request() -> Result<String, MyRequestError> {
    // dummy implmentation.
    return Err(MyRequestError::ConnectionLost);
}
}

In typescript:

try {
    makeRequest();
} catch (e: any) {
    if (MyRequestError.instanceOf(e)) {
        switch (e.tag) {
            case MyRequestError_Tags.UrlParsing: {
                console.error(`Url is bad ${e.inner[0]}!`);
                break;
            }
            case MyRequestError_Tags.Timeout: {
                const { timeout } = e.inner;
                console.error(`Timeout after ${timeout} seconds!`);
                break;
            }
            case MyRequestError_Tags.ConnectionLost {
                console.error(`Connection lost!`);
                break;
            }
        }
    }
}

Flat errors

A common pattern in Rust is to convert enum properties to a message. Uniffi calls these error enums flat_errors.

In this example, a MyError::InvalidDataError has no properties but gets the message "Invalid data", ParseError converts its properties in to a message, and JSONError takes any serde_json::Error to make a JSONError, which then gets converted to a string.

In this case, the conversion is being managed by the thiserror crate’s macros.

#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum MyError {
    // A message from a variant with no properties
    #[error("Invalid data")]
    InvalidDataError,

    // A message from a variant with named properties
    #[error("Parse error at line {line}, column {col}")]
    ParseError { line: usize, col: usize },

    // A message from an JSON error, converted into a MyError
    #[error("JSON Error: {0}")]
    JSONError(#[from] serde_json::Error),
}
}

Unlike flat enums, flat errors have a tag property and a companion MyError_Tags enum.

These can be handled in typescript like so:

try {
    // … do sometihng that throws
} catch (err: any) {
    if (MyError.instanceOf(err)) {
        switch (err.tag) {
            case MyError_Tags.InvalidDataError: {
                // e.message will be "MyError.InvalidDataError: Invalid data"
                break;
            }
            case MyError_Tags.ParseError: {
                // e.message will be paramterized, e.g.
                // "MyError.ParseError: Parse error at line 12, column 4"
                break;
            }
            case MyError_Tags.JSONError: {
                // e.message will be a wrapped serde_json error, e.g.
                // "MyError.JSONError: Expected , \" or \]"
                break;
            }
        }
    }
}

Objects as Errors

As you may have gathered, in Rust errors can be anything including objects. In the rare occasions this may be useful:

#![allow(unused)]
fn main() {
#[derive(uniffi::Object)]
pub struct MyErrorObject {
    e: String,
}

#[uniffi::export]
impl MyErrorObject {
    fn message_from_rust(&self) -> String {
        self.e.clone()
    }
}

#[uniffi::export]
fn throw_object(message: String) -> Result<(), MyErrorObject> {
    Err(MyErrorObject { e: message })
}
}

This is used in Typescript, the error itself is not the object.

try {
    throwObject("a message")
} catch (e: any) {
    if (MyErrorObject.instanceOf(e)) {
        // NOPE
    }
    if (MyErrorObject.hasInner(e)) {
        const error = MyErrorObject.getInner(e);
        MyErrorObject.instanceOf(error); // true
        console.error(error.messageFromRust())
    }
}

Rust Error is renamed as Exception in typescript

To avoid collisions with the ECMAScript standard Error, any Rust enums, objects and records called Error are renamed Exception.

Promise / Futures

uniffi-bindgen-react-native provides support of Futures/async fn. These are mapped to Javascript Promises. More information can be found in the uniffi book.

This example is taken from the above link:

#![allow(unused)]
fn main() {
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}!")
}
}

It can be called from Typescript:

// Wait 1 second for Hello, World!
const message = await sayAfter(1000n, "World");

Calling async typescript

The following code calls an async function implemented by Typescript.

#![allow(unused)]
fn main() {
#[uniffi::export(with_foreign)]
#[async_trait::async_trait]
pub trait Greeter {
    async fn create_greeting(&self, delay_ms: i32, who: String) -> String;
}

async fn greet_with(
            greeter: &Greeter,
            delay_ms: i32,
            who: String
) -> String {
    greeter.create_greeting(delay_ms, who).await
}
}

The typescript:

class TsGreeter implements Greeter {
    async createGreeting(delayMs: number, who: string) {
        await new Promise((resolve) => setTimeout(resolve, delayMs));
        return `Hello, ${who} from Typescript!`;
    }
}

const message = greetWith(new TsGreeter(), 1000, "World");

There is no support for passing a Promise or Future as an argument or error, in either direction.

Local development of uniffi-bindgen-react-native

Pre-installation

This guide is in addition to the Pre-installation guide.

git clone https://github.com/jhugman/uniffi-bindgen-react-native
cd uniffi-bindgen-react-native

Now you need to run the bootstrap xtask:

cargo xtask bootstrap

The first time you run this will take some time: it clones the main branch of facebook/hermes and builds it.

By default, it checks out the main branch, but this can be customized:

cargo xtask bootstrap hermes --branch rn/0.76-stable

It also builds the cpp/test-harness which is the Javascript runtime which can accept .so files written in C++ and Rust.

You can force a re-setup with:

cargo xtask bootstrap --force

Tests to see if a bootstrap step can be skipped is fairly rudimentary: mostly just the existence of a directory, so switching to a new branch of hermes would be done:

cargo xtask clean
cargo xtask bootstrap hermes --branch rn/0.76-stable
cargo xtask bootstrap

Running tests

Most of the testing for uniffi-bindgen-react-native is done in the fixtures directory by testing the generated Typescript and C++ against a Rust crate.

These can be run with:

./scripts/run-tests.sh

One or more fixtures may be run using the -f flag.

./scripts/run-tests.sh -f chronological -f arithmetic

This, in turn, uses the run xtask.

The run-tests.sh script also runs the Typescript-only tests in the typescript/tests directory.

These have been useful to prototype generated Typescript before moving them into templates.

Running rust only unit tests

Rust unit tests are encouraged! They can be run as usual with:

cargo test

Formatting and linting

Pre-commit, you should ensure that the code is formatted.

The fmt xtask will run cargo fmt, cargo clippy on the Rust, prettier on Typescript and clang-tidy on C++ files.

cargo xtask fmt

Running with the --check does not change the files, just finishes abnormally if any of the formatters find something it would like changed.

cargo xtask fmt --check

Adding or changing turbo-module templates

In addition to generating the bindings between Hermes and Rust, uniffi-bindgen-react-native generates the files needed to run this as a turbo-module. The list of files are documented elsewhere in this book.

Templates are written for Askama templating library.

Changing the templates for these files is relatively simple. This PR is a good example of adding a file.

Adding a new template

  1. Add new template to the codegen/templates directory.
  2. Add a new RenderedFile struct, which specifies the template, and its path to the files module in codegen/mod.rs.
  3. Add an entry to the list of generated files in this book.

The template context will have quite a lot of useful information data-structures about the project; the most prominent:

Changing generated Typescript or C++ templates

The central workings of a uniffi-bingen are its templates.

uniffi-bindgen-react-native templates are in the following directories:

Templates are written for Askama templating library.

There is a small-ish runtime for both languages:

This is intended to allow developers from outside the project to contribute more easily.

Making a change to the templates should be accompanied by an additional test, either in an existing test fixture, or a new one.

Running the tests can be done with:

./scripts/run-tests.sh

An individual test can be run:

./scripts/run-tests.sh -f $fixtureName

Cutting a Release

Version numbers

Uniffi has a semver versioning scheme. At time of writing, the current version of uniffi-rs is 0.28.3

uniffi-bindgen-react-native uses this version number with prepended with a - and a variant number, starting at 0.

Thus, at first release, the version of uniffi-bindgen-react-native will be 0.28.3-0.

Compatibility with other packages

Other versioning we should take care to note:

  • React Native
  • create-react-native-library

uniffi-bindgen-react-native the command line utility that ties together much of the building of Rust, and the generating the bindings and turbo-modules. It is also available called ubrn.

Most commands take a --config FILE option. This is a YAML file which collects commonly used options together, and is documented here.

Both spellings of the command ubrn and uniffi-bindgen-react-native are NodeJS scripts.

This makes ubrn available to other scripts in package.json.

If you find yourself running commands from the command line, you can alias the command

alias ubrn=$(yarn uniffi-bindgen-react-native --path)

allows you to run the command from the shell as ubrn, which is simpler to type. From hereon, commands will be given as ubrn commands.

The ubrn command

Running ubrn --help gives the following output:

Usage: uniffi-bindgen-react-native <COMMAND>

Commands:
  checkout  Checkout a given Github repo into `rust_modules`
  build     Build (and optionally generate code) for Android or iOS
  generate  Generate bindings or the turbo-module glue code from the Rust
  help      Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

checkout

Checkout a given Git repo into rust_modules.

Usage: uniffi-bindgen-react-native checkout [OPTIONS] <REPO>

Arguments:
  <REPO>  The repository where to get the crate

Options:
      --config <CONFIG>
      --branch <BRANCH>  The branch or tag which to checkout [default: main]
  -h, --help             Print help

The checkout command can be operated in two ways, either:

  1. with a REPO argument and optional --branch argument. OR
  2. with a config file which may specify a repo and branch, or just a directory.

If the config file is set to a repo, then the repo is cloned in to ./rust_modules/${NAME}.

build

This takes care of the work of compiling the Rust, ready for generating bindings. Each variant takes a:

  • --config config file.
  • --and-generate this runs the generate all command immediately after building.
  • --targets a comma separated list of targets, specific to each platform. This overrides the values in the config file.
  • --release builds a release version of the library.

build android

Build the crate for use on an Android device or emulator, using cargo ndk, which in turn uses Android Native Development Kit.

Usage: uniffi-bindgen-react-native build android [OPTIONS] --config <CONFIG>

Options:
      --config <CONFIG>
          The configuration file for this build

  -t, --targets <TARGETS>...
          Comma separated list of targets, that override the values in the `config.yaml` file.

          Android: aarch64-linux-android, armv7-linux-androideabi, x86_64-linux-android i686-linux-android,

          Synonyms for: arm64-v8a, armeabi-v7a, x86_64, x86

  -r, --release
          Build a release build

      --no-cargo
          If the Rust library has been built for at least one target, then don't re-run cargo build.

          This may be useful if you are using a pre-built library or are managing the build process yourself.

  -g, --and-generate
          Optionally generate the bindings and turbo-module code for the crate

      --no-jniLibs
          Suppress the copying of the Rust library into the JNI library directories

  -h, --help
          Print help (see a summary with '-h')

--release sets the release profile for cargo.

--and-generate is a convenience option to pass the built library file to generate bindings and generate turbo-module for Android and common files.

This is useful as some generated files use the targets specified in this command.

Once the library files (one for each target) are created, they are copied into the jniLibs specified by the YAML configuration.

Note

React Native requires that the Rust library be built as a static library. The CMake based build will combine the C++ with the static library into a shared object.

To configure Rust to build a static library, you should ensure staticlib is in the crate-type list in the [lib] section of the Cargo.toml file. Minimally, this should be in the Cargo.toml manifest file:

[lib]
crate-type = ["staticlib"]

We also need to make sure that we were linking to the correct NDK.

This changes from RN version to version, but in our usage we had to set an ANDROID_NDK_HOME variable in our script for this to pick up the appropriate version. For example:

export ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/26.1.10909125/

You can find the version you need in your react-native android/build.gradle file in the ndkVersion variable.

build ios

Build the crate for use on an iOS device or simulator.

Build the crate for use on an iOS device or simulator

Usage: uniffi-bindgen-react-native build ios [OPTIONS] --config <CONFIG>

Options:
      --config <CONFIG>
          The configuration file for this build

      --sim-only
          Only build for the simulator

      --no-sim
          Exclude builds for the simulator

      --no-xcodebuild
          Does not perform the xcodebuild step to generate the xcframework

          The xcframework will need to be generated externally from this tool. This is useful when adding extra bindings (e.g. Swift) to the project.

  -t, --targets <TARGETS>...
          Comma separated list of targets, that override the values in the `config.yaml` file.

          iOS: aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios

  -r, --release
          Build a release build

      --no-cargo
          If the Rust library has been built for at least one target, then don't re-run cargo build.

          This may be useful if you are using a pre-built library or are managing the build process yourself.

  -g, --and-generate
          Optionally generate the bindings and turbo-module code for the crate

  -h, --help
          Print help (see a summary with '-h')

The configuration file refers to the YAML configuration.

--sim-only and --no-sim restricts the targets to targets with/without sim in the target triple.

--and-generate is a convenience option to pass the built library file to generate bindings and generate turbo-module for iOS and common files.

This is useful as some generated files use the targets specified in this command.

Once the target libraries are compiled, and a config file is specified, they are passed to xcodebuild -create-xcframework to generate an xcframework.

Note

React Native requires that the Rust library be built as a static library. The xcodebuild based build will combine the C++ with the static library .xcframework file.

To configure Rust to build a static library, you should ensure staticlib is in the crate-type list in the [lib] section of the Cargo.toml file. Minimally, this should be in the Cargo.toml manifest file:

[lib]
crate-type = ["staticlib"]

generate

This command is to generate code for:

  1. turbo-modules: installing the Rust crate into a running React Native app
  2. bindings: the code needed to actually bridge between Javascript and the Rust library.

All subcommands require a configuration file.

If you’re already using --and-generate, then you don’t need to know how to invoke this command.

Generate bindings or the turbo-module glue code from the Rust.

These steps are already performed when building with `--and-generate`.

Usage: uniffi-bindgen-react-native generate <COMMAND>

Commands:
  bindings      Generate just the Typescript and C++ bindings
  turbo-module  Generate the TurboModule code to plug the bindings into the app
  all           Generate the Bindings and TurboModule code from a library file and a YAML config file
  help          Print this message or the help of the given subcommand(s)

Options:
  -h, --help
          Print help (see a summary with '-h')

generate bindings

Generate just the bindings. In most cases, this command should not be called directly, but with the build, with --and-generate.

Info

This command follows the command line format of other uniffi-bindgen commands. Most arguments are passed straight to uniffi-bindgen::library_mode::generate_bindings.

For more/better documentation, please see the linked docs.

Warning

Because this mirrors other uniffi-bindgens, the --config option here is asking for a uniffi.toml file.

This command will generate two typescript files and two C++ files per Uniffi namespace. These are: namespace.ts, namespace-ffi.ts, namespace.h, namespace.cpp, substituting namespace for names derived from the Rust crate.

The namespace is defined as:

The string namespace within which this API should be presented to the caller.

This string would typically be used to prefix function names in the FFI, to build a package or module name for the foreign language, etc.

It may also be thought of as a crate or sub-crate which exports uniffi API.

The C++ files will be put into the --cpp-dir and the typescript files into the --ts-dir.

The C++ files can register themselves with the Hermes runtime.

Usage: uniffi-bindgen-react-native generate bindings [OPTIONS] --ts-dir <TS_DIR> --cpp-dir <CPP_DIR> <SOURCE>

Arguments:
  <SOURCE>
          A UDL file or library file

Options:
      --lib-file <LIB_FILE>
          The path to a dynamic library to attempt to extract the definitions from and extend the component interface with

      --crate <CRATE_NAME>
          Override the default crate name that is guessed from UDL file path.

          In library mode, this

      --config <CONFIG>
          The location of the uniffi.toml file

      --library
          Treat the input file as a library, extracting any Uniffi definitions from that

      --no-format
          By default, bindgen will attempt to format the code with prettier and clang-format

      --ts-dir <TS_DIR>
          The directory in which to put the generated Typescript

      --cpp-dir <CPP_DIR>
          The directory in which to put the generated C++

  -h, --help
          Print help (see a summary with '-h')

generate turbo-module

Generate the TurboModule code to plug the bindings into the app.

More details about the files generated is shown here.

Usage: uniffi-bindgen-react-native generate turbo-module --config <CONFIG> [NAMESPACES]...

Arguments:
  [NAMESPACES]...  The namespaces that are generated by `generate bindings`

Options:
      --config <CONFIG>  The configuration file for this build
  -h, --help             Print help

The namespaces in the commmand line are derived from the crate that has had its bindings created.

Info

The locations of the files are derived from the configuration file and the project’s package.json` file.

The relationships between files are preserved– e.g. where one file points to another via a relative path, the relative path is calculated from these locations.

generate all

This command performs the generation of both bindings and turbo-module, using a lib.a file.

This is a convenience method for users who do not or cannot use the ubrn build commands.

Generate the Bindings and TurboModule code from a library file and a YAML config file.

This is the second step of the `--and-generate` option of the build command.

Usage: uniffi-bindgen-react-native generate all --config <CONFIG> <LIB_FILE>

Arguments:
  <LIB_FILE>
          A path to staticlib file

Options:
      --config <CONFIG>
          The configuration file for this project

  -h, --help
          Print help (see a summary with '-h')

help

Prints the help message.

Usage: uniffi-bindgen-react-native <COMMAND>

Commands:
  checkout  Checkout a given Github repo into `rust_modules`
  build     Build (and optionally generate code) for Android or iOS
  generate  Generate bindings or the turbo-module glue code from the Rust
  help      Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

You can add --help to any command to get more information about that command.

Configuration for uniffi-bindgen-react-native

The configuration yaml file is a collection of configuration options used in one or more commands.

The file is designed to be easy to start. A minimal configuation would be:

rust:
  directory: ./rust
  manifest-file: Cargo.toml

Getting started from here would require a command to start the Rust:

cargo init --lib ./rust
cd ./rust
cargo add uniffi

YAML entries

rust

rust:
	repo: https://github.com/example/my-rust-sdk
	branch: main
	manifest-path: crates/my-api/Cargo.toml

In this case, the ubrn checkout command will clone the given repo with the branch/ref into the rust_modules directory of the project. Note that instead of branch you can also use rev or ref.

If run a second time, no overwriting will occur.

The manifest-path is the path relative to the root of the Rust workspace directory. In this case, the manifest is expected to be, relative to your React Native library project: ./rust_modules/my-rust-sdk/crates/my-api/Cargo.tml.

crate:
	directory: ./rust
	manifest-path: crates/my-api/Cargo.toml

In this case, the ./rust directory tells ubrn where the Rust workspace is, relative to your React Native library project. The manifest-path is the relative path from the workspace file to the crate which will be used to build bindings.

bindings

This section governs the generation of the bindings— the nitty-gritty of the Rust API translated into Typescript. This is mostly the location on disk of where these files will end up, but also has a second configuration file.

bindings:
	cpp: cpp/bindings
	ts: ts/bindings
	uniffiToml: ./uniffi.toml

The uniffi.toml file configures custom types, to further customize the conversion into Typescript data-types.

If missing, the defaults will be used:

bindings:
	cpp: cpp/generated
	ts: ts/generated

android

This is to configure the build steps for the Rust, the bindings, and the turbo-module code for Android.

This section can be omitted entirely, as sensible defaults are provided. If you do want to edit the defaults, these are the members of the android section with their defaults:

android:
	directory: ./android
	cargoExtras: []
	targets:
	- aarch64-linux-android
	- armv7-linux-androideabi
	- i686-linux-android
	- x86_64-linux-android
	apiLevel: 21
	jniLibs: src/main/jniLibs
	packageName: <DERIVED FROM package.json>

The directory is the location of the Android project, relative to the root of the React Native library project.

targets is a list of targets to build for. The Rust source code is built once per target.

cargoExtras is a list of extra arguments passed directly to the cargo build command.

apiLevel is the minimum API level to target: this is passed to the cargo ndk command as a --platform argument.

Tip

Reducing the number of targets to build for will speed up the edit-compile-run cycle.

packageName is the name of the Android package that Codegen used to generate the TurboModule. This is derived from the package.json file, and can almost always be left.

To customize the packageName, you should edit or add the entry at the path codegenConfig/android/javaPackageName in package.json.

ios

This is to configure the build steps for the Rust, the bindings, and the turbo-module code for iOS.

This section can be omitted entirely, as sensible defaults are provided. If you do want to edit the defaults, these are the members of the ios section with their defaults:

ios:
	directory: ios
	cargoExtras:: []
	targets:
	- aarch64-apple-ios
	- aarch64-apple-ios-sim
	xcodebuildExtras: []
	frameworkName: build/MyFramework

The directory is the location of the iOS project, relative to the root of the React Native library project.

targets is a list of targets to build for. The Rust source code is built once per target.

cargoExtras is a list of extra arguments passed directly to the cargo build command.

xcodebuildExtras is a list of extra arguments passed directly to the xcodebuild command.

turboModule

This section configures the location of the Typescript and C++ files generated by the generate turbo-module command.

If absent, the defaults will be used:

turboModule:
    ts: src
    cpp: cpp

The Typescript files are the index.ts file, and the Codegen installer file.

Info

By default, the index.ts file is intended to be the entry point for your library.

In this case, changing the location of the ts directory will require changing the main or react-native entry in the package.json file.

noOverwrite

This list of glob patterns of file that should not be generated or overwritten by the --and-generate flag, and the generate turbo-module command.

This is useful if you have customized one or more of the generated files, and do not want lose those changes.

The uniffi.toml file is a toml file used to customize the generation of C++ and Typescript.

As of time of writing, only typescript bindings generation exposes any options for customization, and only for customTypes.

Typescript custom types

From the uniffi-rs manual:

Custom types allow you to extend the UniFFI type system to support types from your Rust crate or 3rd party libraries. This works by converting to and from some other UniFFI type to move data across the FFI.

This table customizes how a type called MillisSinceEpoch comes out of Rust.

We happen to know that it crosses the FFI as a Rust i64, which converts to a JS bigint, but we can do better.

[bindings.typescript.customTypes.MillisSinceEpoch]
# Name of the type in the Typescript code.
typeName = "Date"
# Expression to lift from `bigint` to the higher-level representation `Date`.
lift = 'new Date(Number({}))'
# Expression to lower from `Date` to the low-level representation, `bigint`.
lower = "BigInt({}.getTime())"

This table customizes how a type called Url comes out of Rust. We happen to know that it crosses the FFI as a string.

[bindings.typescript.customTypes.Url]
# We want to use our own Url class; because it's also called
# Url, we don't need to specify a typeName.
# Import the Url class from ../src/converters
imports = [ [ "Url", "../src/converters" ] ]
# Expressions to convert between strings and URLs.
# The `{}` is substituted for the value.
lift = "new Url({})"
lower = "{}.toString()"

We can provide zero or more imports which are slotted into a JS import statement. This allows us to import type and from modules in node_modules.

The next example is a bit contrived, but allows us to see how to customize a generated type that came from Rust.

The EnumWrapper is defined in Rust as:

#![allow(unused)]
fn main() {
pub struct EnumWrapper(MyEnum);
uniffi::custom_newtype!(EnumWrapper, MyEnum);
}

In the uniffi.toml file, we want to convert the wrapped MyEnum into a string. In this case, the string is the custom type, and we need to provide code to convert to and from the custom type.

[bindings.typescript.customTypes.EnumWrapper]
typeName = "string"
# An expression to get from the custom (a string), to the underlying enum.
lower = "{}.indexOf('A') >= 0 ? new MyEnum.A({}) : new MyEnum.B({})"
# An expression to get from the underlying enum to the custom string.
# It has to be an expression, so we use an immediately executing anonymous function.
lift = """((v: MyEnum) => {
    switch (v.tag) {
        case MyEnum_Tags.A:
            return v.inner[0];
        case MyEnum_Tags.B:
            return v.inner[0];
    }
})({})
"""

Generating Turbo Module files to install the bindings

The bindings of the Rust library consist of several C++ files and several typescript files.

There is a host of smaller files that need to be configured with these namespaces, and with configuration from the config YAML file.

These include:

  • For Javascript:
    • An index.ts file, to call into the installation process, initialize the bindings for each namespace, and re-export the generated bindings for client code.
    • A Codegen file, to generates install methods from Javascript to Java and Objective C.
  • For Android:
    • A Package.java and Module.java file, which receives the codegen’d install method calls, to get the Hermes JavascriptRuntime and CallInvokerHolder to pass it via JNI to
    • A cpp-adapter.cpp to receive the JNI calls, and converts those into jsi::Runtime and react::CallInvoker then calls into generic C++ install code.
  • Generic C++ install code:
    • A turbo-module installation .h and .cpp which catches the calls from Android and iOS and registers the bindings C++ with the Hermes jsi::Runtime.
  • For iOS:
    • a Module.h and Module.mm file which receives the codegen’d install method calls, and digs around to find the jsi::Runtime and react::CallInvoker. It then calls into the generic C++ install code.
  • To build for iOS:
    • A podspec file to tell Xcode about the generated files, and the framework name/location of the compiled Rust library.
  • To build for Android
    • A CMakeLists.txt file to configure the Android specific tool chain for all the generated C++ files.
    • The build.gradle file which tells keeps the codegen package name in-sync and configures cmake. (note to self, this could be done from within the CMakeLists.txt file).

Reserved words

Typescript Reserved Words

The following words are reserved words in Typescript.

If the Rust API uses any of these words on their own, the generated typescript is appended with an underscore (_).

Reserved WordsStrict Mode Reserved Words
breakas
caseimplements
catchinterface
classlet
constpackage
continueprivate
debuggerprotected
defaultpublic
deletestatic
doyield
else
enum
export
extends
false
finally
for
function
if
import
in
instanceof
new
null
return
super
switch
this
throw
true
try
typeof
var
void
while
with

e.g.

#![allow(unused)]
fn main() {
#[uniffi::export]
fn void() {}
}

generates valid Typescript:

function void_() {
  // … call into Rust.
}

Error is mapped to Exception

Due to the relative prevalence in idiomatic Rust of an error enum called Error, and the built-in Error class in Javascript, an Error enum is renamed to Exception.

Uniffi Reserved words

In your Rust code, avoid using identifiers beginning with the word uniffi or Uniffi.

Potential collisions

Uniffi adding to your API

Both uniffi, and uniffi-bindgen-react-native tries to stay away from polluting the API with its own identifiers: one of the design goals of the library is to make your Rust library usable in the same way as an idiomatic library.

However, sometimes this is unavoidable.

The following are generated on your behalf, even if you did not specify them:

Methods which may collide, because you can define methods in the same namespace

Interfaces which may be declared, and collide with other types

  • ${NAME}Interface: may collide with another type.

e.g.

#![allow(unused)]
fn main() {
#[derive(uniffi::Object)]
struct Foo {}

#[derive(uniffi::Record)]
struct FooInterface {}
}

The naming of the tags enums for tagged union enums deliberately contains an underscore.

Class and enum class names go through a camel casing which makes this impossible to collide when naming a Rust enum something that collides with the generated Typescript.

e.g.

#![allow(unused)]
fn main() {
// This enum will have a tags enum called MyEnum_Tags
#[derive(uniffi::Enum)]
enum MyEnum {}

// This record will be called MyEnumTags in Typescript.
#[derive(uniffi::Record)]
struct MyEnum_Tags {}
}

Non-collisions

These are methods or members that will not collide under any circumstances because they are defined at a level where user-generated members are not.

Types versus Objects

Records and Enums are generated with both a Typescript type and a Javascript object, of the same name.

These objects of the same name will be referred to as factory objects or helper objects.

Records, i.e. objects without methods

type MyRecord = {
    myProperty: string;
};

const MyRecord = {
    defaults(): Partial<MyRecord>,
    create(missingMembers: Partial<MyRecord>): MyRecord,
    new: MyRecord.create,
};

defaults, create and new will never collide with myProperty because:

  • myProperty is a member of an object of type MyRecord. It is never a member of the object called MyRecord.

Enums with values

Enums define their shape types, with a utility object to hold the variant classes.

To a first approximation, the generated code is drawn:

enum MyShape_Tags { Circle, Rectangle }
type MyShape =
    { tag: MyShape_Tags.Circle, inner: [number;]} |
    { tag: MyShape_Tags.Rectangle, inner: { length: number; width: number; }}

const MyShape = {
    Circle: class Circle { constructor(
        public tag: MyShape_Tags.Circle,
        public inner: [radius: number]) {}
        static instanceOf(obj: any): obj is Circle {}
        static new(…): Circle {}
    },
    Rectangle: class Rectangle { constructor(
        public tag: MyShape_Tags.Rectangle,
        public inner: { length: number; width: number; }) {}
        static instanceOf(obj: any): obj is Rectangle {}
        static new(…): Rectangle {}
    },
    instanceOf(obj: any): obj is MyShape {}
};

This allows us to construct variants with:

const variant: MyShape = new MyShape.Circle(2.0);
MyShape.instanceOf(variant);
MyShape.Circle.instanceOf(variant);

The type MyShape is different to the const MyShape, and typescript can tell the difference based upon the context.

tag, inner and instanceOf do not collide with:

  • variant names, which are all CamelCased.
  • variant value names, which are isolated in the inner object.

Static methods

  • create: a static method in a Record Factory. User defined property, so will never be able
  • hasInner: a static method added to object as Error classes.
  • getInner: a static method added to object as Error classes.
  • instanceOf: a static method added to Object, Enum, Enum variant and Error classes.
  • new: a static method in record factory object