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 descibed 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 compatability 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 Jun 12, 2024
1 parent cd38cce commit 5ffb2d8
Show file tree
Hide file tree
Showing 20 changed files with 391 additions and 424 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

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

### ⚠️ Breaking Changes ⚠️

- 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.
See https://mozilla.github.io/uniffi-rs/latest/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 @@ -52,7 +52,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
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
132 changes: 76 additions & 56 deletions docs/manual/src/udl/custom_types.md
Original file line number Diff line number Diff line change
@@ -1,72 +1,95 @@
# 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 extend the UniFFI type system by converting to and from some other UniFFI
type to move data across the FFI.

## 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, {
from_custom: |s| s.serialize(),
try_into_custom: |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, {
from_custom: |r| LogRecordData {
level: r.level(),
message: r.to_string(),
}
try_into_custom: |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 +135,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, {
from_custom: |handle| handle.0,
try_into_custom: |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 @@ -168,12 +185,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 5ffb2d8

Please sign in to comment.