diff --git a/CHANGELOG.md b/CHANGELOG.md index ed23502d26..6d706b6411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 12e04864f7..0ca4b70f3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,26 +623,53 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541" +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-lite" @@ -659,6 +686,47 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.28.0" @@ -1453,6 +1521,17 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-example-async-api-client" +version = "0.26.1" +dependencies = [ + "async-trait", + "serde", + "serde_json", + "thiserror", + "uniffi", +] + [[package]] name = "uniffi-example-callbacks" version = "0.22.0" @@ -1661,6 +1740,7 @@ name = "uniffi-fixture-futures" version = "0.21.0" dependencies = [ "async-trait", + "futures", "once_cell", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 4bc0da3b0b..bbeb7a8bb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/docs/manual/src/futures.md b/docs/manual/src/futures.md index 0e1d22ea85..26a7e420be 100644 --- a/docs/manual/src/futures.md +++ b/docs/manual/src/futures.md @@ -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. + diff --git a/examples/README.md b/examples/README.md index a737ef202f..4574e8b94a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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: diff --git a/examples/async-api-client/Cargo.toml b/examples/async-api-client/Cargo.toml new file mode 100644 index 0000000000..735ad59378 --- /dev/null +++ b/examples/async-api-client/Cargo.toml @@ -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"] } diff --git a/examples/async-api-client/build.rs b/examples/async-api-client/build.rs new file mode 100644 index 0000000000..2b44dddc78 --- /dev/null +++ b/examples/async-api-client/build.rs @@ -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(); +} diff --git a/examples/async-api-client/src/async-api-client.udl b/examples/async-api-client/src/async-api-client.udl new file mode 100644 index 0000000000..d041fb4b03 --- /dev/null +++ b/examples/async-api-client/src/async-api-client.udl @@ -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); +}; diff --git a/examples/async-api-client/src/lib.rs b/examples/async-api-client/src/lib.rs new file mode 100644 index 0000000000..851de6b8a1 --- /dev/null +++ b/examples/async-api-client/src/lib.rs @@ -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 = std::result::Result; + +#[async_trait::async_trait] +pub trait HttpClient: Send + Sync { + async fn fetch(&self, url: String) -> Result; +} + +#[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, +} + +impl ApiClient { + pub fn new(http_client: Arc) -> Self { + Self { http_client } + } + + pub async fn get_issue( + &self, + owner: String, + repository: String, + issue_number: u32, + ) -> Result { + 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 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"); diff --git a/examples/async-api-client/tests/bindings/test_async_api_client.kts b/examples/async-api-client/tests/bindings/test_async_api_client.kts new file mode 100644 index 0000000000..589a1c82ca --- /dev/null +++ b/examples/async-api-client/tests/bindings/test_async_api_client.kts @@ -0,0 +1,24 @@ +/* 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/. */ + +import uniffi.async_api_client.* + +class KtHttpClient : HttpClient { + override suspend fun fetch(url: String): String { + // In the real-world we would use an async HTTP library and make a real + // HTTP request, but to keep the dependencies simple and avoid test + // fragility we just fake it. + if (url == "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017") { + return testResponseData() + } else { + throw ApiException.Http("Wrong URL: ${url}") + } + } +} + +kotlinx.coroutines.runBlocking { + val client = ApiClient(KtHttpClient()) + val issue = client.getIssue("mozilla", "uniffi-rs", 2017u) + assert(issue.title == "Foreign-implemented async traits") +} diff --git a/examples/async-api-client/tests/bindings/test_async_api_client.py b/examples/async-api-client/tests/bindings/test_async_api_client.py new file mode 100644 index 0000000000..de9d4c3563 --- /dev/null +++ b/examples/async-api-client/tests/bindings/test_async_api_client.py @@ -0,0 +1,24 @@ +import asyncio +import unittest +from urllib.request import urlopen +from async_api_client import * + +# Http client that the Rust code depends on +class PyHttpClient(HttpClient): + async def fetch(self, url): + # In the real-world we would use something like aiohttp and make a real HTTP request, but to keep + # the dependencies simple and avoid test fragility we just fake it. + await asyncio.sleep(0.01) + if url == "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017": + return test_response_data() + else: + raise ApiError.Http(f"Wrong URL: {url}") + +class CallbacksTest(unittest.IsolatedAsyncioTestCase): + async def test_api_client(self): + client = ApiClient(PyHttpClient()) + issue = await client.get_issue("mozilla", "uniffi-rs", 2017) + self.assertEqual(issue.title, "Foreign-implemented async traits") + +unittest.main() + diff --git a/examples/async-api-client/tests/bindings/test_async_api_client.swift b/examples/async-api-client/tests/bindings/test_async_api_client.swift new file mode 100644 index 0000000000..87777afa27 --- /dev/null +++ b/examples/async-api-client/tests/bindings/test_async_api_client.swift @@ -0,0 +1,32 @@ +/* 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/. */ + +import Foundation // To get `DispatchGroup` + +#if canImport(async_api_client) + import async_api_client +#endif + +class SwiftHttpClient : HttpClient { + func fetch(url: String) async throws -> String { + // In the real-world we would use an async HTTP library and make a real + // HTTP request, but to keep the dependencies simple and avoid test + // fragility we just fake it. + if (url == "https://api.github.com/repos/mozilla/uniffi-rs/issues/2017") { + return testResponseData() + } else { + throw ApiError.Http(reason: "Wrong URL: \(url)") + } + } +} + +var counter = DispatchGroup() +counter.enter() +Task { + let client = ApiClient(httpClient: SwiftHttpClient()) + let issue = try! await client.getIssue(owner: "mozilla", repository: "uniffi-rs", issueNumber: 2017) + assert(issue.title == "Foreign-implemented async traits") + counter.leave() +} +counter.wait() diff --git a/examples/async-api-client/tests/test_generated_bindings.rs b/examples/async-api-client/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..609bbe4c2b --- /dev/null +++ b/examples/async-api-client/tests/test_generated_bindings.rs @@ -0,0 +1,5 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test_async_api_client.kts", + "tests/bindings/test_async_api_client.swift", + "tests/bindings/test_async_api_client.py", +); diff --git a/fixtures/futures/Cargo.toml b/fixtures/futures/Cargo.toml index f386c6d85c..be22f29041 100644 --- a/fixtures/futures/Cargo.toml +++ b/fixtures/futures/Cargo.toml @@ -17,6 +17,7 @@ path = "src/bin.rs" [dependencies] uniffi = { workspace = true, features = ["tokio", "cli"] } async-trait = "0.1" +futures = "0.3" thiserror = "1.0" tokio = { version = "1.24.1", features = ["time", "sync"] } once_cell = "1.18.0" diff --git a/fixtures/futures/src/lib.rs b/fixtures/futures/src/lib.rs index 4b4ed1cca9..15bc32b9cf 100644 --- a/fixtures/futures/src/lib.rs +++ b/fixtures/futures/src/lib.rs @@ -11,6 +11,8 @@ use std::{ time::Duration, }; +use futures::future::{AbortHandle, Abortable, Aborted}; + /// Non-blocking timer future. pub struct TimerFuture { shared_state: Arc>, @@ -387,4 +389,71 @@ fn get_say_after_udl_traits() -> Vec> { vec![Arc::new(SayAfterImpl1), Arc::new(SayAfterImpl2)] } +// Async callback interface implemented in foreign code +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait AsyncParser: Send + Sync { + // Simple async method + async fn as_string(&self, delay_ms: i32, value: i32) -> String; + // Async method that can throw + async fn try_from_string(&self, delay_ms: i32, value: String) -> Result; + // Void return, which requires special handling + async fn delay(&self, delay_ms: i32); + // Void return that can also throw + async fn try_delay(&self, delay_ms: String) -> Result<(), ParserError>; +} + +#[derive(thiserror::Error, uniffi::Error, Debug)] +pub enum ParserError { + #[error("NotAnInt")] + NotAnInt, + #[error("UnexpectedError")] + UnexpectedError, +} + +impl From for ParserError { + fn from(_: uniffi::UnexpectedUniFFICallbackError) -> Self { + Self::UnexpectedError + } +} + +#[uniffi::export] +async fn as_string_using_trait(obj: Arc, delay_ms: i32, value: i32) -> String { + obj.as_string(delay_ms, value).await +} + +#[uniffi::export] +async fn try_from_string_using_trait( + obj: Arc, + delay_ms: i32, + value: String, +) -> Result { + obj.try_from_string(delay_ms, value).await +} + +#[uniffi::export] +async fn delay_using_trait(obj: Arc, delay_ms: i32) { + obj.delay(delay_ms).await +} + +#[uniffi::export] +async fn try_delay_using_trait( + obj: Arc, + delay_ms: String, +) -> Result<(), ParserError> { + obj.try_delay(delay_ms).await +} + +#[uniffi::export] +async fn cancel_delay_using_trait(obj: Arc, delay_ms: i32) { + let (abort_handle, abort_registration) = AbortHandle::new_pair(); + thread::spawn(move || { + // Simulate a different thread aborting the process + thread::sleep(Duration::from_millis(1)); + abort_handle.abort(); + }); + let future = Abortable::new(obj.delay(delay_ms), abort_registration); + assert_eq!(future.await, Err(Aborted)); +} + uniffi::include_scaffolding!("futures"); diff --git a/fixtures/futures/tests/bindings/test_futures.kts b/fixtures/futures/tests/bindings/test_futures.kts index 175f4a619a..f853ddb4ea 100644 --- a/fixtures/futures/tests/bindings/test_futures.kts +++ b/fixtures/futures/tests/bindings/test_futures.kts @@ -140,6 +140,78 @@ runBlocking { assertApproximateTime(time, 200, "async methods") } +// Test foreign implemented async trait methods +class KotlinAsyncParser: AsyncParser { + var completedDelays: Int = 0 + + override suspend fun asString(delayMs: Int, value: Int): String { + delay(delayMs.toLong()) + return value.toString() + } + + override suspend fun tryFromString(delayMs: Int, value: String): Int { + delay(delayMs.toLong()) + if (value == "force-unexpected-exception") { + throw RuntimeException("UnexpectedException") + } + try { + return value.toInt() + } catch (e: NumberFormatException) { + throw ParserException.NotAnInt() + } + } + + override suspend fun delay(delayMs: Int) { + delay(delayMs.toLong()) + completedDelays += 1 + } + + override suspend fun tryDelay(delayMs: String) { + val parsed = try { + delayMs.toLong() + } catch (e: NumberFormatException) { + throw ParserException.NotAnInt() + } + delay(parsed) + completedDelays += 1 + } +} + +runBlocking { + val traitObj = KotlinAsyncParser(); + assert(asStringUsingTrait(traitObj, 1, 42) == "42") + assert(tryFromStringUsingTrait(traitObj, 1, "42") == 42) + try { + tryFromStringUsingTrait(traitObj, 1, "fourty-two") + throw RuntimeException("Expected last statement to throw") + } catch(e: ParserException.NotAnInt) { + // Expected + } + try { + tryFromStringUsingTrait(traitObj, 1, "force-unexpected-exception") + throw RuntimeException("Expected last statement to throw") + } catch(e: ParserException.UnexpectedException) { + // Expected + } + delayUsingTrait(traitObj, 1) + try { + tryDelayUsingTrait(traitObj, "one") + throw RuntimeException("Expected last statement to throw") + } catch(e: ParserException.NotAnInt) { + // Expected + } + val completedDelaysBefore = traitObj.completedDelays + cancelDelayUsingTrait(traitObj, 10) + // sleep long enough so that the `delay()` call would finish if it wasn't cancelled. + delay(100) + // If the task was cancelled, then completedDelays won't have increased + assert(traitObj.completedDelays == completedDelaysBefore) + + // Test that all handles were cleaned up + assert(uniffiForeignFutureHandleCount() == 0) +} + + // Test with the Tokio runtime. runBlocking { val time = measureTimeMillis { diff --git a/fixtures/futures/tests/bindings/test_futures.py b/fixtures/futures/tests/bindings/test_futures.py index 7b2e12324f..1b84451b5d 100644 --- a/fixtures/futures/tests/bindings/test_futures.py +++ b/fixtures/futures/tests/bindings/test_futures.py @@ -3,6 +3,7 @@ from datetime import datetime import asyncio import typing +import futures def now(): return datetime.now() @@ -105,6 +106,61 @@ async def test(): asyncio.run(test()) + def test_foreign_async_trait_interface_methods(self): + class PyAsyncParser: + def __init__(self): + self.completed_delays = 0 + + async def as_string(self, delay_ms, value): + await asyncio.sleep(delay_ms / 1000.0) + return str(value) + + async def try_from_string(self, delay_ms, value): + await asyncio.sleep(delay_ms / 1000.0) + if value == "force-unexpected-exception": + raise RuntimeError("UnexpectedException") + try: + return int(value) + except: + raise ParserError.NotAnInt() + + async def delay(self, delay_ms): + await asyncio.sleep(delay_ms / 1000.0) + self.completed_delays += 1 + + async def try_delay(self, delay_ms): + try: + delay_ms = int(delay_ms) + except: + raise ParserError.NotAnInt() + await asyncio.sleep(delay_ms / 1000.0) + self.completed_delays += 1 + + async def test(): + trait_obj = PyAsyncParser() + self.assertEqual(await as_string_using_trait(trait_obj, 1, 42), "42") + self.assertEqual(await try_from_string_using_trait(trait_obj, 1, "42"), 42) + with self.assertRaises(ParserError.NotAnInt): + await try_from_string_using_trait(trait_obj, 1, "fourty-two") + with self.assertRaises(ParserError.UnexpectedError): + await try_from_string_using_trait(trait_obj, 1, "force-unexpected-exception") + await delay_using_trait(trait_obj, 1) + await try_delay_using_trait(trait_obj, "1") + with self.assertRaises(ParserError.NotAnInt): + await try_delay_using_trait(trait_obj, "one") + + completed_delays_before = trait_obj.completed_delays + await cancel_delay_using_trait(trait_obj, 10) + # sleep long enough so that the `delay()` call would finish if it wasn't cancelled. + await asyncio.sleep(0.1) + # If the task was cancelled, then completed_delays won't have increased + self.assertEqual(trait_obj.completed_delays, completed_delays_before) + + + asyncio.run(test()) + # check that all foreign future handles were released + self.assertEqual(len(futures.UNIFFI_FOREIGN_FUTURE_HANDLE_MAP), 0) + def test_async_object_param(self): async def test(): megaphone = new_megaphone() diff --git a/fixtures/futures/tests/bindings/test_futures.swift b/fixtures/futures/tests/bindings/test_futures.swift index 2fd413a8ab..11dacd870e 100644 --- a/fixtures/futures/tests/bindings/test_futures.swift +++ b/fixtures/futures/tests/bindings/test_futures.swift @@ -150,6 +150,97 @@ Task { counter.leave() } +// Test foreign implemented async trait methods +counter.enter() + +struct UnexpectedError : Error { } + +class SwiftAsyncParser: AsyncParser { + var completedDelays: Int = 0 + + func asString(delayMs: Int32, value: Int32) async -> String { + try! await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000) + return String(value) + } + + func tryFromString(delayMs: Int32, value: String) async throws -> Int32 { + try! await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000) + + if (value == "force-unexpected-exception") { + throw UnexpectedError() + } + guard let result = Int32(value) else { + throw ParserError.NotAnInt + } + return result + } + + func delay(delayMs: Int32) async { + do { + try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000) + } catch is CancellationError { + return + } catch let error { + fatalError("Unexpected error in Task.sleep: \(error)") + } + completedDelays += 1 + } + + func tryDelay(delayMs: String) async throws { + guard let parsed = UInt64(delayMs) else { + throw ParserError.NotAnInt + } + do { + try await Task.sleep(nanoseconds: parsed * 1_000_000) + } catch is CancellationError { + return + } catch let error { + fatalError("Unexpected error in Task.sleep: \(error)") + } + completedDelays += 1 + } +} + +Task { + let traitObj = SwiftAsyncParser() + let result = await asStringUsingTrait(obj: traitObj, delayMs: 1, value: 42) + assert(result == "42") + let result2 = try! await tryFromStringUsingTrait(obj: traitObj, delayMs: 1, value: "42") + assert(result2 == 42) + do { + let _ = try await tryFromStringUsingTrait(obj: traitObj, delayMs: 1, value: "fourty-two") + fatalError("Expected previous statement to throw") + } catch ParserError.NotAnInt { + // Expected + } + do { + let _ = try await tryFromStringUsingTrait(obj: traitObj, delayMs: 1, value: "force-unexpected-exception") + fatalError("Expected previous statement to throw") + } catch ParserError.UnexpectedError { + // Expected + } + await delayUsingTrait(obj: traitObj, delayMs: 1) + try! await tryDelayUsingTrait(obj: traitObj, delayMs: "1") + do { + try await tryDelayUsingTrait(obj: traitObj, delayMs: "one") + fatalError("Expected previous statement to throw") + } catch ParserError.NotAnInt { + // Expected + } + + let completedDelaysBefore = traitObj.completedDelays + await cancelDelayUsingTrait(obj: traitObj, delayMs: 10) + // sleep long enough so that the `delay()` call would finish if it wasn't cancelled. + try! await Task.sleep(nanoseconds: 100_000_000) + // If the task was cancelled, then completedDelays won't have increased + assert(traitObj.completedDelays == completedDelaysBefore) + + // Test that all handles here cleaned up + assert(uniffiForeignFutureHandleCountFutures() == 0) + + counter.leave() +} + // Test async function returning an object counter.enter() diff --git a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs index ddcc3705cb..7986588e97 100644 --- a/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs +++ b/uniffi_bindgen/src/bindings/kotlin/gen_kotlin/mod.rs @@ -336,6 +336,7 @@ impl KotlinCodeOracle { fn ffi_type_label_by_value(&self, ffi_type: &FfiType) -> String { match ffi_type { FfiType::RustBuffer(_) => format!("{}.ByValue", self.ffi_type_label(ffi_type)), + FfiType::Struct(name) => format!("{}.UniffiByValue", self.ffi_struct_name(name)), _ => self.ffi_type_label(ffi_type), } } @@ -367,8 +368,9 @@ impl KotlinCodeOracle { FfiType::Float32 => "0.0f".to_owned(), FfiType::Float64 => "0.0".to_owned(), FfiType::RustArcPtr(_) => "Pointer.NULL".to_owned(), - FfiType::RustBuffer(_) => "UniffiRustBuffer.ByValue()".to_owned(), + FfiType::RustBuffer(_) => "RustBuffer.ByValue()".to_owned(), FfiType::Callback(_) => "null".to_owned(), + FfiType::RustCallStatus => "UniffiRustCallStatus.ByValue()".to_owned(), _ => unimplemented!("ffi_default_value: {ffi_type:?}"), } } @@ -408,6 +410,7 @@ impl KotlinCodeOracle { FfiType::RustBuffer(maybe_suffix) => { format!("RustBuffer{}", maybe_suffix.as_deref().unwrap_or_default()) } + FfiType::RustCallStatus => "UniffiRustCallStatus.ByValue".to_string(), FfiType::ForeignBytes => "ForeignBytes.ByValue".to_string(), FfiType::Callback(name) => self.ffi_callback_name(name), FfiType::Struct(name) => self.ffi_struct_name(name), diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt index dc547d4ddf..b28fbd2c80 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Async.kt @@ -39,3 +39,79 @@ internal suspend fun uniffiRustCallAsync( } } +{%- if ci.has_async_callback_interface_definition() %} +internal inline fun uniffiTraitInterfaceCallAsync( + crossinline makeCall: suspend () -> T, + crossinline handleSuccess: (T) -> Unit, + crossinline handleError: (UniffiRustCallStatus.ByValue) -> Unit, +): UniffiForeignFuture { + // Using `GlobalScope` is labeled as a "delicate API" and generally discouraged in Kotlin programs, since it breaks structured concurrency. + // However, our parent task is a Rust future, so we're going to need to break structure concurrency in any case. + // + // Uniffi does its best to support structured concurrency across the FFI. + // If the Rust future is dropped, `uniffiForeignFutureFreeImpl` is called, which will cancel the Kotlin coroutine if it's still running. + @OptIn(DelicateCoroutinesApi::class) + val job = GlobalScope.launch { + try { + handleSuccess(makeCall()) + } catch(e: Exception) { + handleError( + UniffiRustCallStatus.create( + UNIFFI_CALL_UNEXPECTED_ERROR, + {{ Type::String.borrow()|lower_fn }}(e.toString()), + ) + ) + } + } + val handle = uniffiForeignFutureHandleMap.insert(job) + return UniffiForeignFuture(handle, uniffiForeignFutureFreeImpl) +} + +internal inline fun uniffiTraitInterfaceCallAsyncWithError( + crossinline makeCall: suspend () -> T, + crossinline handleSuccess: (T) -> Unit, + crossinline handleError: (UniffiRustCallStatus.ByValue) -> Unit, + crossinline lowerError: (E) -> RustBuffer.ByValue, +): UniffiForeignFuture { + // See uniffiTraitInterfaceCallAsync for details on `DelicateCoroutinesApi` + @OptIn(DelicateCoroutinesApi::class) + val job = GlobalScope.launch { + try { + handleSuccess(makeCall()) + } catch(e: Exception) { + if (e is E) { + handleError( + UniffiRustCallStatus.create( + UNIFFI_CALL_ERROR, + lowerError(e), + ) + ) + } else { + handleError( + UniffiRustCallStatus.create( + UNIFFI_CALL_UNEXPECTED_ERROR, + {{ Type::String.borrow()|lower_fn }}(e.toString()), + ) + ) + } + } + } + val handle = uniffiForeignFutureHandleMap.insert(job) + return UniffiForeignFuture(handle, uniffiForeignFutureFreeImpl) +} + +internal val uniffiForeignFutureHandleMap = UniffiHandleMap() + +internal object uniffiForeignFutureFreeImpl: UniffiForeignFutureFree { + override fun callback(handle: Long) { + val job = uniffiForeignFutureHandleMap.remove(handle) + if (!job.isCompleted) { + job.cancel() + } + } +} + +// For testing +public fun uniffiForeignFutureHandleCount() = uniffiForeignFutureHandleMap.size + +{%- endif %} diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt index 80bc4d3399..30a39d9afb 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/CallbackInterfaceImpl.kt @@ -19,13 +19,14 @@ internal object {{ trait_impl }} { {%- when None %} {%- endmatch %} { val uniffiObj = {{ ffi_converter_name }}.handleMap.get(uniffiHandle) - val makeCall = { -> + val makeCall = {% if meth.is_async() %}suspend {% endif %}{ -> uniffiObj.{{ meth.name()|fn_name() }}( {%- for arg in meth.arguments() %} {{ arg|lift_fn }}({{ arg.name()|var_name }}), {%- endfor %} ) } + {%- if !meth.is_async() %} {%- match meth.return_type() %} {%- when Some(return_type) %} @@ -45,6 +46,52 @@ internal object {{ trait_impl }} { { e: {{error_type|type_name(ci) }} -> {{ error_type|lower_fn }}(e) } ) {%- endmatch %} + + {%- else %} + val uniffiHandleSuccess = { {% if meth.return_type().is_some() %}returnValue{% else %}_{% endif %}: {% match meth.return_type() %}{%- when Some(return_type) %}{{ return_type|type_name(ci) }}{%- when None %}Unit{% endmatch %} -> + val uniffiResult = {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}.UniffiByValue( + {%- match meth.return_type() %} + {%- when Some(return_type) %} + {{ return_type|lower_fn }}(returnValue), + {%- when None %} + {%- endmatch %} + UniffiRustCallStatus.ByValue() + ) + uniffiResult.write() + uniffiFutureCallback.callback(uniffiCallbackData, uniffiResult) + } + val uniffiHandleError = { callStatus: UniffiRustCallStatus.ByValue -> + uniffiFutureCallback.callback( + uniffiCallbackData, + {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}.UniffiByValue( + {%- match meth.return_type() %} + {%- when Some(return_type) %} + {{ return_type.into()|ffi_default_value }}, + {%- when None %} + {%- endmatch %} + callStatus, + ), + ) + } + + uniffiOutReturn.uniffiSetValue( + {%- match meth.throws_type() %} + {%- when None %} + uniffiTraitInterfaceCallAsync( + makeCall, + uniffiHandleSuccess, + uniffiHandleError + ) + {%- when Some(error_type) %} + uniffiTraitInterfaceCallAsyncWithError( + makeCall, + uniffiHandleSuccess, + uniffiHandleError, + { e: {{error_type|type_name(ci) }} -> {{ error_type|lower_fn }}(e) } + ) + {%- endmatch %} + ) + {%- endif %} } } {%- endfor %} @@ -59,7 +106,7 @@ internal object {{ trait_impl }} { {%- for (ffi_callback, meth) in vtable_methods.iter() %} {{ meth.name()|var_name() }}, {%- endfor %} - uniffiFree + uniffiFree, ) // Registers the foreign callback with the Rust side. diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt index b9e55ff821..1fdbd3ffc0 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Helpers.kt @@ -23,6 +23,15 @@ internal open class UniffiRustCallStatus : Structure() { fun isPanic(): Boolean { return code == UNIFFI_CALL_UNEXPECTED_ERROR } + + companion object { + fun create(code: Byte, errorBuf: RustBuffer.ByValue): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } } class InternalException(message: String) : Exception(message) diff --git a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt index ba56716401..c27121b701 100644 --- a/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt +++ b/uniffi_bindgen/src/bindings/kotlin/templates/Types.kt @@ -134,6 +134,10 @@ object NoPointer {%- if ci.has_async_fns() %} {# Import types needed for async support #} {{ self.add_import("kotlin.coroutines.resume") }} +{{ self.add_import("kotlinx.coroutines.launch") }} {{ self.add_import("kotlinx.coroutines.suspendCancellableCoroutine") }} {{ self.add_import("kotlinx.coroutines.CancellableContinuation") }} +{{ self.add_import("kotlinx.coroutines.DelicateCoroutinesApi") }} +{{ self.add_import("kotlinx.coroutines.Job") }} +{{ self.add_import("kotlinx.coroutines.GlobalScope") }} {%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs index 6c1996eb5c..d29812b177 100644 --- a/uniffi_bindgen/src/bindings/python/gen_python/mod.rs +++ b/uniffi_bindgen/src/bindings/python/gen_python/mod.rs @@ -367,6 +367,7 @@ impl PythonCodeOracle { Some(suffix) => format!("_UniffiRustBuffer{suffix}"), None => "_UniffiRustBuffer".to_string(), }, + FfiType::RustCallStatus => "_UniffiRustCallStatus".to_string(), FfiType::ForeignBytes => "_UniffiForeignBytes".to_string(), FfiType::Callback(name) => self.ffi_callback_name(name), FfiType::Struct(name) => self.ffi_struct_name(name), @@ -376,6 +377,33 @@ impl PythonCodeOracle { } } + /// Default values for FFI types + /// + /// Used to set a default return value when returning an error + fn ffi_default_value(&self, return_type: Option<&FfiType>) -> String { + match return_type { + Some(t) => match t { + FfiType::UInt8 + | FfiType::Int8 + | FfiType::UInt16 + | FfiType::Int16 + | FfiType::UInt32 + | FfiType::Int32 + | FfiType::UInt64 + | FfiType::Int64 => "0".to_owned(), + FfiType::Float32 | FfiType::Float64 => "0.0".to_owned(), + FfiType::RustArcPtr(_) => "ctypes.c_void_p()".to_owned(), + FfiType::RustBuffer(maybe_suffix) => match maybe_suffix { + Some(suffix) => format!("_UniffiRustBuffer{suffix}.default()"), + None => "_UniffiRustBuffer.default()".to_owned(), + }, + _ => unimplemented!("FFI return type: {t:?}"), + }, + // When we need to use a value for void returns, we use a `u8` placeholder + None => "0".to_owned(), + } + } + /// Get the name of the protocol and class name for an object. /// /// If we support callback interfaces, the protocol name is the object name, and the class name is derived from that. @@ -501,6 +529,10 @@ pub mod filters { Ok(PythonCodeOracle.ffi_type_label(type_)) } + pub fn ffi_default_value(return_type: Option) -> Result { + Ok(PythonCodeOracle.ffi_default_value(return_type.as_ref())) + } + /// Get the idiomatic Python rendering of a class name (for enums, records, errors, etc). pub fn class_name(nm: &str) -> Result { Ok(PythonCodeOracle.class_name(nm)) diff --git a/uniffi_bindgen/src/bindings/python/templates/Async.py b/uniffi_bindgen/src/bindings/python/templates/Async.py index 4a230112ea..26daa9ba5c 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Async.py +++ b/uniffi_bindgen/src/bindings/python/templates/Async.py @@ -5,6 +5,30 @@ # Stores futures for _uniffi_continuation_callback _UniffiContinuationHandleMap = _UniffiHandleMap() +UNIFFI_GLOBAL_EVENT_LOOP = None + +""" +Set the event loop to use for async functions + +This is needed if some async functions run outside of the eventloop, for example: + - A non-eventloop thread is spawned, maybe from `EventLoop.run_in_executor` or maybe from the + Rust code spawning its own thread. + - The Rust code calls an async callback method from a sync callback function, 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 force an eventloop to be used in this case. +""" +def uniffi_set_event_loop(eventloop: asyncio.BaseEventLoop): + global UNIFFI_GLOBAL_EVENT_LOOP + UNIFFI_GLOBAL_EVENT_LOOP = eventloop + +def _uniffi_get_event_loop(): + if UNIFFI_GLOBAL_EVENT_LOOP is not None: + return UNIFFI_GLOBAL_EVENT_LOOP + else: + return asyncio.get_running_loop() + # Continuation callback for async functions # lift the return value or error and resolve the future, causing the async function to resume. @UNIFFI_RUST_FUTURE_CONTINUATION_CALLBACK @@ -18,7 +42,7 @@ def _uniffi_set_future_result(future, poll_code): async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, lift_func, error_ffi_converter): try: - eventloop = asyncio.get_running_loop() + eventloop = _uniffi_get_event_loop() # Loop and poll until we see a _UNIFFI_RUST_FUTURE_POLL_READY value while True: @@ -37,3 +61,54 @@ async def _uniffi_rust_call_async(rust_future, ffi_poll, ffi_complete, ffi_free, ) finally: ffi_free(rust_future) + +{%- if ci.has_async_callback_interface_definition() %} +def uniffi_trait_interface_call_async(make_call, handle_success, handle_error): + async def make_call_and_call_callback(): + try: + handle_success(await make_call()) + except Exception as e: + print("UniFFI: Unhandled exception in trait interface call", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + handle_error( + _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR, + {{ Type::String.borrow()|lower_fn }}(repr(e)), + ) + eventloop = _uniffi_get_event_loop() + task = asyncio.run_coroutine_threadsafe(make_call_and_call_callback(), eventloop) + handle = UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.insert((eventloop, task)) + return UniffiForeignFuture(handle, uniffi_foreign_future_free) + +def uniffi_trait_interface_call_async_with_error(make_call, handle_success, handle_error, error_type, lower_error): + async def make_call_and_call_callback(): + try: + try: + handle_success(await make_call()) + except error_type as e: + handle_error( + _UniffiRustCallStatus.CALL_ERROR, + lower_error(e), + ) + except Exception as e: + print("UniFFI: Unhandled exception in trait interface call", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + handle_error( + _UniffiRustCallStatus.CALL_UNEXPECTED_ERROR, + {{ Type::String.borrow()|lower_fn }}(repr(e)), + ) + eventloop = _uniffi_get_event_loop() + task = asyncio.run_coroutine_threadsafe(make_call_and_call_callback(), eventloop) + handle = UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.insert((eventloop, task)) + return UniffiForeignFuture(handle, uniffi_foreign_future_free) + +UNIFFI_FOREIGN_FUTURE_HANDLE_MAP = _UniffiHandleMap() + +@UNIFFI_FOREIGN_FUTURE_FREE +def uniffi_foreign_future_free(handle): + (eventloop, task) = UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.remove(handle) + eventloop.call_soon(uniffi_foreign_future_do_free, task) + +def uniffi_foreign_future_do_free(task): + if not task.done(): + task.cancel() +{%- endif %} diff --git a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py index 82907e6cd3..676f01177a 100644 --- a/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py +++ b/uniffi_bindgen/src/bindings/python/templates/CallbackInterfaceImpl.py @@ -20,6 +20,8 @@ def make_call(): args = ({% for arg in meth.arguments() %}{{ arg|lift_fn }}({{ arg.name()|var_name }}), {% endfor %}) method = uniffi_obj.{{ meth.name()|fn_name }} return method(*args) + + {% if !meth.is_async() %} {%- match meth.return_type() %} {%- when Some(return_type) %} def write_return_value(v): @@ -44,6 +46,40 @@ def write_return_value(v): {{ error|lower_fn }}, ) {%- endmatch %} + {%- else %} + def handle_success(return_value): + uniffi_future_callback( + uniffi_callback_data, + {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( + {%- match meth.return_type() %} + {%- when Some(return_type) %} + {{ return_type|lower_fn }}(return_value), + {%- when None %} + {%- endmatch %} + _UniffiRustCallStatus.default() + ) + ) + + def handle_error(status_code, rust_buffer): + uniffi_future_callback( + uniffi_callback_data, + {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( + {%- match meth.return_type() %} + {%- when Some(return_type) %} + {{ meth.return_type().map(FfiType::from)|ffi_default_value }}, + {%- when None %} + {%- endmatch %} + _UniffiRustCallStatus(status_code, rust_buffer), + ) + ) + + {%- match meth.throws_type() %} + {%- when None %} + uniffi_out_return[0] = uniffi_trait_interface_call_async(make_call, handle_success, handle_error) + {%- when Some(error) %} + uniffi_out_return[0] = uniffi_trait_interface_call_async_with_error(make_call, handle_success, handle_error, {{ error|type_name }}, {{ error|lower_fn }}) + {%- endmatch %} + {%- endif %} {%- endfor %} @{{ "CallbackInterfaceFree"|ffi_callback_name }} diff --git a/uniffi_bindgen/src/bindings/python/templates/HandleMap.py b/uniffi_bindgen/src/bindings/python/templates/HandleMap.py index 30472a067d..f7c13cf745 100644 --- a/uniffi_bindgen/src/bindings/python/templates/HandleMap.py +++ b/uniffi_bindgen/src/bindings/python/templates/HandleMap.py @@ -29,3 +29,5 @@ def remove(self, handle): except KeyError: raise InternalError("UniffiHandleMap.remove: Invalid handle") + def __len__(self): + return len(self._map) diff --git a/uniffi_bindgen/src/bindings/python/templates/Helpers.py b/uniffi_bindgen/src/bindings/python/templates/Helpers.py index b349e3ac29..5d4bcbba89 100644 --- a/uniffi_bindgen/src/bindings/python/templates/Helpers.py +++ b/uniffi_bindgen/src/bindings/python/templates/Helpers.py @@ -18,6 +18,10 @@ class _UniffiRustCallStatus(ctypes.Structure): CALL_ERROR = 1 CALL_UNEXPECTED_ERROR = 2 + @staticmethod + def default(): + return _UniffiRustCallStatus(code=_UniffiRustCallStatus.CALL_SUCCESS, error_buf=_UniffiRustBuffer.default()) + def __str__(self): if self.code == _UniffiRustCallStatus.CALL_SUCCESS: return "_UniffiRustCallStatus(CALL_SUCCESS)" @@ -37,7 +41,7 @@ def _rust_call_with_error(error_ffi_converter, fn, *args): # # This function is used for rust calls that return Result<> and therefore can set the CALL_ERROR status code. # error_ffi_converter must be set to the _UniffiConverter for the error class that corresponds to the result. - call_status = _UniffiRustCallStatus(code=_UniffiRustCallStatus.CALL_SUCCESS, error_buf=_UniffiRustBuffer(0, 0, None)) + call_status = _UniffiRustCallStatus.default() args_with_error = args + (ctypes.byref(call_status),) result = fn(*args_with_error) diff --git a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py index f1791f03e0..44e0ba1001 100644 --- a/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py +++ b/uniffi_bindgen/src/bindings/python/templates/RustBufferTemplate.py @@ -6,6 +6,10 @@ class _UniffiRustBuffer(ctypes.Structure): ("data", ctypes.POINTER(ctypes.c_char)), ] + @staticmethod + def default(): + return _UniffiRustBuffer(0, 0, None) + @staticmethod def alloc(size): return _rust_call(_UniffiLib.{{ ci.ffi_rustbuffer_alloc().name() }}, size) diff --git a/uniffi_bindgen/src/bindings/python/templates/wrapper.py b/uniffi_bindgen/src/bindings/python/templates/wrapper.py index 2050b8d589..1ccd6821c0 100644 --- a/uniffi_bindgen/src/bindings/python/templates/wrapper.py +++ b/uniffi_bindgen/src/bindings/python/templates/wrapper.py @@ -25,6 +25,7 @@ import datetime import threading import itertools +import traceback import typing {%- if ci.has_async_fns() %} import asyncio @@ -45,14 +46,14 @@ # Contains loading, initialization code, and the FFI Function declarations. {% include "NamespaceLibraryTemplate.py" %} +# Public interface members begin here. +{{ type_helper_code }} + # Async support {%- if ci.has_async_fns() %} {%- include "Async.py" %} {%- endif %} -# Public interface members begin here. -{{ type_helper_code }} - {%- for func in ci.function_definitions() %} {%- include "TopLevelFunctionTemplate.py" %} {%- endfor %} @@ -74,6 +75,9 @@ {%- for c in ci.callback_interface_definitions() %} "{{ c.name()|class_name }}", {%- endfor %} + {%- if ci.has_async_fns() %} + "uniffi_set_event_loop", + {%- endif %} ] {% import "macros.py" as py %} diff --git a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs index 07da3882b6..d4d52121f0 100644 --- a/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs +++ b/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -152,6 +152,7 @@ mod filters { FfiType::Handle => ":uint64".to_string(), FfiType::RustArcPtr(_) => ":pointer".to_string(), FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), + FfiType::RustCallStatus => "RustCallStatus".to_string(), FfiType::ForeignBytes => "ForeignBytes".to_string(), FfiType::Callback(_) => unimplemented!("FFI Callbacks not implemented"), // Note: this can't just be `unimplemented!()` because some of the FFI function diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index e33a2120ec..92d1860b57 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -537,8 +537,12 @@ impl SwiftCodeOracle { FfiType::Handle => "UInt64".into(), FfiType::RustArcPtr(_) => "UnsafeMutableRawPointer".into(), FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::RustCallStatus => "RustCallStatus".into(), FfiType::ForeignBytes => "ForeignBytes".into(), - FfiType::Callback(name) => self.ffi_callback_name(name), + // Note: @escaping is required for Swift versions before 5.7 for callbacks passed into + // async functions. Swift 5.7 and later does not require it. We should probably remove + // it once we upgrade our minimum requirement to 5.7 or later. + FfiType::Callback(name) => format!("@escaping {}", self.ffi_callback_name(name)), FfiType::Struct(name) => self.ffi_struct_name(name), FfiType::Reference(inner) => { format!("UnsafeMutablePointer<{}>", self.ffi_type_label(inner)) @@ -547,6 +551,30 @@ impl SwiftCodeOracle { } } + /// Default values for FFI types + /// + /// Used to set a default return value when returning an error + fn ffi_default_value(&self, return_type: Option<&FfiType>) -> String { + match return_type { + Some(t) => match t { + FfiType::UInt8 + | FfiType::Int8 + | FfiType::UInt16 + | FfiType::Int16 + | FfiType::UInt32 + | FfiType::Int32 + | FfiType::UInt64 + | FfiType::Int64 => "0".to_owned(), + FfiType::Float32 | FfiType::Float64 => "0.0".to_owned(), + FfiType::RustArcPtr(_) => "nil".to_owned(), + FfiType::RustBuffer(_) => "RustBuffer.empty()".to_owned(), + _ => unimplemented!("FFI return type: {t:?}"), + }, + // When we need to use a value for void returns, we use a `u8` placeholder + None => "0".to_owned(), + } + } + fn ffi_canonical_name(&self, ffi_type: &FfiType) -> String { self.ffi_type_label(ffi_type) } @@ -583,6 +611,13 @@ pub mod filters { Ok(oracle().find(&as_type.as_type()).type_label()) } + pub fn return_type_name(as_type: Option<&impl AsType>) -> Result { + Ok(match as_type { + Some(as_type) => oracle().find(&as_type.as_type()).type_label(), + None => "()".to_owned(), + }) + } + pub fn canonical_name(as_type: &impl AsType) -> Result { Ok(oracle().find(&as_type.as_type()).canonical_name()) } @@ -642,6 +677,10 @@ pub mod filters { Ok(oracle().ffi_canonical_name(ffi_type)) } + pub fn ffi_default_value(return_type: Option) -> Result { + Ok(oracle().ffi_default_value(return_type.as_ref())) + } + /// Like `ffi_type_name`, but used in `BridgingHeaderTemplate.h` which uses a slightly different /// names. pub fn header_ffi_type_name(ffi_type: &FfiType) -> Result { @@ -659,6 +698,7 @@ pub mod filters { FfiType::Handle => "uint64_t".into(), FfiType::RustArcPtr(_) => "void*_Nonnull".into(), FfiType::RustBuffer(_) => "RustBuffer".into(), + FfiType::RustCallStatus => "RustCallStatus".into(), FfiType::ForeignBytes => "ForeignBytes".into(), FfiType::Callback(name) => { format!("{} _Nonnull", SwiftCodeOracle.ffi_callback_name(name)) diff --git a/uniffi_bindgen/src/bindings/swift/templates/Async.swift b/uniffi_bindgen/src/bindings/swift/templates/Async.swift index 761e0dd70e..e16f3108e1 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/Async.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/Async.swift @@ -44,3 +44,73 @@ fileprivate func uniffiFutureContinuationCallback(handle: UInt64, pollResult: In print("uniffiFutureContinuationCallback invalid handle") } } + +{%- if ci.has_async_callback_interface_definition() %} +private func uniffiTraitInterfaceCallAsync( + makeCall: @escaping () async throws -> T, + handleSuccess: @escaping (T) -> (), + handleError: @escaping (Int8, RustBuffer) -> () +) -> UniffiForeignFuture { + let task = Task { + do { + handleSuccess(try await makeCall()) + } catch { + handleError(CALL_UNEXPECTED_ERROR, {{ Type::String.borrow()|lower_fn }}(String(describing: error))) + } + } + let handle = UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.insert(obj: task) + return UniffiForeignFuture(handle: handle, free: uniffiForeignFutureFree) + +} + +private func uniffiTraitInterfaceCallAsyncWithError( + makeCall: @escaping () async throws -> T, + handleSuccess: @escaping (T) -> (), + handleError: @escaping (Int8, RustBuffer) -> (), + lowerError: @escaping (E) -> RustBuffer +) -> UniffiForeignFuture { + let task = Task { + do { + handleSuccess(try await makeCall()) + } catch let error as E { + handleError(CALL_ERROR, lowerError(error)) + } catch { + handleError(CALL_UNEXPECTED_ERROR, {{ Type::String.borrow()|lower_fn }}(String(describing: error))) + } + } + let handle = UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.insert(obj: task) + return UniffiForeignFuture(handle: handle, free: uniffiForeignFutureFree) +} + +// Borrow the callback handle map implementation to store foreign future handles +// TODO: consolidate the handle-map code (https://github.com/mozilla/uniffi-rs/pull/1823) +fileprivate var UNIFFI_FOREIGN_FUTURE_HANDLE_MAP = UniffiHandleMap() + +// Protocol for tasks that handle foreign futures. +// +// Defining a protocol allows all tasks to be stored in the same handle map. This can't be done +// with the task object itself, since has generic parameters. +protocol UniffiForeignFutureTask { + func cancel() +} + +extension Task: UniffiForeignFutureTask {} + +private func uniffiForeignFutureFree(handle: UInt64) { + do { + let task = try UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.remove(handle: handle) + // Set the cancellation flag on the task. If it's still running, the code can check the + // cancellation flag or call `Task.checkCancellation()`. If the task has completed, this is + // a no-op. + task.cancel() + } catch { + print("uniffiForeignFutureFree: handle missing from handlemap") + } +} + +// For testing +public func uniffiForeignFutureHandleCount{{ ci.namespace()|class_name }}() -> Int { + UNIFFI_FOREIGN_FUTURE_HANDLE_MAP.count +} + +{%- endif %} diff --git a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift index 2db6729c30..74ee372642 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/CallbackInterfaceImpl.swift @@ -10,25 +10,25 @@ fileprivate struct {{ trait_impl }} { {%- for (ffi_callback, meth) in vtable_methods %} {{ meth.name()|fn_name }}: { ( {%- for arg in ffi_callback.arguments() %} - {{ arg.name()|var_name }}: {{ arg.type_().borrow()|ffi_type_name }}, + {{ arg.name()|var_name }}: {{ arg.type_().borrow()|ffi_type_name }}{% if !loop.last || ffi_callback.has_rust_call_status_arg() %},{% endif %} {%- endfor -%} {%- if ffi_callback.has_rust_call_status_arg() %} uniffiCallStatus: UnsafeMutablePointer {%- endif %} ) in - let uniffiObj: {{ type_name }} - do { - try uniffiObj = {{ ffi_converter_name }}.handleMap.get(handle: uniffiHandle) - } catch { - uniffiCallStatus.pointee.code = CALL_UNEXPECTED_ERROR - uniffiCallStatus.pointee.errorBuf = {{ Type::String.borrow()|lower_fn }}("Callback handle map error: \(error)") - return + let makeCall = { + () {% if meth.is_async() %}async {% endif %}throws -> {% match meth.return_type() %}{% when Some(t) %}{{ t|type_name }}{% when None %}(){% endmatch %} in + guard let uniffiObj = try? {{ ffi_converter_name }}.handleMap.get(handle: uniffiHandle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return {% if meth.throws() %}try {% endif %}{% if meth.is_async() %}await {% endif %}uniffiObj.{{ meth.name()|fn_name }}( + {%- for arg in meth.arguments() %} + {% if !config.omit_argument_labels() %} {{ arg.name()|arg_name }}: {% endif %}try {{ arg|lift_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} + {%- endfor %} + ) } - let makeCall = { {% if meth.throws() %}try {% endif %}uniffiObj.{{ meth.name()|fn_name }}( - {%- for arg in meth.arguments() %} - {% if !config.omit_argument_labels() %} {{ arg.name()|arg_name }}: {% endif %}try {{ arg|lift_fn }}({{ arg.name()|var_name }}){% if !loop.last %},{% endif %} - {%- endfor %} - ) } + {%- if !meth.is_async() %} + {% match meth.return_type() %} {%- when Some(t) %} let writeReturn = { uniffiOutReturn.pointee = {{ t|lower_fn }}($0) } @@ -51,6 +51,52 @@ fileprivate struct {{ trait_impl }} { lowerError: {{ error_type|lower_fn }} ) {%- endmatch %} + {%- else %} + + let uniffiHandleSuccess = { (returnValue: {{ meth.return_type()|return_type_name }}) in + uniffiFutureCallback( + uniffiCallbackData, + {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( + {%- match meth.return_type() %} + {%- when Some(return_type) %} + returnValue: {{ return_type|lower_fn }}(returnValue), + {%- when None %} + {%- endmatch %} + callStatus: RustCallStatus() + ) + ) + } + let uniffiHandleError = { (statusCode, errorBuf) in + uniffiFutureCallback( + uniffiCallbackData, + {{ meth.foreign_future_ffi_result_struct().name()|ffi_struct_name }}( + {%- match meth.return_type() %} + {%- when Some(return_type) %} + returnValue: {{ meth.return_type().map(FfiType::from)|ffi_default_value }}, + {%- when None %} + {%- endmatch %} + callStatus: RustCallStatus(code: statusCode, errorBuf: errorBuf) + ) + ) + } + + {%- match meth.throws_type() %} + {%- when None %} + let uniffiForeignFuture = uniffiTraitInterfaceCallAsync( + makeCall: makeCall, + handleSuccess: uniffiHandleSuccess, + handleError: uniffiHandleError + ) + {%- when Some(error_type) %} + let uniffiForeignFuture = uniffiTraitInterfaceCallAsyncWithError( + makeCall: makeCall, + handleSuccess: uniffiHandleSuccess, + handleError: uniffiHandleError, + lowerError: {{ error_type|lower_fn }} + ) + {%- endmatch %} + uniffiOutReturn.pointee = uniffiForeignFuture + {%- endif %} }, {%- endfor %} uniffiFree: { (uniffiHandle: UInt64) -> () in diff --git a/uniffi_bindgen/src/bindings/swift/templates/HandleMap.swift b/uniffi_bindgen/src/bindings/swift/templates/HandleMap.swift index af0305872b..6de9f085d6 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/HandleMap.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/HandleMap.swift @@ -30,5 +30,11 @@ fileprivate class UniffiHandleMap { return obj } } + + var count: Int { + get { + map.count + } + } } diff --git a/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift index 2f737b6635..a053334a30 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/RustBufferTemplate.swift @@ -7,6 +7,10 @@ fileprivate extension RustBuffer { self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) } + static func empty() -> RustBuffer { + RustBuffer(capacity: 0, len:0, data: nil) + } + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { try! rustCall { {{ ci.ffi_rustbuffer_from_bytes().name() }}(ForeignBytes(bufferPointer: ptr), $0) } } diff --git a/uniffi_bindgen/src/interface/callbacks.rs b/uniffi_bindgen/src/interface/callbacks.rs index 90c34a9c17..f176a7a684 100644 --- a/uniffi_bindgen/src/interface/callbacks.rs +++ b/uniffi_bindgen/src/interface/callbacks.rs @@ -35,6 +35,7 @@ use std::iter; +use heck::ToUpperCamelCase; use uniffi_meta::Checksum; use super::ffi::{FfiArgument, FfiCallbackFunction, FfiField, FfiFunction, FfiStruct, FfiType}; @@ -107,6 +108,10 @@ impl CallbackInterface { pub fn docstring(&self) -> Option<&str> { self.docstring.as_deref() } + + pub fn has_async_method(&self) -> bool { + self.methods.iter().any(Method::is_async) + } } impl AsType for CallbackInterface { @@ -142,17 +147,80 @@ pub fn ffi_callbacks(trait_name: &str, methods: &[Method]) -> Vec FfiCallbackFunction { + if !method.is_async() { + FfiCallbackFunction { + name: method_ffi_callback_name(trait_name, index), + arguments: iter::once(FfiArgument::new("uniffi_handle", FfiType::UInt64)) + .chain(method.arguments().into_iter().map(Into::into)) + .chain(iter::once(match method.return_type() { + Some(t) => FfiArgument::new("uniffi_out_return", FfiType::from(t).reference()), + None => FfiArgument::new("uniffi_out_return", FfiType::VoidPointer), + })) + .collect(), + has_rust_call_status_arg: true, + return_type: None, + } + } else { + let completion_callback = + ffi_foreign_future_complete(method.return_type().map(FfiType::from)); + FfiCallbackFunction { + name: method_ffi_callback_name(trait_name, index), + arguments: iter::once(FfiArgument::new("uniffi_handle", FfiType::UInt64)) + .chain(method.arguments().into_iter().map(Into::into)) + .chain([ + FfiArgument::new( + "uniffi_future_callback", + FfiType::Callback(completion_callback.name), + ), + FfiArgument::new("uniffi_callback_data", FfiType::UInt64), + FfiArgument::new( + "uniffi_out_return", + FfiType::Struct("ForeignFuture".to_owned()).reference(), + ), + ]) + .collect(), + has_rust_call_status_arg: false, + return_type: None, + } + } +} + +/// Result struct to pass to the completion callback for async methods +pub fn foreign_future_ffi_result_struct(return_ffi_type: Option) -> FfiStruct { + let return_type_name = + FfiType::return_type_name(return_ffi_type.as_ref()).to_upper_camel_case(); + FfiStruct { + name: format!("ForeignFutureStruct{return_type_name}"), + fields: match return_ffi_type { + Some(return_ffi_type) => vec![ + FfiField::new("return_value", return_ffi_type), + FfiField::new("call_status", FfiType::RustCallStatus), + ], + None => vec![ + // In Rust, `return_value` is `()` -- a ZST. + // ZSTs are not valid in `C`, but they also take up 0 space. + // Skip the `return_value` field to make the layout correct. + FfiField::new("call_status", FfiType::RustCallStatus), + ], + }, + } +} + +/// Definition for callback functions to complete an async callback interface method +pub fn ffi_foreign_future_complete(return_ffi_type: Option) -> FfiCallbackFunction { + let return_type_name = + FfiType::return_type_name(return_ffi_type.as_ref()).to_upper_camel_case(); FfiCallbackFunction { - name: method_ffi_callback_name(trait_name, index), - arguments: iter::once(FfiArgument::new("uniffi_handle", FfiType::UInt64)) - .chain(method.arguments().into_iter().map(Into::into)) - .chain(iter::once(match method.return_type() { - Some(t) => FfiArgument::new("uniffi_out_return", FfiType::from(t).reference()), - None => FfiArgument::new("uniffi_out_return", FfiType::VoidPointer), - })) - .collect(), - has_rust_call_status_arg: true, + name: format!("ForeignFutureComplete{return_type_name}"), + arguments: vec![ + FfiArgument::new("callback_data", FfiType::UInt64), + FfiArgument::new( + "result", + FfiType::Struct(format!("ForeignFutureStruct{return_type_name}")), + ), + ], return_type: None, + has_rust_call_status_arg: false, } } diff --git a/uniffi_bindgen/src/interface/ffi.rs b/uniffi_bindgen/src/interface/ffi.rs index 19354e16dc..b27cb78477 100644 --- a/uniffi_bindgen/src/interface/ffi.rs +++ b/uniffi_bindgen/src/interface/ffi.rs @@ -57,6 +57,7 @@ pub enum FfiType { /// /// These are used to pass objects across the FFI. Handle, + RustCallStatus, /// Pointer to an FfiType. Reference(Box), /// Opaque pointer diff --git a/uniffi_bindgen/src/interface/mod.rs b/uniffi_bindgen/src/interface/mod.rs index 33160e21fe..90a941637a 100644 --- a/uniffi_bindgen/src/interface/mod.rs +++ b/uniffi_bindgen/src/interface/mod.rs @@ -249,6 +249,17 @@ impl ComponentInterface { self.callback_interfaces.iter().find(|o| o.name == name) } + /// Get the definitions for every Callback Interface type in the interface. + pub fn has_async_callback_interface_definition(&self) -> bool { + self.callback_interfaces + .iter() + .any(|cbi| cbi.has_async_method()) + || self + .objects + .iter() + .any(|o| o.has_callback_interface() && o.has_async_method()) + } + /// Get the definitions for every Method type in the interface. pub fn iter_callables(&self) -> impl Iterator { // Each of the `as &dyn Callable` casts is a trivial cast, but it seems like the clearest @@ -566,6 +577,10 @@ impl ComponentInterface { /// Does this interface contain async functions? pub fn has_async_fns(&self) -> bool { self.iter_ffi_function_definitions().any(|f| f.is_async()) + || self + .callback_interfaces + .iter() + .any(CallbackInterface::has_async_method) } /// Iterate over `T` parameters of the `FutureCallback` callbacks in this interface @@ -642,6 +657,15 @@ impl ComponentInterface { .into(), ] .into_iter() + .chain( + self.all_possible_return_ffi_types() + .flat_map(|return_type| { + [ + callbacks::foreign_future_ffi_result_struct(return_type.clone()).into(), + callbacks::ffi_foreign_future_complete(return_type).into(), + ] + }), + ) } /// List the definitions of all FFI functions in the interface. diff --git a/uniffi_bindgen/src/interface/object.rs b/uniffi_bindgen/src/interface/object.rs index 416fd5fd0a..5ef8332dfd 100644 --- a/uniffi_bindgen/src/interface/object.rs +++ b/uniffi_bindgen/src/interface/object.rs @@ -135,6 +135,10 @@ impl Object { self.imp.has_callback_interface() } + pub fn has_async_method(&self) -> bool { + self.methods.iter().any(Method::is_async) + } + pub fn constructors(&self) -> Vec<&Constructor> { self.constructors.iter().collect() } @@ -567,6 +571,11 @@ impl Method { .chain(self.return_type.iter().flat_map(Type::iter_types)), ) } + + /// For async callback interface methods, the FFI struct to pass to the completion function. + pub fn foreign_future_ffi_result_struct(&self) -> FfiStruct { + callbacks::foreign_future_ffi_result_struct(self.return_type.as_ref().map(FfiType::from)) + } } impl From for Method { diff --git a/uniffi_core/src/ffi/ffidefault.rs b/uniffi_core/src/ffi/ffidefault.rs index 97f12fb38f..a992ab7384 100644 --- a/uniffi_core/src/ffi/ffidefault.rs +++ b/uniffi_core/src/ffi/ffidefault.rs @@ -57,6 +57,13 @@ impl FfiDefault for crate::RustBuffer { } } +impl FfiDefault for crate::ForeignFuture { + fn ffi_default() -> Self { + extern "C" fn free(_handle: u64) {} + crate::ForeignFuture { handle: 0, free } + } +} + impl FfiDefault for Option { fn ffi_default() -> Self { None diff --git a/uniffi_core/src/ffi/foreignfuture.rs b/uniffi_core/src/ffi/foreignfuture.rs new file mode 100644 index 0000000000..be6a214e84 --- /dev/null +++ b/uniffi_core/src/ffi/foreignfuture.rs @@ -0,0 +1,241 @@ +/* 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/. */ + +//! This module defines a Rust Future that wraps an async foreign function call. +//! +//! The general idea is to create a [oneshot::Channel], hand the sender to the foreign side, and +//! await the receiver side on the Rust side. +//! +//! The foreign side should: +//! * Input a [ForeignFutureCallback] and a `u64` handle in their scaffolding function. +//! This is the sender, converted to a raw pointer, and an extern "C" function that sends the result. +//! * Return a [ForeignFuture], which represents the foreign task object corresponding to the async function. +//! * Call the [ForeignFutureCallback] when the async function completes with: +//! * The `u64` handle initially passed in +//! * The `ForeignFutureResult` for the call +//! * Wait for the [ForeignFutureHandle::free] function to be called to free the task object. +//! If this is called before the task completes, then the task will be cancelled. + +use crate::{LiftReturn, RustCallStatus, UnexpectedUniFFICallbackError}; + +/// Handle for a foreign future +pub type ForeignFutureHandle = u64; + +/// Handle for a callback data associated with a foreign future. +pub type ForeignFutureCallbackData = *mut (); + +/// Callback that's passed to a foreign async functions. +/// +/// See `LiftReturn` trait for how this is implemented. +pub type ForeignFutureCallback = + extern "C" fn(oneshot_handle: u64, ForeignFutureResult); + +/// C struct that represents the result of a foreign future +#[repr(C)] +pub struct ForeignFutureResult { + // Note: for void returns, T is `()`, which isn't directly representable with C since it's a ZST. + // Foreign code should treat that case as if there was no `return_value` field. + return_value: T, + call_status: RustCallStatus, +} + +/// Perform a call to a foreign async method + +/// C struct that represents the foreign future. +/// +/// This is what's returned by the async scaffolding functions. +#[repr(C)] +pub struct ForeignFuture { + pub handle: ForeignFutureHandle, + pub free: extern "C" fn(handle: ForeignFutureHandle), +} + +impl Drop for ForeignFuture { + fn drop(&mut self) { + (self.free)(self.handle) + } +} + +unsafe impl Send for ForeignFuture {} + +pub async fn foreign_async_call(call_scaffolding_function: F) -> T +where + F: FnOnce(ForeignFutureCallback, u64) -> ForeignFuture, + T: LiftReturn, +{ + let (sender, receiver) = oneshot::channel::>(); + // Keep the ForeignFuture around, even though we don't ever use it. + // The important thing is that the ForeignFuture will be dropped when this Future is. + let _foreign_future = + call_scaffolding_function(foreign_future_complete::, sender.into_raw() as u64); + match receiver.await { + Ok(result) => T::lift_foreign_return(result.return_value, result.call_status), + Err(e) => { + // This shouldn't happen in practice, but we can do our best to recover + T::handle_callback_unexpected_error(UnexpectedUniFFICallbackError::new(format!( + "Error awaiting foreign future: {e}" + ))) + } + } +} + +pub extern "C" fn foreign_future_complete, UT>( + oneshot_handle: u64, + result: ForeignFutureResult, +) { + let channel = unsafe { oneshot::Sender::from_raw(oneshot_handle as *mut ()) }; + // Ignore errors in send. + // + // Error means the receiver was already dropped which will happen when the future is cancelled. + let _ = channel.send(result); +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{Lower, RustBuffer}; + use once_cell::sync::OnceCell; + use std::{ + future::Future, + pin::Pin, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + task::{Context, Poll, Wake}, + }; + + struct MockForeignFuture { + freed: Arc, + callback_info: Arc, u64)>>, + rust_future: Option>>>, + } + + impl MockForeignFuture { + fn new() -> Self { + let callback_info = Arc::new(OnceCell::new()); + let freed = Arc::new(AtomicU32::new(0)); + + let rust_future: Pin>> = { + let callback_info = callback_info.clone(); + let freed = freed.clone(); + Box::pin(foreign_async_call::<_, String, crate::UniFfiTag>( + move |callback, data| { + callback_info.set((callback, data)).unwrap(); + ForeignFuture { + handle: Arc::into_raw(freed) as *mut () as u64, + free: Self::free, + } + }, + )) + }; + let rust_future = Some(rust_future); + let mut mock_foreign_future = Self { + freed, + callback_info, + rust_future, + }; + // Poll the future once, to start it up. This ensures that `callback_info` is set. + let _ = mock_foreign_future.poll(); + mock_foreign_future + } + + fn poll(&mut self) -> Poll { + let waker = Arc::new(NoopWaker).into(); + let mut context = Context::from_waker(&waker); + self.rust_future + .as_mut() + .unwrap() + .as_mut() + .poll(&mut context) + } + + fn complete_success(&self, value: String) { + let (callback, data) = self.callback_info.get().unwrap(); + callback( + *data, + ForeignFutureResult { + return_value: >::lower(value), + call_status: RustCallStatus::new(), + }, + ); + } + + fn complete_error(&self, error_message: String) { + let (callback, data) = self.callback_info.get().unwrap(); + callback( + *data, + ForeignFutureResult { + return_value: RustBuffer::default(), + call_status: RustCallStatus::error(error_message), + }, + ); + } + + fn drop_future(&mut self) { + self.rust_future = None + } + + fn free_count(&self) -> u32 { + self.freed.load(Ordering::Relaxed) + } + + extern "C" fn free(handle: u64) { + let flag = unsafe { Arc::from_raw(handle as *mut AtomicU32) }; + flag.fetch_add(1, Ordering::Relaxed); + } + } + + struct NoopWaker; + + impl Wake for NoopWaker { + fn wake(self: Arc) {} + } + + #[test] + fn test_foreign_future() { + let mut mock_foreign_future = MockForeignFuture::new(); + assert_eq!(mock_foreign_future.poll(), Poll::Pending); + mock_foreign_future.complete_success("It worked!".to_owned()); + assert_eq!( + mock_foreign_future.poll(), + Poll::Ready("It worked!".to_owned()) + ); + // Since the future is complete, it should free the foreign future + assert_eq!(mock_foreign_future.free_count(), 1); + } + + #[test] + #[should_panic] + fn test_foreign_future_error() { + let mut mock_foreign_future = MockForeignFuture::new(); + assert_eq!(mock_foreign_future.poll(), Poll::Pending); + mock_foreign_future.complete_error("It Failed!".to_owned()); + let _ = mock_foreign_future.poll(); + } + + #[test] + fn test_drop_after_complete() { + let mut mock_foreign_future = MockForeignFuture::new(); + mock_foreign_future.complete_success("It worked!".to_owned()); + assert_eq!(mock_foreign_future.free_count(), 0); + assert_eq!( + mock_foreign_future.poll(), + Poll::Ready("It worked!".to_owned()) + ); + // Dropping the future after it's complete should not panic, and not cause a double-free + mock_foreign_future.drop_future(); + assert_eq!(mock_foreign_future.free_count(), 1); + } + + #[test] + fn test_drop_before_complete() { + let mut mock_foreign_future = MockForeignFuture::new(); + mock_foreign_future.complete_success("It worked!".to_owned()); + // Dropping the future before it's complete should cancel the future + assert_eq!(mock_foreign_future.free_count(), 0); + mock_foreign_future.drop_future(); + assert_eq!(mock_foreign_future.free_count(), 1); + } +} diff --git a/uniffi_core/src/ffi/mod.rs b/uniffi_core/src/ffi/mod.rs index 8e26be37b0..acaf2b0d06 100644 --- a/uniffi_core/src/ffi/mod.rs +++ b/uniffi_core/src/ffi/mod.rs @@ -8,6 +8,7 @@ pub mod callbackinterface; pub mod ffidefault; pub mod foreignbytes; pub mod foreigncallbacks; +pub mod foreignfuture; pub mod handle; pub mod rustbuffer; pub mod rustcalls; @@ -17,6 +18,7 @@ pub use callbackinterface::*; pub use ffidefault::FfiDefault; pub use foreignbytes::*; pub use foreigncallbacks::*; +pub use foreignfuture::*; pub use handle::*; pub use rustbuffer::*; pub use rustcalls::*; diff --git a/uniffi_core/src/lib.rs b/uniffi_core/src/lib.rs index 3ae2983eab..1f3a2403f8 100644 --- a/uniffi_core/src/lib.rs +++ b/uniffi_core/src/lib.rs @@ -58,6 +58,7 @@ pub mod deps { pub use async_compat; pub use bytes; pub use log; + pub use oneshot; pub use static_assertions; } diff --git a/uniffi_macros/src/export/callback_interface.rs b/uniffi_macros/src/export/callback_interface.rs index 6b5f526aa3..fe145384ec 100644 --- a/uniffi_macros/src/export/callback_interface.rs +++ b/uniffi_macros/src/export/callback_interface.rs @@ -50,8 +50,25 @@ pub(super) fn trait_impl( let param_names = sig.scaffolding_param_names(); let param_types = sig.scaffolding_param_types(); let lift_return = sig.lift_return_impl(); - quote! { - #ident: extern "C" fn(handle: u64, #(#param_names: #param_types,)* &mut #lift_return::ReturnType, &mut ::uniffi::RustCallStatus), + if !sig.is_async { + quote! { + #ident: extern "C" fn( + uniffi_handle: u64, + #(#param_names: #param_types,)* + uniffi_out_return: &mut #lift_return::ReturnType, + uniffi_out_call_status: &mut ::uniffi::RustCallStatus, + ), + } + } else { + quote! { + #ident: extern "C" fn( + uniffi_handle: u64, + #(#param_names: #param_types,)* + uniffi_future_callback: ::uniffi::ForeignFutureCallback<#lift_return::ReturnType>, + uniffi_callback_data: u64, + uniffi_out_return: &mut ::uniffi::ForeignFuture, + ), + } } }); @@ -59,6 +76,8 @@ pub(super) fn trait_impl( .iter() .map(|sig| gen_method_impl(sig, &vtable_cell)) .collect::>>()?; + let has_async_method = methods.iter().any(|m| m.is_async); + let impl_attributes = has_async_method.then(|| quote! { #[::async_trait::async_trait] }); Ok(quote! { struct #vtable_type { @@ -86,6 +105,7 @@ pub(super) fn trait_impl( ::uniffi::deps::static_assertions::assert_impl_all!(#trait_impl_ident: ::core::marker::Send); + #impl_attributes impl #trait_ident for #trait_impl_ident { #(#trait_impl_methods)* } @@ -153,6 +173,7 @@ pub fn ffi_converter_callback_interface_impl( fn gen_method_impl(sig: &FnSignature, vtable_cell: &Ident) -> syn::Result { let FnSignature { ident, + is_async, return_ty, kind, receiver, @@ -190,15 +211,28 @@ fn gen_method_impl(sig: &FnSignature, vtable_cell: &Ident) -> syn::Result #return_ty { - let vtable = #vtable_cell.get(); - let mut uniffi_call_status = ::uniffi::RustCallStatus::new(); - let mut return_value: #lift_return::ReturnType = ::uniffi::FfiDefault::ffi_default(); - (vtable.#ident)(self.handle, #(#lower_exprs,)* &mut return_value, &mut uniffi_call_status); - #lift_return::lift_foreign_return(return_value, uniffi_call_status) - } - }) + if !is_async { + Ok(quote! { + fn #ident(#self_param, #(#params),*) -> #return_ty { + let vtable = #vtable_cell.get(); + let mut uniffi_call_status = ::uniffi::RustCallStatus::new(); + let mut uniffi_return_value: #lift_return::ReturnType = ::uniffi::FfiDefault::ffi_default(); + (vtable.#ident)(self.handle, #(#lower_exprs,)* &mut uniffi_return_value, &mut uniffi_call_status); + #lift_return::lift_foreign_return(uniffi_return_value, uniffi_call_status) + } + }) + } else { + Ok(quote! { + async fn #ident(#self_param, #(#params),*) -> #return_ty { + let vtable = #vtable_cell.get(); + ::uniffi::foreign_async_call::<_, #return_ty, crate::UniFfiTag>(move |uniffi_future_callback, uniffi_future_callback_data| { + let mut uniffi_foreign_future: ::uniffi::ForeignFuture = ::uniffi::FfiDefault::ffi_default(); + (vtable.#ident)(self.handle, #(#lower_exprs,)* uniffi_future_callback, uniffi_future_callback_data, &mut uniffi_foreign_future); + uniffi_foreign_future + }).await + } + }) + } } pub(super) fn metadata_items( diff --git a/uniffi_macros/src/export/scaffolding.rs b/uniffi_macros/src/export/scaffolding.rs index 9b14d49be9..b461e8d552 100644 --- a/uniffi_macros/src/export/scaffolding.rs +++ b/uniffi_macros/src/export/scaffolding.rs @@ -98,6 +98,9 @@ struct ScaffoldingBits { lift_closure: TokenStream, /// Expression to call the Rust function after a successful lift. rust_fn_call: TokenStream, + /// Convert the result of `rust_fn_call`, stored in a variable named `uniffi_result` into its final value. + /// This is used to do things like error conversion / Arc wrapping + convert_result: TokenStream, } impl ScaffoldingBits { @@ -106,10 +109,10 @@ impl ScaffoldingBits { let call_params = sig.rust_call_params(false); let rust_fn_call = quote! { #ident(#call_params) }; // UDL mode adds an extra conversion (#1749) - let rust_fn_call = if udl_mode && sig.looks_like_result { - quote! { #rust_fn_call.map_err(::std::convert::Into::into) } + let convert_result = if udl_mode && sig.looks_like_result { + quote! { uniffi_result.map_err(::std::convert::Into::into) } } else { - rust_fn_call + quote! { uniffi_result } }; Self { @@ -117,6 +120,7 @@ impl ScaffoldingBits { param_types: sig.scaffolding_param_types().collect(), lift_closure: sig.lift_closure(None), rust_fn_call, + convert_result, } } @@ -160,10 +164,10 @@ impl ScaffoldingBits { let call_params = sig.rust_call_params(true); let rust_fn_call = quote! { uniffi_args.0.#ident(#call_params) }; // UDL mode adds an extra conversion (#1749) - let rust_fn_call = if udl_mode && sig.looks_like_result { - quote! { #rust_fn_call.map_err(::std::convert::Into::into) } + let convert_result = if udl_mode && sig.looks_like_result { + quote! { uniffi_result .map_err(::std::convert::Into::into) } } else { - rust_fn_call + quote! { uniffi_result } }; Self { @@ -175,6 +179,7 @@ impl ScaffoldingBits { .collect(), lift_closure, rust_fn_call, + convert_result, } } @@ -183,13 +188,13 @@ impl ScaffoldingBits { let call_params = sig.rust_call_params(false); let rust_fn_call = quote! { #self_ident::#ident(#call_params) }; // UDL mode adds extra conversions (#1749) - let rust_fn_call = match (udl_mode, sig.looks_like_result) { + let convert_result = match (udl_mode, sig.looks_like_result) { // For UDL - (true, false) => quote! { ::std::sync::Arc::new(#rust_fn_call) }, + (true, false) => quote! { ::std::sync::Arc::new(uniffi_result) }, (true, true) => { - quote! { #rust_fn_call.map(::std::sync::Arc::new).map_err(::std::convert::Into::into) } + quote! { uniffi_result.map(::std::sync::Arc::new).map_err(::std::convert::Into::into) } } - (false, _) => rust_fn_call, + (false, _) => quote! { uniffi_result }, }; Self { @@ -197,6 +202,7 @@ impl ScaffoldingBits { param_types: sig.scaffolding_param_types().collect(), lift_closure: sig.lift_closure(None), rust_fn_call, + convert_result, } } } @@ -215,6 +221,7 @@ pub(super) fn gen_ffi_function( param_types, lift_closure, rust_fn_call, + convert_result, } = match &sig.kind { FnKind::Function => ScaffoldingBits::new_for_function(sig, udl_mode), FnKind::Method { self_ident } => { @@ -236,6 +243,7 @@ pub(super) fn gen_ffi_function( let ffi_ident = sig.scaffolding_fn_ident()?; let name = &sig.name; + let return_ty = &sig.return_ty; let return_impl = &sig.lower_return_impl(); Ok(if !sig.is_async { @@ -251,7 +259,10 @@ pub(super) fn gen_ffi_function( ::uniffi::rust_call(call_status, || { #return_impl::lower_return( match uniffi_lift_args() { - Ok(uniffi_args) => #rust_fn_call, + Ok(uniffi_args) => { + let uniffi_result = #rust_fn_call; + #convert_result + } Err((arg_name, anyhow_error)) => { #return_impl::handle_failed_lift(arg_name, anyhow_error) }, @@ -274,13 +285,16 @@ pub(super) fn gen_ffi_function( let uniffi_lift_args = #lift_closure; match uniffi_lift_args() { Ok(uniffi_args) => { - ::uniffi::rust_future_new( - async move { #future_expr.await }, + ::uniffi::rust_future_new::<_, #return_ty, _>( + async move { + let uniffi_result = #future_expr.await; + #convert_result + }, crate::UniFfiTag ) }, Err((arg_name, anyhow_error)) => { - ::uniffi::rust_future_new( + ::uniffi::rust_future_new::<_, #return_ty, _>( async move { #return_impl::handle_failed_lift(arg_name, anyhow_error) },