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 thethrows
attribute in the .udl), then the uniffi generated scaffolding code willpanic!()
-
If the value is being used as an argument to a function/constructor that does return a
Result
(ie, does have athrows
attribute in the .udl), then the uniffi generated scaffolding code will useanyhow::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 of0
or-1
will always panic. - Calling
take_handle_2
with a value of0
will throw anExampleError
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 yourinto_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;