Skip to content

Commit

Permalink
New custom type system
Browse files Browse the repository at this point in the history
Implemented a new custom type system described in the new docs.  This
system is easier to use and also allows us to do some extra tricks
behind the scene.  For example, I hope to add a flag that will toggle
between implementing `FfiConverter` for the local tag vs a blanket impl.
It's not possible to do that with the current system and adding support
would be awkward.

I wanted to keep backwards compatibility for a bit, but couldn't figure
out a good way to do it.  The main issue is that for the old system, if
a custom type is declared in the UDL then we generate the `FfiConverter`
implementation, while the new system expects the user to call
`custom_type!` to create the impl.  I tried to get things working by
creating a blanket impl of `FfiConverter` for types that implemented
`UniffiCustomTypeConverter`, but ran into issues -- the first blocker I
found was that there's no way to generate `TYPE_ID_META` since we don't
know the name of the custom type.

Removed the nested-module-import fixture.  The custom type code will no
longer test it once we remove the old code, since it's not using
`UniffiCustomTypeConverter`.  I couldn't think of a way to make it work
again.
  • Loading branch information
bendk committed Jul 23, 2024
1 parent 1946490 commit 83a2328
Show file tree
Hide file tree
Showing 23 changed files with 447 additions and 429 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

[All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.28.0...HEAD).

### ⚠️ 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/0.29/Upgrading.html for help upgrading and https://mozilla.github.io/uniffi-rs/0.29/udl/custom_types.html for details.

## v0.28.0 (backend crates: v0.28.0) - (_2024-06-11_)

### What's new?
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
4 changes: 2 additions & 2 deletions docs/adr/0009-remote-types-interfaces-and-proc-macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ pub enum LogLevel {

/// Define a custom type conversion from `log::Level` to the above type.
uniffi::custom_type!(log::Level, LogLevel, {
from_custom: |l| match l {
into_existing: |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_from_existing: |l| Ok(match l ({
LogLevel::Error => log::Level::Error,
LogLevel::Warn => log::Level::Warn,
LogLevel::Info => log::Level::Info,
Expand Down
35 changes: 35 additions & 0 deletions docs/manual/src/Upgrading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# v0.28.x -> v0.29.x

## Custom types

Custom types are now implemented using a macro, rather than implementing the
`UniffiCustomTypeConverter` trait.

Before:

```rust
impl UniffiCustomTypeConverter for MyType {
type Builtin = UniFFIBuiltinType;

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

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

After:

```
uniffi::custom_type!(MyType, UniFFIBuiltinType, {
try_from_existing: |val| { ... },
into_existing: |obj| { ... },
})
```

The custom_type macro is more flexible than the old system. For example, the `try_from_existing` and
`into_existing` can be omitted in many cases, and will use the `TryInto` and `From` traits by default.
See the [Custom Types](./udl/custom_types.md) for details and other features of the new macro.
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 @@ -354,44 +354,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
146 changes: 89 additions & 57 deletions docs/manual/src/udl/custom_types.md
Original file line number Diff line number Diff line change
@@ -1,72 +1,107 @@
# 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 offer a way to use user-defined types in your interface that don't derive one of the UniFFI type traits (`uniffi::Record`, `uniffi::Enum`, `uniffi::Object`, etc.).
Instead, custom types are converted to/from a existing type that does derive those traits when being passed across the FFI.

Custom types are often used with structs that wraps a primitive type, for example `Guid(String)`.
These types can be passed across the FFI as the primitive type, which can be more efficient than passing a struct.
The foreign bindings will treat these types as the primitive type, for example `Guid` could appear a string to the foreign code.

Custom types can also be customized on the foreign side. For example, a URL could 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

Lastly, the `Guid(String)` example shows another benefit of custom types: they can be used to
support Rust types that are not currently supported by UniFFI like tuple-style structs.

## 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.

```rust
pub struct Handle(i64);

// Some complex struct that can be serialized/deserialized to a string.
use some_mod::SerializableStruct;

/// `SerializableStruct` objects will be passed across the FFI the same way `String` values are.
uniffi::custom_type!(SerializableStruct, String);
```

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

- Values passed to the foreign code will be converted using `<SerializableStruct as Into<String>>` before being lowered as a `String`.
- Values passed to the Rust code will be converted using `<String as TryInto<SerializableStruct>>` after lifted as a `String`.
- The `TryInto::Error` type can be anything that implements `Into<anyhow::Error>`.
- `<String as Into<SerializableStruct>>` will also work, since there is a blanket impl in the core libary.

### custom_type! with manual conversions

You can also manually specify the conversions by passing an extra param to the macro:

```rust
uniffi::custom_type!(SerializableStruct, String, {
into_existing: |s| s.serialize(),
try_from_existing: |s| s.deserialize(),
});
```

### custom_newtype!

The `custom_newtype!` can trivially handle newtypes that wrap a UniFFI type.

```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`.

An implementation is provided if you used the `uniffi::custom_newtype!()` macro.
But if you use UDL or otherwise need to implement your own:
**note**: UDL users still need to call the `custom_type!` or `custom_newtype!` macro in their Rust
code.

This trait is generated by UniFFI and can be found in the generated
Rust scaffolding - it is defined as:
## User-defined types

```Rust
trait UniffiCustomTypeConverter {
type Builtin;
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:

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self>
where
Self: Sized;
fn from_custom(obj: Self) -> Self::Builtin;
}
```
```rust

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:
pub type LogRecord = log::Record;

```rust
impl UniffiCustomTypeConverter for Handle {
type Builtin = i64;
#[derive(uniffi::Record)]
pub type LogRecordData {
level: LogLevel,
message: String,
}

fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
Ok(Handle(val))
uniffi::custom_type!(LogRecord, LogRecordData, {
into_existing: |r| LogRecordData {
level: r.level(),
message: r.to_string(),
}
try_from_existing: |r| LogRecord::builder()
.level(r.level)
.args(format_args!("{}", r.message))
.build()
});

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
Expand Down Expand Up @@ -112,20 +147,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, {
into_existing: |handle| handle.0,
try_from_existing: |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 Down Expand Up @@ -154,7 +183,7 @@ 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()"
into_existing = "{}.toString()"
```

Here's how the configuration works in `uniffi.toml`.
Expand All @@ -168,12 +197,15 @@ Here's how the configuration works in `uniffi.toml`.
* `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
## 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:
To use custom types from other crates, use a typedef wrapped with the `[External]` attribute.
For example, if another crate wanted to use the examples above:

```idl
[External="crate_defining_handle_name"]
typedef extern Handle;
typedef i64 Handle;
[External="crate_defining_log_record_name"]
typedef dictionary LogRecord;
```
Loading

0 comments on commit 83a2328

Please sign in to comment.