Skip to content

Commit

Permalink
Allow Rust traits to be exposed as an interface. (#1457)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhammond committed Apr 30, 2023
1 parent 20d276e commit b982143
Show file tree
Hide file tree
Showing 38 changed files with 575 additions and 116 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@
- UniFFI users will automatically get the benefits of this without any code changes.
- External bindings authors will need to update their bindings code. See PR #1494 for details.
- ABI: Changed API checksum handling. This affects external bindings authors who will need to update their code to work with the new system. See PR #1469 for details.
- Removed the long deprecated `ThreadSafe` attribute.

### What's changed

- The `include_scaffolding!()` macro must now either be called from your crate root or you must have `use the_mod_that_calls_include_scaffolding::*` in your crate root. This was always the expectation, but wasn't required before. This will now start failing with errors that say `crate::UniFfiTag` does not exist.
- proc-macros now work with many more types including type aliases, type paths, etc.
- The `uniffi_types` module is no longer needed when using proc-macros.

- Traits can be exposed as a UniFFI `interface` by using a `[Trait]` attribute in the UDL.
See [the documentation](https://mozilla.github.io/uniffi-rs/udl/interfaces.html#exposing-traits-as-interfaces).

## v0.23.0 (backend crates: v0.23.0) - (_2023-01-27_)

[All changes in v0.23.0](https://github.com/mozilla/uniffi-rs/compare/v0.22.0...v0.23.0).
Expand Down
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ members = [
"uniffi",
"weedle2",

"examples/app/uniffi-bindgen-cli",
"examples/arithmetic",
"examples/callbacks",
"examples/custom-types",
"examples/geometry",
"examples/rondpoint",
"examples/sprites",
"examples/todolist",
"examples/custom-types",
"examples/app/uniffi-bindgen-cli",
"examples/traits",

"fixtures/benchmarks",
"fixtures/coverall",
Expand Down
55 changes: 55 additions & 0 deletions docs/manual/src/udl/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,61 @@ func display(list: TodoListProtocol) {
Following this pattern will make it easier for you to provide mock implementation of the Rust-based objects
for testing.

## Exposing Traits as interfaces

It's possible to have UniFFI expose a Rust trait as an interface by specifying a `Trait` attribute.

For example, in the UDL file you might specify:

```idl
[Trait]
interface Button {
string name();
};
```

With the following Rust implementation:

```rust
pub trait Button: Send + Sync {
fn name(&self) -> String;
}

struct StopButton {}

impl Button for StopButton {
fn name(&self) -> String {
"stop".to_string()
}
}
```

Uniffi explicitly checks all interfaces are `Send + Sync` - there's a ui-test which demonstrates obscure rust compiler errors when it's not true. Traits however need to explicitly add those bindings.

References to traits are passed around like normal interface objects - in an `Arc<>`.
For example, this UDL:

```idl
namespace traits {
sequence<Button> get_buttons();
Button press(Button button);
};
```

would have these signatures in Rust:

```rust
fn get_buttons() -> Vec<Arc<dyn Button>> { ... }
fn press(button: Arc<dyn Button>) -> Arc<dyn Button> { ... }
```

See the ["traits" example](https://github.com/mozilla/uniffi-rs/tree/main/examples/traits) for more.

### Traits construction

Because any number of `struct`s may implement a trait, they don't have constructors.

## Alternate Named Constructors

In addition to the default constructor connected to the `::new()` method, you can specify
Expand Down
22 changes: 22 additions & 0 deletions examples/traits/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "uniffi-example-traits"
edition = "2021"
version = "0.22.0"
authors = ["Firefox Sync Team <sync-team@mozilla.com>"]
license = "MPL-2.0"
publish = false

[lib]
crate-type = ["lib", "cdylib"]
name = "uniffi_traits"

[dependencies]
uniffi = {path = "../../uniffi"}
thiserror = "1.0"

[build-dependencies]
uniffi = {path = "../../uniffi", features = ["build"] }

[dev-dependencies]
uniffi = {path = "../../uniffi", features = ["bindgen-tests"] }

7 changes: 7 additions & 0 deletions examples/traits/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

fn main() {
uniffi::generate_scaffolding("./src/traits.udl").unwrap();
}
36 changes: 36 additions & 0 deletions examples/traits/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::sync::Arc;

// namespace functions.
fn get_buttons() -> Vec<Arc<dyn Button>> {
vec![Arc::new(StopButton {}), Arc::new(GoButton {})]
}

fn press(button: Arc<dyn Button>) -> Arc<dyn Button> {
button
}

pub trait Button: Send + Sync {
fn name(&self) -> String;
}

struct GoButton {}

impl Button for GoButton {
fn name(&self) -> String {
"go".to_string()
}
}

struct StopButton {}

impl Button for StopButton {
fn name(&self) -> String {
"stop".to_string()
}
}

include!(concat!(env!("OUT_DIR"), "/traits.uniffi.rs"));
12 changes: 12 additions & 0 deletions examples/traits/src/traits.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace traits {
// Get all the buttons we can press.
sequence<Button> get_buttons();
// press a button and return it.
Button press(Button button);
};

// This is a trait in Rust.
[Trait]
interface Button {
string name();
};
7 changes: 7 additions & 0 deletions examples/traits/tests/bindings/test_traits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from traits import *

for button in get_buttons():
if button.name() in ["go", "stop"]:
press(button)
else:
print("unknown button", button)
1 change: 1 addition & 0 deletions examples/traits/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uniffi::build_foreign_language_testcases!("tests/bindings/test_traits.py",);
9 changes: 9 additions & 0 deletions examples/traits/uniffi.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[bindings.kotlin]
package_name = "uniffi.traits"
cdylib_name = "uniffi_traits"

[bindings.swift]
cdylib_name = "uniffi_traits"

[bindings.python]
cdylib_name = "uniffi_traits"
23 changes: 23 additions & 0 deletions fixtures/coverall/src/coverall.udl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace coverall {

u64 get_num_alive();

sequence<TestTrait> get_traits();

// void returning error throwing namespace function to catch clippy warnings (eg, #1330)
[Throws=CoverallError]
void println(string text);
Expand All @@ -29,6 +31,7 @@ dictionary SimpleDict {
double float64;
double? maybe_float64;
Coveralls? coveralls;
TestTrait? test_trait;
};

dictionary DictWithDefaults {
Expand Down Expand Up @@ -150,3 +153,23 @@ interface ThreadsafeCounter {
void busy_wait(i32 ms);
i32 increment_if_busy();
};

// This is a trait implemented on the Rust side.
[Trait]
interface TestTrait {
string name(); // The name of the implementation

[Self=ByArc]
u64 number();

/// Calls `Arc::strong_count()` on the `Arc` containing `self`.
[Self=ByArc]
u64 strong_count();

/// Takes an `Arc<Self>` and stores it in `self`, dropping the existing
/// reference. Note you can create circular references by passing `self`.
void take_other(TestTrait? other);

/// Returns what was previously set via `take_other()`, or null.
TestTrait? get_other();
};
19 changes: 7 additions & 12 deletions fixtures/coverall/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use std::time::SystemTime;

use once_cell::sync::Lazy;

mod traits;
pub use traits::{get_traits, TestTrait};

static NUM_ALIVE: Lazy<RwLock<u64>> = Lazy::new(|| RwLock::new(0));

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -41,7 +44,7 @@ pub enum ComplexError {
PermissionDenied { reason: String },
}

#[derive(Debug, Clone)]
#[derive(Clone, Debug, Default)]
pub struct SimpleDict {
text: String,
maybe_text: Option<String>,
Expand All @@ -62,6 +65,7 @@ pub struct SimpleDict {
float64: f64,
maybe_float64: Option<f64>,
coveralls: Option<Arc<Coveralls>>,
test_trait: Option<Arc<dyn TestTrait>>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -98,30 +102,21 @@ fn create_some_dict() -> SimpleDict {
float64: 0.0,
maybe_float64: Some(1.0),
coveralls: Some(Arc::new(Coveralls::new("some_dict".to_string()))),
test_trait: Some(Arc::new(traits::Trait2 {})),
}
}

fn create_none_dict() -> SimpleDict {
SimpleDict {
text: "text".to_string(),
maybe_text: None,
a_bool: true,
maybe_a_bool: None,
unsigned8: 1,
maybe_unsigned8: None,
unsigned16: 3,
maybe_unsigned16: None,
unsigned64: u64::MAX,
maybe_unsigned64: None,
signed8: 8,
maybe_signed8: None,
signed64: i64::MAX,
maybe_signed64: None,
float32: 1.2345,
maybe_float32: None,
float64: 0.0,
maybe_float64: None,
coveralls: None,
..Default::default()
}
}

Expand Down
74 changes: 74 additions & 0 deletions fixtures/coverall/src/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::sync::{Arc, Mutex};

// namespace functions.
pub fn get_traits() -> Vec<Arc<dyn TestTrait>> {
vec![
Arc::new(Trait1 {
..Default::default()
}),
Arc::new(Trait2 {}),
]
}

pub trait TestTrait: Send + Sync + std::fmt::Debug {
fn name(&self) -> String;

fn number(self: Arc<Self>) -> u64;

fn strong_count(self: Arc<Self>) -> u64 {
Arc::strong_count(&self) as u64
}

fn take_other(&self, other: Option<Arc<dyn TestTrait>>);

fn get_other(&self) -> Option<Arc<dyn TestTrait>>;
}

#[derive(Debug, Default)]
pub(crate) struct Trait1 {
// A reference to another trait.
other: Mutex<Option<Arc<dyn TestTrait>>>,
}

impl TestTrait for Trait1 {
fn name(&self) -> String {
"trait 1".to_string()
}

fn number(self: Arc<Self>) -> u64 {
1_u64
}

fn take_other(&self, other: Option<Arc<dyn TestTrait>>) {
*self.other.lock().unwrap() = other.map(|arc| Arc::clone(&arc))
}

fn get_other(&self) -> Option<Arc<dyn TestTrait>> {
(*self.other.lock().unwrap()).as_ref().map(Arc::clone)
}
}

#[derive(Debug)]
pub(crate) struct Trait2 {}
impl TestTrait for Trait2 {
fn name(&self) -> String {
"trait 2".to_string()
}

fn number(self: Arc<Self>) -> u64 {
2_u64
}

// Don't bother implementing these here - the test on the struct above is ok.
fn take_other(&self, _other: Option<Arc<dyn TestTrait>>) {
unimplemented!();
}

fn get_other(&self) -> Option<Arc<dyn TestTrait>> {
unimplemented!()
}
}
Loading

0 comments on commit b982143

Please sign in to comment.