Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Rust traits to be exposed as an interface. #1457

Merged
merged 1 commit into from
Apr 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
}
```
mhammond marked this conversation as resolved.
Show resolved Hide resolved

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 <[email protected]>"]
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