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;