Skip to content

Commit

Permalink
Experiment: Use Rust code directly as the interface definition.
Browse files Browse the repository at this point in the history
This is a fun little something I've been messing around with,
partly just to learn more about Rust macros and `syn`, and partly
to see if they could make a good replacement for our current use
of WebIDL for an interface definition language.

Key ideas:

* Declare the UniFFI component interface using a restricted subset
  of Rust syntax, directly as part of the code that implements the
  Rust side of the component. There is no separate .udl file.

* Use a procedural macro to insert the Rust scaffolding directly
  at crate build time, rather than requiring a separate `build.rs`
  setup to generate and include it.

* When generating the foreign-language bindings, have `uniffi-bindgen`
  parse the Rust source directly in order to find the component
  interface.

This seems to work surprisingly well so far. If we do decide to
go this route, the code here will need a lot more polish before
it's ready to land...but it works! And it works in a way that's
not too conceptually different from what we're currently doing
with a separate `.udl` file.
  • Loading branch information
rfk committed Aug 4, 2021
1 parent e602e11 commit 9613f96
Show file tree
Hide file tree
Showing 52 changed files with 2,019 additions and 947 deletions.
5 changes: 3 additions & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ Other directories of interest include:
controls how values of various types are passed back-and-forth over the FFI layer, by means of the `ViaFfi` trait.
- **[`./uniffi_build`](../uniffi_build):** This is a small hook to run `uniffi-bindgen` from the `build.rs` script
of a UniFFI component, in order to automatically generate the Rust scaffolding as part of its build process.
- **[`./uniffi_macros`](../uniffi_macros):** This contains some helper macros that UniFFI components can use to
simplify loading the generated scaffolding, and executing foreign-language tests.
- **[`./uniffi_macros`](../uniffi_macros):** This contains the implementation of the
`declare_interface` macro, along with some helper macros that UniFFI components can
use for executing foreign-language tests.
- **[`./fixtures`](../fixtures):** These are various test fixtures which we use to ensure good test coverage and
guard against regressions.

