Skip to content

Commit

Permalink
Merge pull request #2018 from bendk/async-trait-interfaces
Browse files Browse the repository at this point in the history
Foreign-implemented async trait methods (#2017)
  • Loading branch information
bendk authored Mar 18, 2024
2 parents 789a902 + e7b0b55 commit 6ddd8cd
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 6ddd8cd

Please sign in to comment.