Custom types
Custom types allow you to create a new UniFFI type which is passed over the FFI as another "bridge" type.
For example, you might have a type named Url
which has a bridge type String
, or a Handle
bridged by an i64
.
Any valid type can be a bridge type - not only builtins, but structs, records, enums etc.
This not only allows using types which otherwise would be impossible over the FFI (eg, url::Url
), but other interesting "newtype" options to extend the type system.
The foreign bindings will treat these types as the bridge type - but they may optionally transform the type. For example, our Url
has a bridged type of string
- we could choose for Kotin to either get that as a String
, or supply a conversion to/from a Kotlin java.net.URL
.
This would mean that Url
would be:
* Represented by the url::Url
type in Rust
* Passed across the FFI as a string
* Represented by the java.net.URL
type in Kotlin
For terminology, we lean on our existing lifting and lowering; in the same way an FFI type is "lifted" into the Rust type, and a Rust type is "lowered" into to FFI, here the bridge type is lifted into our custom type, and our custom type is lowered into the bridge type.
This creates a 2-step lifting/lowering process: our custom type is lifted/lowered to/from the bridge type, then that bridge type lifted/lowered to/from the actual FFI type.
By default, we assume some Into/From
relationships between the types, but you can also supply conversion closures.
Custom types in the scaffolding code
custom_type!
Use the custom_type!
macro to define a new custom type.
The simplest case is for a type with Into/From
already setup - eg, our Handle
/// handle which wraps an integer
pub struct Handle(i64);
// Defining `From<Handle> for i64` also gives us `Into<i64> for Handle`
impl From<Handle> for i64 {
fn from(val: Handle) -> Self {
val.0
}
}
uniffi::custom_type!(Handle, i64);
Handle
can be used in Rust, while foreign bindings will use i64
(or optionally converted, see below)
custom_type!
conversions
You can also manually specify the conversions by passing extra params to the macro. Use this when the trait implementations do not exist, or they aren't desirable for some reason.
uniffi::custom_type!(SerializableStruct, String, {
// Lowering our Rust SerializableStruct into a String.
lower: |s| s.serialize(),
// Lifting our foreign String into our Rust SerializableStruct
try_lift: |s| s.deserialize(),
});
If you do not supply conversions to and from the bridge type, and assuming SerializableStruct
and String
, the following is used:
- Values lowered to the foreign code will be converted using
Into<String>
then lowered as aString
value. - Values lifted to the Rust code (eg, a
String
) is then converted using<String as TryInto<SerializableStruct>>
; theTryInto::Error
type can be anything that implementsInto<anyhow::Error>
. TryFrom<String>
andFrom<SerializableStruct>
will also work, using the blanket impl from the core library.
custom_newtype!
The custom_newtype!
macro is able to handle Rust newtype-style structs which wrap a UniFFI type.
eg, our Handle
object above could be declared as:
/// Handle which wraps an integer
pub struct Handle(i64);
/// `Handle` objects will be passed across the FFI the same way `i64` values are.
uniffi::custom_newtype!(Handle, i64);
UDL
Define custom types in UDL via a typedef
with a Custom
attribute, specifying the UniFFI type
followed by the custom type.
[Custom]
typedef i64 Handle;
You can specify the crate name if the custom type implementation is external.
[Custom="crate_defining_handle_name"]
typedef i64 Handle;
[Custom="crate_defining_log_record_name"]
typedef dictionary LogRecord;
note: you must still call the custom_type!
or custom_newtype!
macros in your Rust code, as described above.
User-defined types
All examples so far in this section convert the custom type to a builtin type.
It's also possible to convert them to a user-defined type (Record, Enum, Interface, etc.).
For example you might want to convert log::Record
class into a UniFFI record:
pub type LogRecord = log::Record;
#[derive(uniffi::Record)]
pub type LogRecordData {
level: LogLevel,
message: String,
}
uniffi::custom_type!(LogRecord, LogRecordData, {
lower: |r| LogRecordData {
level: r.level(),
message: r.to_string(),
}
try_lift: |r| Ok(LogRecord::builder()
.level(r.level)
.args(format_args!("{}", r.message))
.build())
});
Error handling during conversion
You might have noticed that the try_lift
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,
}
uniffi::custom_type!(ExampleHandle, Builtin, {
lower: |handle| handle.0,
try_lift: |value| match value {
0 => Err(ExampleErrors::InvalidHandle.into()),
-1 => Err(SomeOtherError.into()), // SomeOtherError decl. not shown.
n => Ok(Handle(n)),
}
})
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.
lift = "URL({})"
# Expression to convert the custom type to the builtin type. In this example, `{}` will be replaced with the URL value.
lower = "{}.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.lift
: Expression to convert the UDL type to the custom type.{}
will be replaced with the value of the UDL type.lower
: 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 yourlift
/lower
functions.