Skip to content

Commit 17482fa

Browse files
authored
Merge pull request #2040 from bendk/blocking-task-queue-without-specialized-code
Blocking task queue without specialized code
2 parents 6b09f11 + 1fbfde3 commit 17482fa

File tree

9 files changed

+333
-132
lines changed

9 files changed

+333
-132
lines changed

examples/async-api-client/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
This crate is a toy build an async API client, with some parts implemented in Rust and some parts
2+
implemented in the foreign language. Each side makes async calls across the FFI.
3+
4+
The motivation is to show how to build an async-based Rust library, using a foreign async executor to drive the futures.
5+
Note that the Rust code does not start any threads of its own, nor does it use startup an async runtime like tokio.
6+
Instead, it awaits async calls to the foreign code and the foreign executor manages the threads.
7+
8+
There are two basic ways the Rust code in this crate awaits the foreign code:
9+
10+
## API calls
11+
12+
API calls are the simple case.
13+
Rust awaits an HTTP call to the foreign side, then uses `serde` to parse the JSON into a structured response.
14+
As long as the Rust code is "non-blocking" this system should work fine.
15+
Note: there is not a strict definition for "non-blocking", but typically it means not performing IO and not executing a long-running CPU operation.
16+
17+
## Blocking tasks
18+
19+
The more difficult case is a blocking Rust call.
20+
The example from this crate is reading the API credentials from disk.
21+
The `tasks.rs` module and the foreign implementations of the `TaskRunner` interface are an experiment to show how this can be accomplished using async callback methods.
22+
23+
The code works, but is a bit clunky.
24+
For example requiring that the task closure is `'static` creates some extra work for the `load_credentials` function.
25+
It also requires an extra `Mutex` and `Arc`.
26+
27+
The UniFFI team is looking for ways to simplify this process by handling it natively in UniFFI, see https://github.com/mozilla/uniffi-rs/pull/1837.
28+
If you are writing Rust code that needs to make async blocking calls, please tell us about your use case which will help us develop the feature.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use crate::{run_task, ApiError, Result, TaskRunner};
6+
use std::sync::Arc;
7+
8+
#[async_trait::async_trait]
9+
pub trait HttpClient: Send + Sync {
10+
async fn fetch(&self, url: String, credentials: String) -> Result<String>;
11+
}
12+
13+
impl From<serde_json::Error> for ApiError {
14+
fn from(e: serde_json::Error) -> Self {
15+
Self::Json {
16+
reason: e.to_string(),
17+
}
18+
}
19+
}
20+
21+
#[derive(Debug, serde::Deserialize)]
22+
pub struct Issue {
23+
pub url: String,
24+
pub title: String,
25+
pub state: IssueState,
26+
}
27+
28+
#[derive(Debug, serde::Deserialize)]
29+
pub enum IssueState {
30+
#[serde(rename = "open")]
31+
Open,
32+
#[serde(rename = "closed")]
33+
Closed,
34+
}
35+
36+
pub struct ApiClient {
37+
http_client: Arc<dyn HttpClient>,
38+
task_runner: Arc<dyn TaskRunner>,
39+
}
40+
41+
impl ApiClient {
42+
// Pretend this is a blocking call that needs to load the credentials from disk/network
43+
fn load_credentials_sync(&self) -> String {
44+
String::from("username:password")
45+
}
46+
47+
async fn load_credentials(self: Arc<Self>) -> String {
48+
let self_cloned = Arc::clone(&self);
49+
run_task(&self.task_runner, move || {
50+
self_cloned.load_credentials_sync()
51+
})
52+
.await
53+
}
54+
}
55+
56+
impl ApiClient {
57+
pub fn new(http_client: Arc<dyn HttpClient>, task_runner: Arc<dyn TaskRunner>) -> Self {
58+
Self {
59+
http_client,
60+
task_runner,
61+
}
62+
}
63+
64+
pub async fn get_issue(
65+
self: Arc<Self>,
66+
owner: String,
67+
repository: String,
68+
issue_number: u32,
69+
) -> Result<Issue> {
70+
let credentials = self.clone().load_credentials().await;
71+
let url =
72+
format!("https://api.github.com/repos/{owner}/{repository}/issues/{issue_number}");
73+
let body = self.http_client.fetch(url, credentials).await?;
74+
Ok(serde_json::from_str(&body)?)
75+
}
76+
}

