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 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;