Skip to content

Commit 6064cab

Browse files
Daniel SalinasDaniel Salinas
Daniel Salinas
authored and
Daniel Salinas
committed
Add reproduction for uniffi arc objects
1 parent 1b2f0ff commit 6064cab

File tree

7 files changed

+373
-0
lines changed

7 files changed

+373
-0
lines changed

fixtures/objects/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "uniffi-fixtures-objects"
3+
version = "0.21.0"
4+
authors = ["zzorba"]
5+
edition = "2021"
6+
license = "MPL-2.0"
7+
publish = false
8+
9+
[lib]
10+
name = "objects"
11+
crate-type = ["lib", "cdylib"]
12+
13+
[dependencies]
14+
anyhow = "1"
15+
uniffi = { workspace = true, features = ["tokio", "cli"] }
16+
async-trait = "0.1"
17+
futures = "0.3"
18+
thiserror = "1.0"
19+
tokio = { version = "1.24.1", features = ["time", "sync"] }
20+
once_cell = "1.18.0"
21+
ubrn_testing = { path = "../../crates/ubrn_testing" }
22+
23+
[build-dependencies]
24+
uniffi = { workspace = true, features = ["build"] }
25+
26+
[dev-dependencies]
27+
uniffi = { workspace = true, features = ["bindgen-tests"] }

