Skip to content

Custom types

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. You must provide a UniffiCustomTypeConverter implementation to convert the types.

Custom types in the scaffolding code

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

pub struct Handle(i64);

In this trivial example, the simplest way to expose this is with a macro.

uniffi::custom_newtype!(Handle, i64);

Or you can define this in UDL 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.

An implementation is provided if you used the uniffi::custom_newtype!() macro. But if you use UDL or otherwise need to implement your own:

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;