Skip to content

Commit

Permalink
Foreign-implemented async trait methods (#2017)
Browse files Browse the repository at this point in the history
These methods input a completion function that they call when the async
function is complete. They return a ForeignFuture struct, which
represents the foreign task object and is used to drop/cancel futures.
Like Rust, dropping and cancelling are coupled together into one operation.

- Added `RustCallStatus` as an `FfiType` variant, since I wanted to use
  it in the `ForeignFutureResult` structs.  In theory, we could define
  `RustCallStatus` a `FfiType::Struct`, but I didn't want to introduce
  that change in this PR.
- Fixed the result mapping code to work with async functions.  Before we
  were executing the mapping call, then awaiting the result, but we need
  to do that in the opposite order (`foo.into().await` vs `foo.await.into()`).
  Also, specify the return type generics for `rust_future_new` so that
  the Rust can figure out the into() generics.
  • Loading branch information
bendk committed Mar 18, 2024
1 parent 789a902 commit e7b0b55
Show file tree
Hide file tree
Showing 46 changed files with 1,615 additions and 63 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

- Functions, methods and constructors exported by procmacros can be renamed for the forgeign bindings. See the procmaco manual section.

- Rust trait interfaces can now have async functions. See the futures manual section for details.
- Trait interfaces can now have async functions, both Rust and foreign-implemented. See the futures manual section for details.

- Procmacros support tuple-enums.

Expand Down
92 changes: 86 additions & 6 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"examples/app/uniffi-bindgen-cli",
"examples/arithmetic",
"examples/arithmetic-procmacro",
"examples/async-api-client",
"examples/callbacks",
"examples/custom-types",
"examples/futures",
Expand Down
22 changes: 22 additions & 0 deletions docs/manual/src/futures.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,25 @@ pub trait SayAfterTrait: Send + Sync {
async fn say_after(&self, ms: u16, who: String) -> String;
}
```

## Combining Rust and foreign async code

Traits with callback interface support that export async methods can be combined with async Rust code.
See the [async-api-client example](https://github.com/mozilla/uniffi-rs/tree/main/examples/async-api-client) for an example of this.

### Python: uniffi_set_event_loop()

Python bindings export a function named `uniffi_set_event_loop()` which handles a corner case when
integrating async Rust and Python code. `uniffi_set_event_loop()` is needed when Python async
functions run outside of the eventloop, for example:

- Rust code is executing outside of the eventloop. Some examples:
- Rust code spawned its own thread
- Python scheduled the Rust code using `EventLoop.run_in_executor`
- The Rust code calls a Python async callback method, using something like `pollster` to block
on the async call.

In this case, we need an event loop to run the Python async function, but there's no eventloop set for the thread.
Use `uniffi_set_event_loop()` to handle this case.
It should be called before the Rust code makes the async call and passed an eventloop to use.

3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Newcomers are recommended to explore them in the following order:
code, through rust and back again.
* [`./fxa-client`](./fxa-client/) doesn't work yet, but it contains aspirational example of what the UDL
might look like for an actual real-world component.
* [`./async-api-client`](./async-api-client/) shows how to handle async calls across the FFI. The
foreign code supplies the HTTP client, the Rust code uses that client to expose a GitHub API
client, then the foreign code consumes the client. All code on both sides of the FFI is async.

Each example has the following structure:

Expand Down
23 changes: 23 additions & 0 deletions examples/async-api-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "uniffi-example-async-api-client"
edition = "2021"
version = "0.26.1"
license = "MPL-2.0"
publish = false

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

[dependencies]
async-trait = "0.1"
uniffi = { workspace = true }
serde = { version = "1", features=["derive"] }
serde_json = "1"
thiserror = "1.0"

[build-dependencies]
uniffi = { workspace = true, features = ["build"] }

[dev-dependencies]
uniffi = { workspace = true, features = ["bindgen-tests"] }
7 changes: 7 additions & 0 deletions examples/async-api-client/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/async-api-client.udl").unwrap();
}
36 changes: 36 additions & 0 deletions examples/async-api-client/src/async-api-client.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace async_api_client {
string test_response_data();
};

[Error]
interface ApiError {
Http(string reason);
Api(string reason);
Json(string reason);
};

// Implemented by the foreign bindings
[Trait, WithForeign]
interface HttpClient {
[Throws=ApiError, Async]
string fetch(string url); // fetch an URL and return the body
};

dictionary Issue {
string url;
string title;
IssueState state;
};

enum IssueState {
"Open",
"Closed",
};

// Implemented by the Rust code
interface ApiClient {
constructor(HttpClient http_client);

[Throws=ApiError, Async]
Issue get_issue(string owner, string repository, u32 issue_number);
};
142 changes: 142 additions & 0 deletions examples/async-api-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* 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;

#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("HttpError: {reason}")]
Http { reason: String },
#[error("ApiError: {reason}")]
Api { reason: String },
#[error("JsonError: {reason}")]
Json { reason: String },
}

pub type Result<T> = std::result::Result<T, ApiError>;

#[async_trait::async_trait]
pub trait HttpClient: Send + Sync {
async fn fetch(&self, url: String) -> Result<String>;
}

#[derive(Debug, serde::Deserialize)]
pub struct Issue {
url: String,
title: String,
state: IssueState,
}

#[derive(Debug, serde::Deserialize)]
pub enum IssueState {
#[serde(rename = "open")]
Open,
#[serde(rename = "closed")]
Closed,
}

pub struct ApiClient {
http_client: Arc<dyn HttpClient>,
}

impl ApiClient {
pub fn new(http_client: Arc<dyn HttpClient>) -> Self {
Self { http_client }
}

pub async fn get_issue(
&self,
owner: String,
repository: String,
issue_number: u32,
) -> Result<Issue> {
let url =
format!("https://api.github.com/repos/{owner}/{repository}/issues/{issue_number}");
let body = self.http_client.fetch(url).await?;
Ok(serde_json::from_str(&body)?)
}
}

impl From<serde_json::Error> for ApiError {
fn from(e: serde_json::Error) -> Self {
Self::Json {
reason: e.to_string(),
}
}
}

/// Sample data downloaded from a real github api call
///
/// The tests don't make real HTTP calls to avoid them failing because of network errors.
pub fn test_response_data() -> String {
String::from(
r#"{
"url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017",
"repository_url": "https://api.github.com/repos/mozilla/uniffi-rs",
"labels_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/labels{/name}",
"comments_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/comments",
"events_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/events",
"html_url": "https://github.com/mozilla/uniffi-rs/issues/2017",
"id": 2174982360,
"node_id": "I_kwDOECpYAM6Bo5jY",
"number": 2017,
"title": "Foreign-implemented async traits",
"user": {
"login": "bendk",
"id": 1012809,
"node_id": "MDQ6VXNlcjEwMTI4MDk=",
"avatar_url": "https://avatars.githubusercontent.com/u/1012809?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/bendk",
"html_url": "https://github.com/bendk",
"followers_url": "https://api.github.com/users/bendk/followers",
"following_url": "https://api.github.com/users/bendk/following{/other_user}",
"gists_url": "https://api.github.com/users/bendk/gists{/gist_id}",
"starred_url": "https://api.github.com/users/bendk/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/bendk/subscriptions",
"organizations_url": "https://api.github.com/users/bendk/orgs",
"repos_url": "https://api.github.com/users/bendk/repos",
"events_url": "https://api.github.com/users/bendk/events{/privacy}",
"received_events_url": "https://api.github.com/users/bendk/received_events",
"type": "User",
"site_admin": false
},
"labels": [
],
"state": "open",
"locked": false,
"assignee": null,
"assignees": [
],
"milestone": null,
"comments": 0,
"created_at": "2024-03-07T23:07:29Z",
"updated_at": "2024-03-07T23:07:29Z",
"closed_at": null,
"author_association": "CONTRIBUTOR",
"active_lock_reason": null,
"body": "We currently allow Rust code to implement async trait methods, but foreign implementations are not supported. We should extend support to allow for foreign code.\\r\\n\\r\\nI think this is a key feature for full async support. It allows Rust code to define an async method that depends on a foreign async method. This allows users to use async code without running a Rust async runtime, you can effectively piggyback on the foreign async runtime.",
"closed_by": null,
"reactions": {
"url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/timeline",
"performed_via_github_app": null,
"state_reason": null
}"#,
)
}

uniffi::include_scaffolding!("async-api-client");
Loading

0 comments on commit e7b0b55

Please sign in to comment.