-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Experiment: Use Rust code directly as the interface definition.
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
Showing
52 changed files
with
2,017 additions
and
947 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
// ... | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.