Skip to content

Commit

Permalink
Adding support for Rust async trait methods
Browse files Browse the repository at this point in the history
This was pretty easy, it mostly meant not hard coding that we don't
support async methods and which order to apply `#[uniffi::export]` vs
`#[async_trait::async_trait]`.

The next step is foreign-implemented traits.
  • Loading branch information
bendk committed Feb 7, 2024
1 parent 3aa6735 commit 49becea
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

[All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.26.0...HEAD).

### What's new?

- Rust trait interfaces can now have async functions. See the futures manual section for details.

### What's fixed?

- Fixed a memory leak in callback interface handling.
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions docs/manual/src/futures.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,29 @@ This code uses `asyncio` to drive the future to completion, while our exposed fu
In Rust `Future` terminology this means the foreign bindings supply the "executor" - think event-loop, or async runtime. In this example it's `asyncio`. There's no requirement for a Rust event loop.

There are [some great API docs](https://docs.rs/uniffi_core/latest/uniffi_core/ffi/rustfuture/index.html) on the implementation that are well worth a read.

## Exporting async trait methods

UniFFI is compatible with the [async-trait](https://crates.io/crates/async-trait) crate and this can
be used to export trait interfaces over the FFI.

When using UDL, wrap your trait with the `#[async_trait]` attribute. In the UDL, annotate all async
methods with `[Async]`:

```idl
[Trait]
interface SayAfterTrait {
[Async]
string say_after(u16 ms, string who);
};
```

When using proc-macros, make sure to put `#[uniffi::export]` outside the `#[async_trait]` attribute:

```rust
#[uniffi::export]
#[async_trait::async_trait]
pub trait SayAfterTrait: Send + Sync {
async fn say_after(&self, ms: u16, who: String) -> String;
}
```
1 change: 1 addition & 0 deletions fixtures/futures/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ path = "src/bin.rs"

[dependencies]
uniffi = { workspace = true, features = ["tokio", "cli"] }
async-trait = "0.1"
thiserror = "1.0"
tokio = { version = "1.24.1", features = ["time", "sync"] }
once_cell = "1.18.0"
Expand Down
6 changes: 6 additions & 0 deletions fixtures/futures/src/futures.udl
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ namespace futures {
[Async]
boolean always_ready();
};

[Trait]
interface SayAfterUdlTrait {
[Async]
string say_after(u16 ms, string who);
};
55 changes: 55 additions & 0 deletions fixtures/futures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,59 @@ pub async fn use_shared_resource(options: SharedResourceOptions) -> Result<(), A
Ok(())
}

// Example of an trait with async methods
#[uniffi::export]
#[async_trait::async_trait]
pub trait SayAfterTrait: Send + Sync {
async fn say_after(&self, ms: u16, who: String) -> String;
}

// Example of async trait defined in the UDL file
#[async_trait::async_trait]
pub trait SayAfterUdlTrait: Send + Sync {
async fn say_after(&self, ms: u16, who: String) -> String;
}

struct SayAfterImpl1;

struct SayAfterImpl2;

#[async_trait::async_trait]
impl SayAfterTrait for SayAfterImpl1 {
async fn say_after(&self, ms: u16, who: String) -> String {
say_after(ms, who).await
}
}

#[async_trait::async_trait]
impl SayAfterTrait for SayAfterImpl2 {
async fn say_after(&self, ms: u16, who: String) -> String {
say_after(ms, who).await
}
}

#[uniffi::export]
fn get_say_after_traits() -> Vec<Arc<dyn SayAfterTrait>> {
vec![Arc::new(SayAfterImpl1), Arc::new(SayAfterImpl2)]
}

#[async_trait::async_trait]
impl SayAfterUdlTrait for SayAfterImpl1 {
async fn say_after(&self, ms: u16, who: String) -> String {
say_after(ms, who).await
}
}

#[async_trait::async_trait]
impl SayAfterUdlTrait for SayAfterImpl2 {
async fn say_after(&self, ms: u16, who: String) -> String {
say_after(ms, who).await
}
}

#[uniffi::export]
fn get_say_after_udl_traits() -> Vec<Arc<dyn SayAfterUdlTrait>> {
vec![Arc::new(SayAfterImpl1), Arc::new(SayAfterImpl2)]
}

uniffi::include_scaffolding!("futures");
28 changes: 28 additions & 0 deletions fixtures/futures/tests/bindings/test_futures.kts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,34 @@ runBlocking {
assert(not_megaphone == null)
}

// Test async methods in trait interfaces
runBlocking {
val traits = getSayAfterTraits()
val time = measureTimeMillis {
val result1 = traits[0].sayAfter(100U, "Alice")
val result2 = traits[1].sayAfter(100U, "Bob")

assert(result1 == "Hello, Alice!")
assert(result2 == "Hello, Bob!")
}

assertApproximateTime(time, 200, "async methods")
}

// Test async methods in UDL-defined trait interfaces
runBlocking {
val traits = getSayAfterUdlTraits()
val time = measureTimeMillis {
val result1 = traits[0].sayAfter(100U, "Alice")
val result2 = traits[1].sayAfter(100U, "Bob")

assert(result1 == "Hello, Alice!")
assert(result2 == "Hello, Bob!")
}

assertApproximateTime(time, 200, "async methods")
}

// Test with the Tokio runtime.
runBlocking {
val time = measureTimeMillis {
Expand Down
30 changes: 30 additions & 0 deletions fixtures/futures/tests/bindings/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,36 @@ async def test():

asyncio.run(test())

def test_async_trait_interface_methods(self):
async def test():
traits = get_say_after_traits()
t0 = now()
result1 = await traits[0].say_after(100, 'Alice')
result2 = await traits[1].say_after(100, 'Bob')
t1 = now()

self.assertEqual(result1, 'Hello, Alice!')
self.assertEqual(result2, 'Hello, Bob!')
t_delta = (t1 - t0).total_seconds()
self.assertGreater(t_delta, 0.2)

asyncio.run(test())

def test_udl_async_trait_interface_methods(self):
async def test():
traits = get_say_after_udl_traits()
t0 = now()
result1 = await traits[0].say_after(100, 'Alice')
result2 = await traits[1].say_after(100, 'Bob')
t1 = now()

self.assertEqual(result1, 'Hello, Alice!')
self.assertEqual(result2, 'Hello, Bob!')
t_delta = (t1 - t0).total_seconds()
self.assertGreater(t_delta, 0.2)

asyncio.run(test())

def test_async_object_param(self):
async def test():
megaphone = new_megaphone()
Expand Down
38 changes: 38 additions & 0 deletions fixtures/futures/tests/bindings/test_futures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,44 @@ Task {
counter.leave()
}

// Test async trait interface methods
counter.enter()

Task {
let traits = getSayAfterTraits()

let t0 = Date()
let result1 = await traits[0].sayAfter(ms: 1000, who: "Alice")
let result2 = await traits[1].sayAfter(ms: 1000, who: "Bob")
let t1 = Date()

assert(result1 == "Hello, Alice!")
assert(result2 == "Hello, Bob!")
let tDelta = DateInterval(start: t0, end: t1)
assert(tDelta.duration > 2 && tDelta.duration < 2.1)

counter.leave()
}

// Test UDL-defined async trait interface methods
counter.enter()

Task {
let traits = getSayAfterUdlTraits()

let t0 = Date()
let result1 = await traits[0].sayAfter(ms: 1000, who: "Alice")
let result2 = await traits[1].sayAfter(ms: 1000, who: "Bob")
let t1 = Date()

assert(result1 == "Hello, Alice!")
assert(result2 == "Hello, Bob!")
let tDelta = DateInterval(start: t0, end: t1)
assert(tDelta.duration > 2 && tDelta.duration < 2.1)

counter.leave()
}

// Test async function returning an object
counter.enter()

Expand Down
4 changes: 3 additions & 1 deletion uniffi_bindgen/src/interface/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,17 +605,19 @@ impl From<uniffi_meta::TraitMethodMetadata> for Method {
fn from(meta: uniffi_meta::TraitMethodMetadata) -> Self {
let ffi_name = meta.ffi_symbol_name();
let checksum_fn_name = meta.checksum_symbol_name();
let is_async = meta.is_async;
let return_type = meta.return_type.map(Into::into);
let arguments = meta.inputs.into_iter().map(Into::into).collect();
let ffi_func = FfiFunction {
name: ffi_name,
is_async,
..FfiFunction::default()
};
Self {
name: meta.name,
object_name: meta.trait_name,
object_module_path: meta.module_path,
is_async: false,
is_async,
arguments,
return_type,
docstring: meta.docstring.clone(),
Expand Down
2 changes: 1 addition & 1 deletion uniffi_bindgen/src/scaffolding/templates/ObjectTemplate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#[::uniffi::export_for_udl{% if obj.has_callback_interface() %}(with_foreign){% endif %}]
pub trait r#{{ obj.name() }} {
{%- for meth in obj.methods() %}
fn {% if meth.is_async() %}async {% endif %}r#{{ meth.name() }}(
{% if meth.is_async() %}async {% endif %}fn r#{{ meth.name() }}(
{% if meth.takes_self_by_arc()%}self: Arc<Self>{% else %}&self{% endif %},
{%- for arg in meth.arguments() %}
r#{{ arg.name() }}: {% if arg.by_ref() %}&{% endif %}{{ arg.as_type().borrow()|type_rs }},
Expand Down
6 changes: 0 additions & 6 deletions uniffi_macros/src/export/trait_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,6 @@ pub(super) fn gen_trait_scaffolding(
.into_iter()
.map(|item| match item {
ImplItem::Method(sig) => {
if sig.is_async {
return Err(syn::Error::new(
sig.span,
"async trait methods are not supported",
));
}
let fn_args = ExportFnArgs {
async_runtime: None,
};
Expand Down

0 comments on commit 49becea

Please sign in to comment.