examples/async-api-client/src/async-api-client.udl

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,20 @@ interface ApiError {
1313
[Trait, WithForeign]
1414
interface HttpClient {
1515
[Throws=ApiError, Async]
16-
string fetch(string url); // fetch an URL and return the body
16+
string fetch(string url, string credentials); // fetch an URL and return the body
17+
};
18+
19+
// Run Rust tasks in a thread pool.
20+
// Implemented by the foreign bindings
21+
[Trait, WithForeign]
22+
interface TaskRunner {
23+
[Async]
24+
void run_task(RustTask task);
25+
};
26+
27+
[Trait]
28+
interface RustTask {
29+
void execute();
1730
};
1831

1932
dictionary Issue {
@@ -29,7 +42,7 @@ enum IssueState {
2942

3043
// Implemented by the Rust code
3144
interface ApiClient {
32-
constructor(HttpClient http_client);
45+
constructor(HttpClient http_client, TaskRunner task_runner);
3346

3447
[Throws=ApiError, Async]
3548
Issue get_issue(string owner, string repository, u32 issue_number);

examples/async-api-client/src/lib.rs

Lines changed: 7 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
use std::sync::Arc;
5+
mod api_client;
6+
mod tasks;
7+
mod test_data;
8+
9+
pub use api_client::{ApiClient, HttpClient, Issue, IssueState};
10+
pub use tasks::{run_task, RustTask, TaskRunner};
11+
pub use test_data::test_response_data;
612

713
#[derive(Debug, thiserror::Error)]
814
pub enum ApiError {
@@ -16,127 +22,4 @@ pub enum ApiError {
1622

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

19-
#[async_trait::async_trait]
20-
pub trait HttpClient: Send + Sync {
21-
async fn fetch(&self, url: String) -> Result<String>;
22-
}
23-
24-
#[derive(Debug, serde::Deserialize)]
25-
pub struct Issue {
26-
url: String,
27-
title: String,
28-
state: IssueState,
29-
}
30-
31-
#[derive(Debug, serde::Deserialize)]
32-
pub enum IssueState {
33-
#[serde(rename = "open")]
34-
Open,
35-
#[serde(rename = "closed")]
36-
Closed,
37-
}
38-
39-
pub struct ApiClient {
40-
http_client: Arc<dyn HttpClient>,
41-
}
42-
43-
impl ApiClient {
44-
pub fn new(http_client: Arc<dyn HttpClient>) -> Self {
45-
Self { http_client }
46-
}
47-
48-
pub async fn get_issue(
49-
&self,
50-
owner: String,
51-
repository: String,
52-
issue_number: u32,
53-
) -> Result<Issue> {
54-
let url =
55-
format!("https://api.github.com/repos/{owner}/{repository}/issues/{issue_number}");
56-
let body = self.http_client.fetch(url).await?;
57-
Ok(serde_json::from_str(&body)?)
58-
}
59-
}
60-
61-
impl From<serde_json::Error> for ApiError {
62-
fn from(e: serde_json::Error) -> Self {
63-
Self::Json {
64-
reason: e.to_string(),
65-
}
66-
}
67-
}
68-
69-
/// Sample data downloaded from a real github api call
70-
///
71-
/// The tests don't make real HTTP calls to avoid them failing because of network errors.
72-
pub fn test_response_data() -> String {
73-
String::from(
74-
r#"{
75-
"url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017",
76-
"repository_url": "https://api.github.com/repos/mozilla/uniffi-rs",
77-
"labels_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/labels{/name}",
78-
"comments_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/comments",
79-
"events_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/events",
80-
"html_url": "https://github.com/mozilla/uniffi-rs/issues/2017",
81-
"id": 2174982360,
82-
"node_id": "I_kwDOECpYAM6Bo5jY",
83-
"number": 2017,
84-
"title": "Foreign-implemented async traits",
85-
"user": {
86-
"login": "bendk",
87-
"id": 1012809,
88-
"node_id": "MDQ6VXNlcjEwMTI4MDk=",
89-
"avatar_url": "https://avatars.githubusercontent.com/u/1012809?v=4",
90-
"gravatar_id": "",
91-
"url": "https://api.github.com/users/bendk",
92-
"html_url": "https://github.com/bendk",
93-
"followers_url": "https://api.github.com/users/bendk/followers",
94-
"following_url": "https://api.github.com/users/bendk/following{/other_user}",
95-
"gists_url": "https://api.github.com/users/bendk/gists{/gist_id}",
96-
"starred_url": "https://api.github.com/users/bendk/starred{/owner}{/repo}",
97-
"subscriptions_url": "https://api.github.com/users/bendk/subscriptions",
98-
"organizations_url": "https://api.github.com/users/bendk/orgs",
99-
"repos_url": "https://api.github.com/users/bendk/repos",
100-
"events_url": "https://api.github.com/users/bendk/events{/privacy}",
101-
"received_events_url": "https://api.github.com/users/bendk/received_events",
102-
"type": "User",
103-
"site_admin": false
104-
},
105-
"labels": [
106-
107-
],
108-
"state": "open",
109-
"locked": false,
110-
"assignee": null,
111-
"assignees": [
112-
113-
],
114-
"milestone": null,
115-
"comments": 0,
116-
"created_at": "2024-03-07T23:07:29Z",
117-
"updated_at": "2024-03-07T23:07:29Z",
118-
"closed_at": null,
119-
"author_association": "CONTRIBUTOR",
120-
"active_lock_reason": null,
121-
"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.",
122-
"closed_by": null,
123-
"reactions": {
124-
"url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/reactions",
125-
"total_count": 0,
126-
"+1": 0,
127-
"-1": 0,
128-
"laugh": 0,
129-
"hooray": 0,
130-
"confused": 0,
131-
"heart": 0,
132-
"rocket": 0,
133-
"eyes": 0
134-
},
135-
"timeline_url": "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017/timeline",
136-
"performed_via_github_app": null,
137-
"state_reason": null
138-
}"#,
139-
)
140-
}
141-
14225
uniffi::include_scaffolding!("async-api-client");
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use std::sync::{Arc, Mutex};
6+
7+
#[async_trait::async_trait]
8+
pub trait TaskRunner: Send + Sync {
9+
async fn run_task(&self, task: Arc<dyn RustTask>);
10+
}
11+
12+
pub trait RustTask: Send + Sync {
13+
fn execute(&self);
14+
}
15+
16+
pub async fn run_task<F, T>(runner: &Arc<dyn TaskRunner>, closure: F) -> T
17+
where
18+
F: FnOnce() -> T + Send + Sync + 'static,
19+
T: Send + 'static,
20+
{
21+
let closure = Arc::new(TaskClosure::new(closure));
22+
runner
23+
.run_task(Arc::clone(&closure) as Arc<dyn RustTask>)
24+
.await;
25+
closure.take_result()
26+
}
27+
28+
struct TaskClosure<F, T>
29+
where
30+
F: FnOnce() -> T + Send + Sync,
31+
T: Send,
32+
{
33+
inner: Mutex<TaskClosureInner<F, T>>,
34+
}
35+
36+
enum TaskClosureInner<F, T>
37+
where
38+
F: FnOnce() -> T + Send + Sync,
39+
T: Send,
40+
{
41+
Pending(F),
42+
Running,
43+
Complete(T),
44+
Finished,
45+
}
46+
47+
impl<F, T> TaskClosure<F, T>
48+
where
49+
F: FnOnce() -> T + Send + Sync,
50+
T: Send,
51+
{
52+
fn new(closure: F) -> Self {
53+
Self {
54+
inner: Mutex::new(TaskClosureInner::Pending(closure)),
55+
}
56+
}
57+
58+
fn take_result(&self) -> T {
59+
let mut inner = self.inner.lock().unwrap();
60+
match *inner {
61+
TaskClosureInner::Pending(_) => panic!("Task never ran"),
62+
TaskClosureInner::Running => panic!("Task still running"),
63+
TaskClosureInner::Finished => panic!("Task already finished"),
64+
TaskClosureInner::Complete(_) => (),
65+
};
66+
match std::mem::replace(&mut *inner, TaskClosureInner::Finished) {
67+
TaskClosureInner::Complete(v) => v,
68+
_ => unreachable!(),
69+
}
70+
}
71+
}
72+
73+
impl<F, T> RustTask for TaskClosure<F, T>
74+
where
75+
F: FnOnce() -> T + Send + Sync,
76+
T: Send,
77+
{
78+
fn execute(&self) {
79+
let mut inner = self.inner.lock().unwrap();
80+
match std::mem::replace(&mut *inner, TaskClosureInner::Running) {
81+
TaskClosureInner::Pending(f) => {
82+
let result = f();
83+
*inner = TaskClosureInner::Complete(result)
84+
}
85+
TaskClosureInner::Running => panic!("Task already started"),
86+
TaskClosureInner::Complete(_) => panic!("Task already executed"),
87+
TaskClosureInner::Finished => panic!("Task already finished"),
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)