Skip to content

Commit

Permalink
Allow Rust traits to be exposed as an interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
mhammond committed Apr 30, 2023
1 parent 20d276e commit 0870081
Show file tree
Hide file tree
Showing 38 changed files with 578 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
58 changes: 58 additions & 0 deletions docs/manual/src/udl/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,64 @@ 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()
}
}
```

Note how the trait is explicitly bound to the `Send + Sync` traits. This is because, like regular objects,
references to traits must be safe to access concurrently. To help enforce this and to ensure error messages
are meaningful, UniFFI arranges to insist that every trait exposed must explicitly include `Send + Sync`.

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 can not have constructors.
This means they are only able to be supplied by a function/method described in the UDL, with the implementation of that function choosing which concrete `struct` to return.

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

Please sign in to comment.