Skip to content

Add reproduction for uniffi arc objects #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 3 additions & 3 deletions fixtures/error-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
22 changes: 22 additions & 0 deletions fixtures/wasm-arc-futures/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
39 changes: 39 additions & 0 deletions fixtures/wasm-arc-futures/README.md
Original file line number Diff line number Diff line change
@@ -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`
Copy link
Preview

Copilot AI May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There appears to be a stray backtick at the end of the cargo build command, which could cause confusion. Consider removing the extra backtick for clarity.

Suggested change
$ cargo build --release -p uniffi`
$ cargo build --release -p uniffi

Copilot uses AI. Check for mistakes.

```

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.
187 changes: 187 additions & 0 deletions fixtures/wasm-arc-futures/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn Future<Output = ()> + Send>>;
#[cfg(target_arch = "wasm32")]
type EventHandlerFut = Pin<Box<dyn Future<Output = ()>>>;

#[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<String>,
callbacks: Vec<Box<EventHandlerFn>>,
}

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<EventHandlerFn>) -> Arc<Self> {
Arc::new(SimpleObject {
inner: Mutex::new("key".to_string()),
callbacks: vec![cb],
})
}
}

#[uniffi::export]
impl SimpleObject {
pub async fn update(self: Arc<Self>, 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<EventHandlerFn> {
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> {
SimpleObject::new_with_callback(from_static())
}

#[uniffi::export]
async fn throw_object() -> Result<(), Arc<SimpleObject>> {
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<dyn SimpleCallback>) -> Arc<dyn SimpleCallback> {
callback
}

fn from_simple_callback(callback: Arc<dyn SimpleCallback>) -> Box<EventHandlerFn> {
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<dyn SimpleCallback>) -> Arc<SimpleObject> {
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<dyn AsyncCallback>) -> Arc<dyn AsyncCallback> {
callback
}

fn from_async_callback(callback: Arc<dyn AsyncCallback>) -> Box<EventHandlerFn> {
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<dyn AsyncCallback>) -> Arc<SimpleObject> {
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<dyn RustCallback> {
Arc::new(NoopRustCallback)
}

uniffi::setup_scaffolding!();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
jsi
wasm
Loading
Loading