fixtures/objects/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# A basic test for uniffi components
2+
3+
This test covers async functions and methods. It also provides examples.
4+
5+
## Run the tests
6+
7+
Simply use `cargo`:
8+
9+
```sh
10+
$ cargo test
11+
```
12+
13+
It is possible to filter by test names, like `cargo test -- swift` to only run
14+
Swift's tests.
15+
16+
## Run the examples
17+
18+
At the time of writing, each `examples/*` directory has a `Makefile`. They are
19+
mostly designed for Unix-ish systems, sorry for that.
20+
21+
To run the examples, first `uniffi` must be compiled:
22+
23+
```sh
24+
$ cargo build --release -p uniffi`
25+
```
26+
27+
Then, each `Makefile` has 2 targets: `build` and `run`:
28+
29+
```sh
30+
$ # Build the examples.
31+
$ make build
32+
$
33+
$ # Run the example.
34+
$ make run
35+
```
36+
37+
One note for `examples/kotlin/`, some JAR files must be present, so please
38+
run `make install-jar` first: It will just download the appropriated JAR files
39+
directly inside the directory from Maven.

fixtures/objects/src/lib.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/
5+
*/
6+
7+
use std::future::Future;
8+
use std::pin::Pin;
9+
use std::sync::Arc;
10+
11+
#[cfg(not(target_arch = "wasm32"))]
12+
type EventHandlerFut = Pin<Box<dyn Future<Output = ()> + Send>>;
13+
#[cfg(target_arch = "wasm32")]
14+
type EventHandlerFut = Pin<Box<dyn Future<Output = ()>>>;
15+
16+
#[cfg(not(target_arch = "wasm32"))]
17+
type EventHandlerFn = dyn Fn(u64) -> EventHandlerFut + Send + Sync;
18+
#[cfg(target_arch = "wasm32")]
19+
type EventHandlerFn = dyn Fn(u64) -> EventHandlerFut;
20+
21+
22+
// Async callback interface implemented in foreign code
23+
#[derive(uniffi::Object)]
24+
pub struct SimpleObject {
25+
inner: Arc<InnerObject>
26+
}
27+
28+
#[derive(Debug, thiserror::Error, uniffi::Error)]
29+
#[uniffi(flat_error)]
30+
enum ClientError {
31+
#[error("Password is too weak")]
32+
Error(String),
33+
}
34+
35+
#[uniffi::export]
36+
impl SimpleObject {
37+
async fn builder_pattern(&self, key: String) -> Result<Arc<SimpleObject>, ClientError> {
38+
Ok(Arc::new(SimpleObject {
39+
inner: Arc::new(InnerObject {
40+
foo: Foo {
41+
key,
42+
value: 42,
43+
},
44+
callbacks: vec![],
45+
}),
46+
}))
47+
}
48+
}
49+
50+
pub struct InnerObject {
51+
pub foo: Foo,
52+
pub callbacks: Vec<Box<EventHandlerFn>>,
53+
}
54+
55+
pub struct Foo {
56+
pub key: String,
57+
pub value: u64,
58+
}
59+
60+
#[uniffi::export]
61+
async fn make_object() -> Arc<SimpleObject> {
62+
Arc::new(SimpleObject {
63+
inner: Arc::new(InnerObject {
64+
foo: Foo {
65+
key: "key".to_string(),
66+
value: 42,
67+
},
68+
callbacks: vec![],
69+
}),
70+
})
71+
}
72+
73+
uniffi::setup_scaffolding!();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
jsi
2+
wasm
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/
5+
*/
6+
7+
import myModule, {
8+
asStringUsingTrait,
9+
AsyncParser,
10+
cancelDelayUsingTrait,
11+
delayUsingTrait,
12+
ParserError,
13+
tryDelayUsingTrait,
14+
tryFromStringUsingTrait,
15+
} from "../../generated/async_callbacks";
16+
import { asyncTest, Asserts, test } from "@/asserts";
17+
import {
18+
uniffiRustFutureHandleCount,
19+
uniffiForeignFutureHandleCount,
20+
} from "uniffi-bindgen-react-native";
21+
import "@/polyfills";
22+
23+
// Initialize the callbacks for the module.
24+
// This will be hidden in the installation process.
25+
myModule.initialize();
26+
27+
function delayPromise(delayMs: number): Promise<void> {
28+
return new Promise((resolve) => setTimeout(resolve, delayMs));
29+
}
30+
31+
function cancellableDelayPromise(
32+
delayMs: number,
33+
abortSignal: AbortSignal,
34+
): Promise<void> {
35+
return new Promise((resolve, reject) => {
36+
const timer = setTimeout(resolve, delayMs);
37+
abortSignal.addEventListener("abort", () => {
38+
clearTimeout(timer);
39+
reject(abortSignal.reason);
40+
});
41+
});
42+
}
43+
44+
function checkRemainingFutures(t: Asserts) {
45+
t.assertEqual(
46+
0,
47+
uniffiRustFutureHandleCount(),
48+
"Number of remaining futures should be zero",
49+
);
50+
t.assertEqual(
51+
0,
52+
uniffiForeignFutureHandleCount(),
53+
"Number of remaining foreign futures should be zero",
54+
);
55+
}
56+
57+
(async () => {
58+
// AsyncParser.
59+
class TsAsyncParser implements AsyncParser {
60+
constructor(public completedDelays: number = 0) {}
61+
async asString(delayMs: number, value: number): Promise<string> {
62+
await this.doDelay(delayMs);
63+
return value.toString();
64+
}
65+
async tryFromString(delayMs: number, value: string): Promise<number> {
66+
if (value == "force-panic") {
67+
throw new Error("force-panic");
68+
}
69+
if (value == "force-unexpected-exception") {
70+
throw new ParserError.UnexpectedError();
71+
}
72+
const v = this.parseInt(value);
73+
await this.doDelay(delayMs);
74+
return v;
75+
}
76+
async delay(delayMs: number): Promise<void> {
77+
await this.doDelay(delayMs);
78+
}
79+
async tryDelay(delayMs: string): Promise<void> {
80+
await this.doDelay(this.parseInt(delayMs));
81+
}
82+
83+
toString(): string {
84+
return "TsAsyncParser";
85+
}
86+
87+
private async doDelay(ms: number): Promise<void> {
88+
await delayPromise(ms);
89+
this.completedDelays += 1;
90+
}
91+
92+
private parseInt(value: string): number {
93+
const num = Number.parseInt(value);
94+
if (Number.isNaN(num)) {
95+
throw new ParserError.NotAnInt();
96+
}
97+
return num;
98+
}
99+
}
100+
101+
await asyncTest("Async callbacks", async (t) => {
102+
const traitObj = new TsAsyncParser();
103+
104+
const result = await asStringUsingTrait(traitObj, 1, 42);
105+
t.assertEqual(result, "42");
106+
107+
const result2 = await tryFromStringUsingTrait(traitObj, 1, "42");
108+
t.assertEqual(result2, 42);
109+
110+
await delayUsingTrait(traitObj, 1);
111+
await tryDelayUsingTrait(traitObj, "1");
112+
checkRemainingFutures(t);
113+
t.end();
114+
});
115+
116+
await asyncTest("Async callbacks with errors", async (t) => {
117+
const traitObj = new TsAsyncParser();
118+
119+
try {
120+
await tryFromStringUsingTrait(traitObj, 1, "force-panic");
121+
t.fail("No error detected");
122+
} catch (e: any) {
123+
// OK
124+
t.assertTrue(ParserError.UnexpectedError.instanceOf(e));
125+
}
126+
127+
try {
128+
await tryFromStringUsingTrait(traitObj, 1, "fourty-two");
129+
t.fail("No error detected");
130+
} catch (e: any) {
131+
t.assertTrue(ParserError.NotAnInt.instanceOf(e));
132+
}
133+
134+
await t.assertThrowsAsync(
135+
ParserError.NotAnInt.instanceOf,
136+
async () => await tryFromStringUsingTrait(traitObj, 1, "fourty-two"),
137+
);
138+
await t.assertThrowsAsync(
139+
ParserError.UnexpectedError.instanceOf,
140+
async () =>
141+
await tryFromStringUsingTrait(
142+
traitObj,
143+
1,
144+
"force-unexpected-exception",
145+
),
146+
);
147+
148+
try {
149+
await tryDelayUsingTrait(traitObj, "one");
150+
t.fail("Expected previous statement to throw");
151+
} catch (e: any) {
152+
// Expected
153+
}
154+
checkRemainingFutures(t);
155+
t.end();
156+
});
157+
158+
class CancellableTsAsyncParser extends TsAsyncParser {
159+
/**
160+
* Each async callback method has an additional optional argument
161+
* `asyncOptions_`. This contains an `AbortSignal`.
162+
*
163+
* If the Rust task is cancelled, then this abort signal is
164+
* told, which can be used to co-operatively cancel the
165+
* async callback.
166+
*
167+
* @param delayMs
168+
* @param asyncOptions_
169+
*/
170+
async delay(
171+
delayMs: number,
172+
asyncOptions_?: { signal: AbortSignal },
173+
): Promise<void> {
174+
await this.doCancellableDelay(delayMs, asyncOptions_?.signal);
175+
}
176+
177+
private async doCancellableDelay(
178+
ms: number,
179+
signal?: AbortSignal,
180+
): Promise<void> {
181+
if (signal) {
182+
await cancellableDelayPromise(ms, signal);
183+
} else {
184+
await delayPromise(ms);
185+
}
186+
this.completedDelays += 1;
187+
}
188+
}
189+
190+
/**
191+
* Rust supports task cancellation, but it's not automatic. It is rather like
192+
* Javascript's.
193+
*
194+
* In Javascript, an `AbortController` is used to make an `AbortSignal`.
195+
*
196+
* The task itself periodically checks the `AbortSignal` (or listens for an `abort` event),
197+
* then takes abortive actions. This usually happens when the `AbortController.abort` method
198+
* is called.
199+
*
200+
* In Rust, an `AbortHandle` is analagous to the `AbortController`.
201+
*
202+
* This test checks if that signal is being triggered by a Rust.
203+
*/
204+
await asyncTest("cancellation of async JS callbacks", async (t) => {
205+
const traitObj = new CancellableTsAsyncParser();
206+
207+
// #JS_TASK_CANCELLATION
208+
const completedDelaysBefore = traitObj.completedDelays;
209+
// This method calls into the async callback to sleep (in Javascript) for 100 seconds.
210+
// On a different thread, in Rust, it cancels the task. This sets the `AbortSignal` passed to the
211+
// callback function.
212+
await cancelDelayUsingTrait(traitObj, 10000);
213+
// If the task was cancelled, then completedDelays won't have increased.
214+
t.assertEqual(
215+
traitObj.completedDelays,
216+
completedDelaysBefore,
217+
"Delay should have been cancelled",
218+
);
219+
220+
// Test that all handles here cleaned up
221+
checkRemainingFutures(t);
222+
t.end();
223+
});
224+
})();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at http://mozilla.org/MPL/2.0/
5+
*/

fixtures/objects/uniffi.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[bindings.typescript]
2+
logLevel = "debug"
3+
consoleImport = "@/hermes"

0 commit comments

Comments
 (0)