Expand Down
2 changes: 1 addition & 1 deletion docs/manual/src/Getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ fn add(a: u32, b: u32) -> u32 {
```

And top brass would like you to expose this *business-critical* operation to Kotlin and Swift.
**Don't panic!** We will show you how do that using UniFFI.
**Don't panic!** We will show you how to do that using uniffi.
11 changes: 9 additions & 2 deletions docs/manual/src/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ Note that this tool will not help you ship a Rust library to these platforms, bu

## Design

UniFFI requires to write an Interface Definition Language (based on [WebIDL](https://heycam.github.io/webidl/)) file describing the methods and data structures available to the targeted languages.
This .udl (UniFFI Definition Language) file, whose definitions must match with the exposed Rust code, is then used to generate Rust *scaffolding* code and foreign-languages *bindings*. This process can take place either during the build process or be manually initiated by the developer.
UniFFI requires you to declare the interface you want to expose to other languages using a restricted
subset of Rust syntax, along with a couple of helper macros. This interface declaration is used
to generate two things:

* Alongside your hand-written Rust code, UniFFI will generate some Rust *scaffolding* that exposes
your Rust datatypes and functions over a low-level C-compatible FFI.
* For each target foreign language, UniFFI will use the interface declaration to generate
*foreign-language bindings* that consume this low-level FFI and expose it via more idiomatic
higher-level code in the target language.

![uniffi diagram](./uniffi_diagram.png)

Expand Down
19 changes: 10 additions & 9 deletions docs/manual/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
- [Motivation](./Motivation.md)
- [Tutorial](./Getting_started.md)
- [Prerequisites](./tutorial/Prerequisites.md)
- [Describing the interface](./tutorial/udl_file.md)
- [Describing the interface](./tutorial/interface_definition.md)
- [Generating the Rust scaffolding code](./tutorial/Rust_scaffolding.md)
- [Generating the foreign-language bindings](./tutorial/foreign_language_bindings.md)
- [The UDL file](./udl_file_spec.md)
- [Namespace](./udl/namespace.md)
- [Built-in types](./udl/builtin_types.md)
- [Enumerations](./udl/enumerations.md)
- [Structs/Dictionaries](./udl/structs.md)
- [Functions](./udl/functions.md)
- [Throwing errors](./udl/errors.md)
- [Interfaces/Objects](./udl/interfaces.md)
- [The Interface Definition](./interface_definition.md)
- [Namespace](./interface/namespace.md)
- [Built-in types](./interface/builtin_types.md)
- [Records](./interface/records.md)
- [Enumerations](./interface/enumerations.md)
- [Functions](./interface/functions.md)
- [Throwing errors](./interface/errors.md)
- [Objects](./interface/objects.md)
- [Callback Interfaces](./interface/callback_interfaces.md)

# Kotlin

Expand Down
25 changes: 25 additions & 0 deletions docs/manual/src/interface/builtin_types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Built-in types

The following built-in types can be passed as arguments/returned by Rust methods:

| Rust type | Notes |
|-------------------------|-----------------------------------|
| `bool` | |
| `u8/i8..u64/i64` | |
| `f32` | |
| `f64` | |
| `String` | |
| `&T` | This works for `&str` and `&[T]` |
| `Option<T>` | |
| `Vec<T>` | |
| `HashMap<String, T>` | Only string keys are supported |
| `()` | Empty return |
| `Result<T, E>` | See [Errors](./errors.md) section |
| `std::time::SystemTime` | |
| `std::time::Duration` | |

And of course you can use your own types, which is covered in the following sections.

UniFFI does not currently support type aliases, so you cannot do e.g. `type handle = u64`
and then use `handle` as the argument type of your exposed functions. This limitation
may be removed in future.
70 changes: 70 additions & 0 deletions docs/manual/src/interface/callback_interfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Callback interfaces

Callback interfaces are traits that can be implemented by foreign-language code
and used from Rust. They can help provide Rust code with access to features
available to the host language, but not easily replicated in Rust:

* accessing device APIs
* provide glue to clip together Rust components at runtime.
* access shared resources and assets bundled with the app.

Callback interfaces are defined in the interface definition as a `pub trait`
with one or more methods. This toy example defines a way of Rust accessing
a key-value store exposed by the host operating system (e.g. the system keychain).

```rust
trait Keychain: Send {
pub fn get(key: String) -> Option<String>
pub fn put(key: String, value: String)
}
```

In order to actually *use* a callback interface, the Rust code must also
provide some other function or method that accepts it as an argument,
like this:

```rust
pub struct Authenticator {
keychain: Box<dyn Keychain>,
}

impl Authenticator {
pub fn new(keychain: Box<dyn Keychain>) -> Self {
Self { keychain }
}
pub fn login(&self) {
let username = self.keychain.get("username".into());
let password = self.keychain.get("password".into());
// Go ahead and use the credentials...
}
}
```

You can then create a foreign language implementation of the callback interface;
here's an example in Kotlin:

```kotlin
class AndroidKeychain: Keychain {
override fun get(key: String): String? {
// … elide the implementation.
return value
}
override fun put(key: String) {
// … elide the implementation.
}
}
```

And pass the implementation to Rust:

```kotlin
val authenticator = Authenticator(AndroidKeychain())
// later on:
authenticator.login()
```

The generated bindings take care to ensure that once the `Box<dyn Keychain>` is dropped in Rust,
then it is cleaned up in Kotlin.

Also note, that storing the `Box<dyn Keychain>` in the `Authenticator` required that all implementations
*must* implement `Send`.
32 changes: 32 additions & 0 deletions docs/manual/src/interface/enumerations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Enumerations

The interface definition can include Rust enums using standard syntax.
UniFFI supports both flat C-style enums:

```rust
enum Animal {
Dog,
Cat,
}
```

As well as enums with associated data, as long as the variants have named fields:

```rust
enum IpAddr {
V4 {q1: u8, q2: u8, q3: u8, q4: u8},
V6 {addr: string},
}
```

These will be exposed to the foreign-language bindings using a suitable
native enum syntax. For example:

* In Kotlin, flat enums are exposed as an `enum class` while enums with
associated data are exposed as a `sealed class`.
* In Swift, enums are exposed using the native `enum` syntax.

Like [records](./records.md), enums are only used to communicate data
and do not have any associated behaviours.

The requirement of having only named fields may be removed in future.
40 changes: 40 additions & 0 deletions docs/manual/src/interface/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Throwing errors

It is often the case that a function does not return `T` in Rust but `Result<T, E>` to reflect that it is fallible. UniFFI can expose such functions as long as your error type `E` meets the following requirements:

* It is a `pub enum` exposed as part of the interface definition (see [enumerations](./enumerations.md))
* It impls the `std::error::Error` trait

(Using [thiserror](https://crates.io/crates/thiserror) works!).

Here's how you would write a Rust failible function:

```rust

#[uniffi::declare_interface]
mod example {

use thiserror::Error;

#[derive(Debug, Error)]
pub enum ArithmeticError {
#[error("Integer overflow on an operation with {a} and {b}")]
IntegerOverflow { a: u64, b: u64 },
}

pub fn add(a: u64, b: u64) -> Result<u64, ArithmeticError> {
a.checked_add(b).ok_or(ArithmeticError::IntegerOverflow { a, b })
}
}
```

Note that you cannot currently use a typedef for the `Result` type, UniFFI only supports
the full `Result<T, E>` syntax. This limitation may be removed in future.

The resulting `add` function would be exposed to the foreign-language code using
its native error-handling mechanism. For example:

* In Kotlin, there would be an `ArithmeticErrorException` class with an inner class
for each variant, and the `add` function would throw it.
* In Swift, there would be an `ArithmeticError` enum with variants matching the Rust enum,
and the `add` function would be marked `throws` and could throw it.
70 changes: 70 additions & 0 deletions docs/manual/src/interface/functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Functions

The interface definition can contain public functions using the standard Rust syntax:

```rust
#[uniffi::declare_interface]
mod example {
fn hello_world() -> String {
"Hello World!".to_owned()
}
}
```

The arguments and return types must be types that are understood by UniFFI.

UniFFI does not understand path references or type aliases, so things like
the following will produce a compile-time error:

```rust

struct NotPartOfTheInterface {
value: u32,
}

#[uniffi::declare_interface]
mod example {

type MyString = String

// Error: UniFFI doesn't know what "MyString" is.
fn hello_world() -> MyString {
"Hello World!".to_owned()
}

// Error: UniFFI doesn't know about the `NotPartOfTheInterface` type.
fn example(v: NotPartOfTheInterface) {
println!("value: {}", v.value);
}

// Error: UniFFI doesn't understand the `std::vec::` path; just use `Vec<T>`.
fn smallest(values: std::vec::Vec<i32>) -> i32 {
values.iter().min()
}
}
```

## Optional arguments & default values

Function arguments can be marked as optional with a default value specified.
TODO: what will the Rust syntax for this be?

The Rust code will declare this using a macro annotation:

```rust
#[uniffi::defaults(name="World")] // TODO: not even sure what syntax is possible here...
fn hello_name(name: String) -> String {
format!("Hello {}", name)
}
```

The generated foreign-language bindings will use function parameters with default values.
This works for the Kotlin, Swift and Python targets.

For example the generated Kotlin code will be equivalent to:

```kotlin
fun helloName(name: String = "World" ): String {
// ...
}
```
19 changes: 19 additions & 0 deletions docs/manual/src/interface/namespace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Namespace

Every UniFFI component has an associated *namespace* which is taken from
the name of the inline module defining the interface:

```rust
#[uniffi::declare_interface]
mod example_namespace {
// The resulting interface definition will have a namespace
// of "example_namespace".
}
```

The namespace servces multiple purposes:
- It defines the default package/module/etc namespace in generated foreign-language bindings.
- It is used to ensure uniqueness of names in the generated C FFI.

You should ensure that the namespace of your component will be unique
amongst all UniFFI components that might be used together in an application.
Loading

0 comments on commit 9613f96

Please sign in to comment.