Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New custom type system #2150

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

## [[UnreleasedUniFFIVersion]] (backend crates: [[UnreleasedBackendVersion]]) - (_[[ReleaseDate]]_)

### ⚠️ Breaking Changes ⚠️

- The Rust side of the custom type system has changed and users will need to update their code.
The `UniffiCustomTypeConverter` trait is no longer used, use the `custom_type!` macro instead.
We did this to help fix some edge-cases with custom types wrapping types from other crates (eg, Url).
See https://mozilla.github.io/uniffi-rs/next/Upgrading.html for help upgrading and https://mozilla.github.io/uniffi-rs/next/udl/custom_types.html for details.

### What's fixed?

- Fixed bug in metadata extraction with large ELF files.
Expand Down
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ members = [
"fixtures/regressions/swift-callbacks-omit-labels",
"fixtures/regressions/swift-dictionary-nesting",
"fixtures/regressions/unary-result-alias",
"fixtures/regressions/nested-module-import",
"fixtures/regressions/wrong-lower-check",
"fixtures/trait-methods",
"fixtures/uitests",
Expand Down
6 changes: 3 additions & 3 deletions docs/adr/0009-remote-types-interfaces-and-proc-macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,16 @@ pub enum LogLevel {
Trace = 5,
}

/// Define a custom type conversion from `log::Level` to the above type.
/// Define a type conversion from `log::Level` to the custom [`LogLevel`] enum.
uniffi::custom_type!(log::Level, LogLevel, {
from_custom: |l| match l {
lower: |l| match l {
log::Level::Error => LogLevel::Error,
log::Level::Warn => LogLevel::Warn,
log::Level::Info => LogLevel::Info,
log::Level::Debug => LogLevel::Debug,
log::Level::Trace => LogLevel::Trace,
},
try_into_custom: |l| Ok(match l ({
try_lift: |l| Ok(match l ({
LogLevel::Error => log::Level::Error,
LogLevel::Warn => log::Level::Warn,
LogLevel::Info => log::Level::Info,
Expand Down
34 changes: 34 additions & 0 deletions docs/manual/src/Upgrading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# v0.28.x -> v0.29.x

## Custom types

Custom types are now implemented using a macro rather than implementing the `UniffiCustomTypeConverter` trait,
addressing some edge-cases with custom types wrapping types from other crates (eg, Url).

bendk marked this conversation as resolved.
Show resolved Hide resolved
Before:

```rust
impl UniffiCustomTypeConverter for NewCustomType {
type Builtin = BridgeType;

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
...
}

fn from_custom(obj: Self) -> Self::Builtin {
...
}
}
```

After:

```
uniffi::custom_type!(NewCustomType, BridgeType, {
try_lift: |val| { Ok(...) },
lower: |obj| { ... },
})
```

The `custom_type!` macro is more flexible than the old system - eg, the closures can be omitted in many cases where `From` and `Into` exist.
See the [Custom Types](./udl/custom_types.md) for details.
40 changes: 2 additions & 38 deletions docs/manual/src/proc_macro/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,44 +381,8 @@ impl Foo {

## The `uniffi::custom_type` and `uniffi::custom_newtype` macros

There are 2 macros available which allow procmacros to support "custom types" as described in the
[UDL documentation for Custom Types](../udl/custom_types.md)

The `uniffi::custom_type!` macro requires you to specify the name of the custom type, and the name of the
builtin which implements this type. Use of this macro requires you to manually implement the
`UniffiCustomTypeConverter` trait for for your type, as shown below.
```rust
pub struct Uuid {
val: String,
}

// Use `Uuid` as a custom type, with `String` as the Builtin
uniffi::custom_type!(Uuid, String);

impl UniffiCustomTypeConverter for Uuid {
type Builtin = String;

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
Ok(Uuid { val })
}

fn from_custom(obj: Self) -> Self::Builtin {
obj.val
}
}
```

There's also a `uniffi::custom_newtype!` macro, designed for custom types which use the
"new type" idiom. You still need to specify the type name and builtin type, but because UniFFI
is able to make assumptions about how the type is laid out, `UniffiCustomTypeConverter`
is implemented automatically.

```rust
uniffi::custom_newtype!(NewTypeHandle, i64);
pub struct NewtypeHandle(i64);
```

and that's it!
See the [UDL documentation for Custom Types](../udl/custom_types.md). It works exactly the same for
proc-macros.

## The `uniffi::Error` derive

Expand Down
177 changes: 113 additions & 64 deletions docs/manual/src/udl/custom_types.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,140 @@
# 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 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](../internals/lifting_and_lowering.html); 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

Consider the following trivial Rust abstraction for a "handle" which wraps an integer:
### `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`

```rust
/// 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);
```
and `Handle` can be used in Rust, while foreign bindings will use `i64` (or optionally converted, see below)

### `custom_type!` conversions

In this trivial example, the simplest way to expose this is with a macro.
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.

```rust
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 a `String` value.
- Values lifted to the Rust code (eg, a `String`) is then converted using `<String as TryInto<SerializableStruct>>`;
the `TryInto::Error` type can be anything that implements `Into<anyhow::Error>`.
- `TryFrom<String>` and `From<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:
```rust
/// 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);
```

Or you can define this in UDL via a `typedef` with a `Custom` attribute,
defining the builtin type that it's based on.
### UDL

Define custom types in UDL via a `typedef` with a `Custom` attribute, specifying the UniFFI type
followed by the custom type.

```idl
[Custom]
typedef i64 Handle;
```

For this to work, your Rust code must also implement a special trait named
`UniffiCustomTypeConverter`.
**note**: you must still call the `custom_type!` or `custom_newtype!` macros in your Rust code, as described above.

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:
#### Using custom types from other crates

```Rust
trait UniffiCustomTypeConverter {
type Builtin;
To use custom types from other crates from UDL, use a typedef wrapped with the `[External]` attribute.

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self>
where
Self: Sized;
fn from_custom(obj: Self) -> Self::Builtin;
}
For example, if another crate wanted to use the examples here:

```idl
[External="crate_defining_handle_name"]
typedef i64 Handle;
[External="crate_defining_log_record_name"]
typedef dictionary LogRecord;
```
## User-defined types

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

```rust
impl UniffiCustomTypeConverter for Handle {
type Builtin = i64;

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
Ok(Handle(val))
}
pub type LogRecord = log::Record;

fn from_custom(obj: Self) -> Self::Builtin {
obj.0
}
#[derive(uniffi::Record)]
pub type LogRecordData {
level: LogLevel,
message: String,
}
```

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.
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 `into_custom` function returns a `uniffi::Result<Self>` (which is an
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:
Expand Down Expand Up @@ -112,20 +177,14 @@ pub enum ExampleError {
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))
}
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:
Expand All @@ -152,9 +211,9 @@ 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({})"
lift = "URL({})"
# Expression to convert the custom type to the builtin type. In this example, `{}` will be replaced with the URL value.
from_custom = "{}.toString()"
lower = "{}.toString()"
```

Here's how the configuration works in `uniffi.toml`.
Expand All @@ -164,16 +223,6 @@ Here's how the configuration works in `uniffi.toml`.
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:

```idl
[External="crate_defining_handle_name"]
typedef extern Handle;
```
* `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 your `lift`/`lower` functions.
Loading