diff --git a/Cargo.toml b/Cargo.toml index 730d8ba8..5f071100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ paste = "1.0.14" pathdiff = { version = "0.2.1", features = ["camino"] } rinja = "0.3.5" serde = { version = "1", features = ["derive"] } -uniffi = "=0.29.0" +uniffi = { git = "https://github.com/jhugman/uniffi-rs", branch = "jhugman/2511-relax-send-sync-cbi" } +# uniffi = "=0.29.0" uniffi_bindgen = "=0.29.0" uniffi_meta = "=0.29.0" diff --git a/fixtures/error-types/Cargo.toml b/fixtures/error-types/Cargo.toml index 61e742a6..5b7611a4 100644 --- a/fixtures/error-types/Cargo.toml +++ b/fixtures/error-types/Cargo.toml @@ -10,12 +10,12 @@ crate-type = ["lib", "cdylib"] name = "uniffi_error_types" [dependencies] -uniffi = { workspace = true } +uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] } anyhow = "1" thiserror = "1.0" [build-dependencies] -uniffi = { workspace = true, features = ["build"] } +uniffi = { workspace = true, features = ["build", "wasm-unstable-single-threaded"] } [dev-dependencies] -uniffi = { workspace = true, features = ["bindgen-tests"] } +uniffi = { workspace = true, features = ["bindgen-tests", "wasm-unstable-single-threaded"] } diff --git a/fixtures/wasm-arc-futures/Cargo.toml b/fixtures/wasm-arc-futures/Cargo.toml new file mode 100644 index 00000000..c645cec3 --- /dev/null +++ b/fixtures/wasm-arc-futures/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "uniffi-fixtures-wasm-arc-futures" +version = "0.21.0" +authors = ["zzorba"] +edition = "2021" +license = "MPL-2.0" +publish = false + +[lib] +name = "wasm_arc_futures" +crate-type = ["lib", "cdylib"] + +[dependencies] +uniffi = { workspace = true, features = ["cli", "wasm-unstable-single-threaded"] } +async-trait = "0.1" +ubrn_testing = { path = "../../crates/ubrn_testing" } + +[build-dependencies] +uniffi = { workspace = true, features = ["build", "wasm-unstable-single-threaded"] } + +[dev-dependencies] +uniffi = { workspace = true, features = ["bindgen-tests", "wasm-unstable-single-threaded"] } diff --git a/fixtures/wasm-arc-futures/README.md b/fixtures/wasm-arc-futures/README.md new file mode 100644 index 00000000..9eb99058 --- /dev/null +++ b/fixtures/wasm-arc-futures/README.md @@ -0,0 +1,39 @@ +# A basic test for uniffi components + +This test covers async functions and methods. It also provides examples. + +## Run the tests + +Simply use `cargo`: + +```sh +$ cargo test +``` + +It is possible to filter by test names, like `cargo test -- swift` to only run +Swift's tests. + +## Run the examples + +At the time of writing, each `examples/*` directory has a `Makefile`. They are +mostly designed for Unix-ish systems, sorry for that. + +To run the examples, first `uniffi` must be compiled: + +```sh +$ cargo build --release -p uniffi` +``` + +Then, each `Makefile` has 2 targets: `build` and `run`: + +```sh +$ # Build the examples. +$ make build +$ +$ # Run the example. +$ make run +``` + +One note for `examples/kotlin/`, some JAR files must be present, so please +run `make install-jar` first: It will just download the appropriated JAR files +directly inside the directory from Maven. \ No newline at end of file diff --git a/fixtures/wasm-arc-futures/src/lib.rs b/fixtures/wasm-arc-futures/src/lib.rs new file mode 100644 index 00000000..3288136b --- /dev/null +++ b/fixtures/wasm-arc-futures/src/lib.rs @@ -0,0 +1,187 @@ +/* + * 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::fmt; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use ubrn_testing::timer::{TimerFuture, TimerService}; + +#[cfg(not(target_arch = "wasm32"))] +type EventHandlerFut = Pin + Send>>; +#[cfg(target_arch = "wasm32")] +type EventHandlerFut = Pin>>; + +#[cfg(not(target_arch = "wasm32"))] +type EventHandlerFn = dyn Fn(String, String) -> EventHandlerFut + Send + Sync; +#[cfg(target_arch = "wasm32")] +type EventHandlerFn = dyn Fn(String, String) -> EventHandlerFut; + +#[derive(uniffi::Object)] +pub struct SimpleObject { + inner: Mutex, + callbacks: Vec>, +} + +impl fmt::Debug for SimpleObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SimpleObject") + } +} + +impl fmt::Display for SimpleObject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl SimpleObject { + #[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] + fn new_with_callback(cb: Box) -> Arc { + Arc::new(SimpleObject { + inner: Mutex::new("key".to_string()), + callbacks: vec![cb], + }) + } +} + +#[uniffi::export] +impl SimpleObject { + pub async fn update(self: Arc, updated: String) { + let old = { + let mut data = self.inner.lock().unwrap(); + let old = data.clone(); + *data = updated.clone(); + old + }; + for callback in self.callbacks.iter() { + callback(old.clone(), updated.clone()).await; + } + } +} + +pub async fn wait(_old: String, _new: String) { + TimerFuture::sleep(Duration::from_millis(200)).await; +} + +fn from_static() -> Box { + Box::new(|old, new| Box::pin(wait(old, new))) +} + +// Make an object, with no callbacks. +// This relies on a timer, which is implemented for wasm using gloo. +// This is not Send, so EventHandlerFn and EventHandlerFut are different +// for wasm. +#[uniffi::export] +async fn make_object() -> Arc { + SimpleObject::new_with_callback(from_static()) +} + +#[uniffi::export] +async fn throw_object() -> Result<(), Arc> { + let obj = make_object().await; + Err(obj) +} + +// Simple callback interface object, with a synchronous method. +// The foreign trait isn't asynchronous, so we shouldn't be seeing +// any problem here. +#[uniffi::export(with_foreign)] +pub trait SimpleCallback: Sync + Send { + fn on_update(&self, previous: String, current: String); +} + +#[uniffi::export] +async fn simple_callback(callback: Arc) -> Arc { + callback +} + +fn from_simple_callback(callback: Arc) -> Box { + Box::new(move |old: String, new: String| { + let callback = callback.clone(); + Box::pin(async move { + callback.on_update(old, new); + }) + }) +} + +#[uniffi::export] +async fn make_object_with_callback(callback: Arc) -> Arc { + SimpleObject::new_with_callback(from_simple_callback(callback)) +} + +// An async callback interface; the async foreign trait will be +// a Send and Sync, so this shouldn't be testing anything new. +#[cfg(target_arch = "wasm32")] +#[uniffi::export(with_foreign)] +#[async_trait::async_trait(?Send)] +pub trait AsyncCallback { + async fn on_update(&self, previous: String, current: String); +} + +#[cfg(not(target_arch = "wasm32"))] +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait AsyncCallback: Send + Sync { + async fn on_update(&self, previous: String, current: String); +} + +#[uniffi::export] +async fn async_callback(callback: Arc) -> Arc { + callback +} + +fn from_async_callback(callback: Arc) -> Box { + Box::new(move |old: String, new: String| { + let callback = callback.clone(); + Box::pin(async move { + // Look, there's an .await here. + callback.on_update(old, new).await; + }) + }) +} + +#[uniffi::export] +async fn make_object_with_async_callback(callback: Arc) -> Arc { + SimpleObject::new_with_callback(from_async_callback(callback)) +} + +// Rust only trait +#[cfg(not(target_arch = "wasm32"))] +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait RustCallback: Sync + Send { + async fn on_update(&self, previous: String, current: String) -> String; +} + +#[cfg(target_arch = "wasm32")] +#[uniffi::export(with_foreign)] +#[async_trait::async_trait(?Send)] +pub trait RustCallback { + async fn on_update(&self, previous: String, current: String) -> String; +} + +struct NoopRustCallback; + +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl RustCallback for NoopRustCallback { + async fn on_update(&self, previous: String, current: String) -> String { + use std::time::Duration; + use ubrn_testing::timer::{TimerFuture, TimerService}; + TimerFuture::sleep(Duration::from_millis(200)).await; + format!("{previous} -> {current}") + } +} + +#[uniffi::export] +async fn rust_callback() -> Arc { + Arc::new(NoopRustCallback) +} + +uniffi::setup_scaffolding!(); diff --git a/fixtures/wasm-arc-futures/tests/bindings/.supported-flavors.txt b/fixtures/wasm-arc-futures/tests/bindings/.supported-flavors.txt new file mode 100644 index 00000000..ebbecdc9 --- /dev/null +++ b/fixtures/wasm-arc-futures/tests/bindings/.supported-flavors.txt @@ -0,0 +1,2 @@ +jsi +wasm diff --git a/fixtures/wasm-arc-futures/tests/bindings/test_wasm_arc_futures.ts b/fixtures/wasm-arc-futures/tests/bindings/test_wasm_arc_futures.ts new file mode 100644 index 00000000..adbba29d --- /dev/null +++ b/fixtures/wasm-arc-futures/tests/bindings/test_wasm_arc_futures.ts @@ -0,0 +1,156 @@ +/* + * 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 myModule, { + asyncCallback, + AsyncCallback, + makeObject, + makeObjectWithAsyncCallback, + makeObjectWithCallback, + rustCallback, + simpleCallback, + SimpleCallback, + SimpleObject, + throwObject, +} from "../../generated/wasm_arc_futures"; +import { asyncTest, Asserts, test } from "@/asserts"; +import { + uniffiRustFutureHandleCount, + uniffiForeignFutureHandleCount, + UniffiThrownObject, +} from "uniffi-bindgen-react-native"; +import "@/polyfills"; + +// Initialize the callbacks for the module. +// This will be hidden in the installation process. +myModule.initialize(); + +function checkRemainingFutures(t: Asserts) { + t.assertEqual( + 0, + uniffiRustFutureHandleCount(), + "Number of remaining futures should be zero", + ); + t.assertEqual( + 0, + uniffiForeignFutureHandleCount(), + "Number of remaining foreign futures should be zero", + ); +} + +(async () => { + await asyncTest( + "Empty object; if this compiles, the test has passed", + async (t) => { + const obj = await makeObject(); + t.assertNotNull(obj); + await obj.update("alice"); + // 200 ms later. + checkRemainingFutures(t); + t.end(); + }, + ); + + await asyncTest("Updater actually calls the update callback", async (t) => { + let previousValue: string | undefined; + let updated = 0; + class Updater implements SimpleCallback { + onUpdate(old: string, new_: string): void { + previousValue = old; + updated++; + } + reset(): void { + previousValue = undefined; + updated = 0; + } + } + + const cbJs = new Updater(); + const cbRs = await simpleCallback(cbJs); + cbRs.onUpdate("old", "new"); + t.assertEqual(previousValue, "old"); + t.assertEqual(updated, 1); + cbJs.reset(); + + const obj = await makeObjectWithCallback(cbJs); + t.assertNotNull(obj); + + await obj.update("alice"); + t.assertEqual(previousValue, "key"); + t.assertEqual(updated, 1); + + await obj.update("bob"); + t.assertEqual(previousValue, "alice"); + t.assertEqual(updated, 2); + + checkRemainingFutures(t); + t.end(); + }); + + await asyncTest("Updater actually calls the async callback", async (t) => { + let updated = 0; + let previousValue: string | undefined; + class Updater implements AsyncCallback { + async onUpdate(old: string, new_: string): Promise { + previousValue = old; + updated++; + } + reset(): void { + previousValue = undefined; + updated = 0; + } + } + + const cbJs = new Updater(); + const cbRs = await asyncCallback(cbJs); + await cbRs.onUpdate("old", "new"); + t.assertEqual(previousValue, "old"); + t.assertEqual(updated, 1); + cbJs.reset(); + + const obj = await makeObjectWithAsyncCallback(cbJs); + t.assertNotNull(obj); + + await obj.update("alice"); + t.assertEqual(previousValue, "key"); + t.assertEqual(updated, 1); + + await obj.update("bob"); + t.assertEqual(previousValue, "alice"); + t.assertEqual(updated, 2); + + checkRemainingFutures(t); + t.end(); + }); + + await asyncTest("Object as error", async (t) => { + await t.assertThrowsAsync((e: any) => { + if (!UniffiThrownObject.instanceOf(e)) { + return false; + } + if (!SimpleObject.hasInner(e)) { + return false; + } + let obj = e.inner; + t.assertNotNull(obj); + t.assertTrue(SimpleObject.instanceOf(obj)); + return true; + }, throwObject); + checkRemainingFutures(t); + t.end(); + }); + + await asyncTest("Async callbacks", async (t) => { + const cb = await rustCallback(); + const r1 = await cb.onUpdate("old", "new"); + t.assertEqual(r1, "old -> new"); + + const r2 = await cb.onUpdate("OLD", "NEW"); + t.assertEqual(r2, "OLD -> NEW"); + checkRemainingFutures(t); + t.end(); + }); +})(); diff --git a/fixtures/wasm-arc-futures/tests/test_generated_bindings.rs b/fixtures/wasm-arc-futures/tests/test_generated_bindings.rs new file mode 100644 index 00000000..24ffd28d --- /dev/null +++ b/fixtures/wasm-arc-futures/tests/test_generated_bindings.rs @@ -0,0 +1,5 @@ +/* + * 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/ + */ diff --git a/fixtures/wasm-arc-futures/uniffi.toml b/fixtures/wasm-arc-futures/uniffi.toml new file mode 100644 index 00000000..e90e2659 --- /dev/null +++ b/fixtures/wasm-arc-futures/uniffi.toml @@ -0,0 +1,3 @@ +[bindings.typescript] +logLevel = "debug" +consoleImport = "@/hermes"