From e7b0b550b1c234b96c24441411315d13528209eb Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Fri, 15 Dec 2023 10:05:07 -0500 Subject: [PATCH] Foreign-implemented async trait methods (#2017) These methods input a completion function that they call when the async function is complete. They return a ForeignFuture struct, which represents the foreign task object and is used to drop/cancel futures. Like Rust, dropping and cancelling are coupled together into one operation. - Added `RustCallStatus` as an `FfiType` variant, since I wanted to use it in the `ForeignFutureResult` structs. In theory, we could define `RustCallStatus` a `FfiType::Struct`, but I didn't want to introduce that change in this PR. - Fixed the result mapping code to work with async functions. Before we were executing the mapping call, then awaiting the result, but we need to do that in the opposite order (`foo.into().await` vs `foo.await.into()`). Also, specify the return type generics for `rust_future_new` so that the Rust can figure out the into() generics. --- CHANGELOG.md | 2 +- Cargo.lock | 92 ++++++- Cargo.toml | 1 + docs/manual/src/futures.md | 22 ++ examples/README.md | 3 + examples/async-api-client/Cargo.toml | 23 ++ examples/async-api-client/build.rs | 7 + .../async-api-client/src/async-api-client.udl | 36 +++ examples/async-api-client/src/lib.rs | 142 +++++++++++ .../tests/bindings/test_async_api_client.kts | 24 ++ .../tests/bindings/test_async_api_client.py | 24 ++ .../bindings/test_async_api_client.swift | 32 +++ .../tests/test_generated_bindings.rs | 5 + fixtures/futures/Cargo.toml | 1 + fixtures/futures/src/lib.rs | 69 +++++ .../futures/tests/bindings/test_futures.kts | 72 ++++++ .../futures/tests/bindings/test_futures.py | 56 ++++ .../futures/tests/bindings/test_futures.swift | 91 +++++++ .../src/bindings/kotlin/gen_kotlin/mod.rs | 5 +- .../src/bindings/kotlin/templates/Async.kt | 76 ++++++ .../kotlin/templates/CallbackInterfaceImpl.kt | 51 +++- .../src/bindings/kotlin/templates/Helpers.kt | 9 + .../src/bindings/kotlin/templates/Types.kt | 4 + .../src/bindings/python/gen_python/mod.rs | 32 +++ .../src/bindings/python/templates/Async.py | 77 +++++- .../python/templates/CallbackInterfaceImpl.py | 36 +++ .../bindings/python/templates/HandleMap.py | 2 + .../src/bindings/python/templates/Helpers.py | 6 +- .../python/templates/RustBufferTemplate.py | 4 + .../src/bindings/python/templates/wrapper.py | 10 +- .../src/bindings/ruby/gen_ruby/mod.rs | 1 + .../src/bindings/swift/gen_swift/mod.rs | 42 ++- .../src/bindings/swift/templates/Async.swift | 70 +++++ .../templates/CallbackInterfaceImpl.swift | 72 +++++- .../bindings/swift/templates/HandleMap.swift | 6 + .../swift/templates/RustBufferTemplate.swift | 4 + uniffi_bindgen/src/interface/callbacks.rs | 86 ++++++- uniffi_bindgen/src/interface/ffi.rs | 1 + uniffi_bindgen/src/interface/mod.rs | 24 ++ uniffi_bindgen/src/interface/object.rs | 9 + uniffi_core/src/ffi/ffidefault.rs | 7 + uniffi_core/src/ffi/foreignfuture.rs | 241 ++++++++++++++++++ uniffi_core/src/ffi/mod.rs | 2 + uniffi_core/src/lib.rs | 1 + .../src/export/callback_interface.rs | 56 +++- uniffi_macros/src/export/scaffolding.rs | 42 ++- 46 files changed, 1615 insertions(+), 63 deletions(-) create mode 100644 examples/async-api-client/Cargo.toml create mode 100644 examples/async-api-client/build.rs create mode 100644 examples/async-api-client/src/async-api-client.udl create mode 100644 examples/async-api-client/src/lib.rs create mode 100644 examples/async-api-client/tests/bindings/test_async_api_client.kts create mode 100644 examples/async-api-client/tests/bindings/test_async_api_client.py create mode 100644 examples/async-api-client/tests/bindings/test_async_api_client.swift create mode 100644 examples/async-api-client/tests/test_generated_bindings.rs create mode 100644 uniffi_core/src/ffi/foreignfuture.rs 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) },