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.

Logging the FFI

The generated Typescript code can optionally be created to generate logging.

[bindings.typescript]
logLevel = "debug"
consoleImport = "@/hermes"

consoleImport is an optional string which is the location of a module from which a console will be imported. This is useful in environments where console do not exist.

Log level

Possible values:

  • none: The Uniffi generated Typescript produces no logging.
  • debug: The generated Typescript records the call sites of async functions.
  • verbose: As debug but also: all calls into Rust are logged to the console. This can be quite… verbose.

The recording of async call sites is also helpful for app development, so process.env.NODE_ENV !== "production" is checked at startup of runtime.

When process.env.NODE_ENV === "production", async errors detected by Rust are reported but not with a helpful Typescript stack trace. Recording the call sites has a performance cost so is turned off for production.

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];
    }
})({})
"""