diff --git a/Cargo.toml b/Cargo.toml index 8bcab5b3..a3201fac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,16 +9,16 @@ resolver = "2" [workspace.dependencies] anyhow = "1" +askama = "0.13.1" camino = "1.1.6" -cargo_metadata = "0.15" +cargo_metadata = "0.19.2" clap = { version = "4.5.4", features = ["derive"] } extend = "1.2.0" heck = "0.5.0" paste = "1.0.14" pathdiff = { version = "0.2.1", features = ["camino"] } -rinja = "0.3.5" serde = { version = "1", features = ["derive"] } toml = "0.8.22" -uniffi = "=0.29.0" -uniffi_bindgen = "=0.29.0" -uniffi_meta = "=0.29.0" +uniffi = "=0.29.3" +uniffi_bindgen = "=0.29.3" +uniffi_meta = "=0.29.3" diff --git a/crates/ubrn_bindgen/Cargo.toml b/crates/ubrn_bindgen/Cargo.toml index 695796e8..cdaa03ad 100644 --- a/crates/ubrn_bindgen/Cargo.toml +++ b/crates/ubrn_bindgen/Cargo.toml @@ -10,13 +10,13 @@ wasm = ["quote", "prettyplease", "syn", "proc-macro2"] [dependencies] anyhow = { workspace = true } +askama = { workspace = true } camino = { workspace = true } cargo_metadata = { workspace = true } clap = { workspace = true } extend = { workspace = true } heck = { workspace = true } paste = { workspace = true } -rinja = { workspace = true } serde = { workspace = true } textwrap = "0.16.1" toml = "0.5" diff --git a/crates/ubrn_bindgen/rinja.toml b/crates/ubrn_bindgen/askama.toml similarity index 100% rename from crates/ubrn_bindgen/rinja.toml rename to crates/ubrn_bindgen/askama.toml diff --git a/crates/ubrn_bindgen/src/bindings/gen_cpp/filters.rs b/crates/ubrn_bindgen/src/bindings/gen_cpp/filters.rs index d04b889b..3ed95b3f 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_cpp/filters.rs +++ b/crates/ubrn_bindgen/src/bindings/gen_cpp/filters.rs @@ -8,21 +8,21 @@ use uniffi_bindgen::{interface::FfiType, ComponentInterface}; use crate::bindings::extensions::{ComponentInterfaceExt, FfiTypeExt}; -pub fn ffi_type_name_from_js(ffi_type: &FfiType) -> Result { +pub fn ffi_type_name_from_js(ffi_type: &FfiType) -> Result { Ok(match ffi_type { FfiType::MutReference(inner) | FfiType::Reference(inner) => ffi_type_name_from_js(inner)?, _ => ffi_type_name(ffi_type)?, }) } -pub fn cpp_namespace(ffi_type: &FfiType, ci: &ComponentInterface) -> Result { +pub fn cpp_namespace(ffi_type: &FfiType, ci: &ComponentInterface) -> Result { Ok(ffi_type.cpp_namespace(ci)) } pub fn bridging_namespace( ffi_type: &FfiType, ci: &ComponentInterface, -) -> Result { +) -> Result { // Bridging types are only in the uniffi_jsi namespace (`ci.cpp_namespace_includes()`) // or the generated namespace. Most of the time, `ffi_type.cpp_namespace()` does // the right thing, except in the case of Callbacks and Structs. @@ -36,17 +36,20 @@ pub fn bridging_namespace( }) } -pub fn bridging_class(ffi_type: &FfiType, ci: &ComponentInterface) -> Result { +pub fn bridging_class( + ffi_type: &FfiType, + ci: &ComponentInterface, +) -> Result { let ns = bridging_namespace(ffi_type, ci)?; let type_name = ffi_type_name_from_js(ffi_type)?; Ok(format!("{ns}::Bridging<{type_name}>")) } -pub fn ffi_type_name_to_rust(ffi_type: &FfiType) -> Result { +pub fn ffi_type_name_to_rust(ffi_type: &FfiType) -> Result { ffi_type_name(ffi_type) } -pub fn ffi_type_name(ffi_type: &FfiType) -> Result { +pub fn ffi_type_name(ffi_type: &FfiType) -> Result { Ok(match ffi_type { FfiType::UInt8 => "uint8_t".into(), FfiType::Int8 => "int8_t".into(), @@ -72,14 +75,14 @@ pub fn ffi_type_name(ffi_type: &FfiType) -> Result { }) } -pub fn var_name(nm: &str) -> Result { +pub fn var_name(nm: &str) -> Result { Ok(nm.to_lower_camel_case()) } -pub fn ffi_callback_name(nm: &str) -> Result { +pub fn ffi_callback_name(nm: &str) -> Result { Ok(format!("Uniffi{}", nm.to_upper_camel_case())) } -pub fn ffi_struct_name(nm: &str) -> Result { +pub fn ffi_struct_name(nm: &str) -> Result { Ok(format!("Uniffi{}", nm.to_upper_camel_case())) } diff --git a/crates/ubrn_bindgen/src/bindings/gen_cpp/mod.rs b/crates/ubrn_bindgen/src/bindings/gen_cpp/mod.rs index b6e5c103..570062ea 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_cpp/mod.rs +++ b/crates/ubrn_bindgen/src/bindings/gen_cpp/mod.rs @@ -10,7 +10,7 @@ mod util; use std::borrow::Borrow; use anyhow::Result; -use rinja::Template; +use askama::Template; use ubrn_common::CrateMetadata; use uniffi_bindgen::interface::{FfiDefinition, FfiType}; use uniffi_bindgen::ComponentInterface; diff --git a/crates/ubrn_bindgen/src/bindings/gen_typescript/config.rs b/crates/ubrn_bindgen/src/bindings/gen_typescript/config.rs index 9995f5ac..1489b4ee 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_typescript/config.rs +++ b/crates/ubrn_bindgen/src/bindings/gen_typescript/config.rs @@ -7,8 +7,6 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use uniffi_bindgen::backend::TemplateExpression; - #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub(crate) struct TsConfig { @@ -54,7 +52,16 @@ pub(crate) struct CustomTypeConfig { pub(crate) imports: Vec<(String, String)>, pub(crate) type_name: Option, #[serde(alias = "lift")] - pub(crate) into_custom: TemplateExpression, + pub(crate) into_custom: String, #[serde(alias = "lower")] - pub(crate) from_custom: TemplateExpression, + pub(crate) from_custom: String, +} + +impl CustomTypeConfig { + pub(crate) fn lift(&self, variable: &str) -> String { + self.into_custom.replace("{}", variable) + } + pub(crate) fn lower(&self, variable: &str) -> String { + self.from_custom.replace("{}", variable) + } } diff --git a/crates/ubrn_bindgen/src/bindings/gen_typescript/filters.rs b/crates/ubrn_bindgen/src/bindings/gen_typescript/filters.rs index 3e7addc4..042513f8 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_typescript/filters.rs +++ b/crates/ubrn_bindgen/src/bindings/gen_typescript/filters.rs @@ -17,7 +17,7 @@ use uniffi_bindgen::{ pub(super) fn type_name( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { let type_ = types.as_type(as_type); Ok(type_.as_codetype().type_label(types.ci)) } @@ -25,7 +25,7 @@ pub(super) fn type_name( pub(super) fn ffi_type_name_from_type( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { let type_ = types.as_type(as_type); let ffi_type = FfiType::from(type_); ffi_type_name(&ffi_type) @@ -34,7 +34,7 @@ pub(super) fn ffi_type_name_from_type( pub(super) fn decl_type_name( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { let type_ = types.as_type(as_type); Ok(type_.as_codetype().decl_type_label(types.ci)) } @@ -42,7 +42,7 @@ pub(super) fn decl_type_name( pub(super) fn ffi_converter_name( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { let type_ = types.as_type(as_type); Ok(type_.as_codetype().ffi_converter_name()) } @@ -50,7 +50,7 @@ pub(super) fn ffi_converter_name( pub(super) fn ffi_error_converter_name( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { // special handling for types used as errors. let type_ = types.as_type(as_type); let mut name = type_.as_codetype().ffi_converter_name(); @@ -73,7 +73,7 @@ pub(super) fn ffi_error_converter_name( pub(super) fn lower_error_fn( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { Ok(format!( "{ct}.lower.bind({ct})", ct = ffi_error_converter_name(as_type, types)? @@ -83,14 +83,17 @@ pub(super) fn lower_error_fn( pub(super) fn lift_error_fn( as_type: &impl AsType, types: &TypeRenderer, -) -> Result { +) -> Result { Ok(format!( "{ct}.lift.bind({ct})", ct = ffi_error_converter_name(as_type, types)? )) } -pub(super) fn lift_fn(as_type: &impl AsType, types: &TypeRenderer) -> Result { +pub(super) fn lift_fn( + as_type: &impl AsType, + types: &TypeRenderer, +) -> Result { Ok(format!( "{ct}.lift.bind({ct})", ct = ffi_converter_name(as_type, types)? @@ -101,7 +104,7 @@ pub fn render_literal( literal: &Literal, as_ct: &impl AsType, ci: &ComponentInterface, -) -> Result { +) -> Result { Ok(as_ct.as_codetype().literal(literal, ci)) } @@ -109,7 +112,7 @@ pub fn variant_discr_literal( e: &Enum, index: &usize, ci: &ComponentInterface, -) -> Result { +) -> Result { let literal = e.variant_discr(*index).expect("invalid index"); let ts_literal = Type::Int32.as_codetype().literal(&literal, ci); Ok(match literal { @@ -140,7 +143,7 @@ pub fn variant_discr_literal( }) } -pub fn ffi_type_name_for_cpp(type_: &FfiType, is_internal: &bool) -> Result { +pub fn ffi_type_name_for_cpp(type_: &FfiType, is_internal: &bool) -> Result { Ok(if *is_internal { CodeOracle.ffi_type_label_for_cpp(type_) } else { @@ -148,52 +151,52 @@ pub fn ffi_type_name_for_cpp(type_: &FfiType, is_internal: &bool) -> Result Result { +pub fn ffi_type_name(ffi_type: &FfiType) -> Result { Ok(CodeOracle.ffi_type_label(ffi_type)) } -pub fn ffi_default_value(type_: &FfiType) -> Result { +pub fn ffi_default_value(type_: &FfiType) -> Result { Ok(CodeOracle.ffi_default_value(type_)) } /// Get the idiomatic Typescript rendering of a function name. -pub fn class_name(nm: &str, ci: &ComponentInterface) -> Result { +pub fn class_name(nm: &str, ci: &ComponentInterface) -> Result { Ok(CodeOracle.class_name(ci, nm)) } /// Get the idiomatic Typescript rendering of a function name. -pub fn fn_name(nm: &str) -> Result { +pub fn fn_name(nm: &str) -> Result { Ok(CodeOracle.fn_name(nm)) } /// Get the idiomatic Typescript rendering of a variable name. -pub fn var_name(nm: &str) -> Result { +pub fn var_name(nm: &str) -> Result { Ok(CodeOracle.var_name(nm)) } /// Get the idiomatic Swift rendering of an arguments name. /// This is the same as the var name but quoting is not required. -pub fn arg_name(nm: &str) -> Result { +pub fn arg_name(nm: &str) -> Result { Ok(CodeOracle.var_name(nm)) } /// Get a String representing the name used for an individual enum variant. -pub fn variant_name(v: &Variant) -> Result { +pub fn variant_name(v: &Variant) -> Result { Ok(CodeOracle.enum_variant_name(v.name())) } /// Get the idiomatic Typescript rendering of an FFI callback function name -pub fn ffi_callback_name(nm: &str) -> Result { +pub fn ffi_callback_name(nm: &str) -> Result { Ok(CodeOracle.ffi_callback_name(nm)) } /// Get the idiomatic Typescript rendering of an FFI struct name -pub fn ffi_struct_name(nm: &str) -> Result { +pub fn ffi_struct_name(nm: &str) -> Result { Ok(CodeOracle.ffi_struct_name(nm)) } /// Get the idiomatic Typescript rendering of docstring -pub fn docstring(docstring: &str, spaces: &i32) -> Result { +pub fn docstring(docstring: &str, spaces: &i32) -> Result { let middle = textwrap::indent(&textwrap::dedent(docstring), " * "); let wrapped = format!("/**\n{middle}\n */"); diff --git a/crates/ubrn_bindgen/src/bindings/gen_typescript/mod.rs b/crates/ubrn_bindgen/src/bindings/gen_typescript/mod.rs index b01271e6..b4142881 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_typescript/mod.rs +++ b/crates/ubrn_bindgen/src/bindings/gen_typescript/mod.rs @@ -22,8 +22,8 @@ use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, HashSet}; use anyhow::{Context, Result}; +use askama::Template; use heck::ToUpperCamelCase; -use rinja::Template; use uniffi_bindgen::interface::{AsType, Callable, FfiDefinition, FfiType, Type, UniffiTrait}; use uniffi_bindgen::ComponentInterface; @@ -208,7 +208,7 @@ impl<'a> TypeRenderer<'a> { // import { foo } from "uniffi-bindgen-react-native/bar" // ``` // - // Returns an empty string so that it can be used inside an rinja `{{ }}` block. + // Returns an empty string so that it can be used inside an askama `{{ }}` block. fn import_infra(&self, what: &str, _from: &str) -> &str { self.add_import( Imported::JSType(what.to_owned()), diff --git a/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/CustomTypeTemplate.ts b/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/CustomTypeTemplate.ts index 971b03b9..95de7892 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/CustomTypeTemplate.ts +++ b/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/CustomTypeTemplate.ts @@ -37,22 +37,22 @@ const {{ ffi_converter_name }} = (() => { class FFIConverter implements FfiConverter { lift(value: FfiType): TsType { const intermediate = intermediateConverter.lift(value); - return {{ config.into_custom.render("intermediate") }}; + return {{ config.lift("intermediate") }}; } lower(value: TsType): FfiType { - const intermediate = {{ config.from_custom.render("value") }}; + const intermediate = {{ config.lower("value") }}; return intermediateConverter.lower(intermediate); } read(from: RustBuffer): TsType { const intermediate = intermediateConverter.read(from); - return {{ config.into_custom.render("intermediate") }}; + return {{ config.lift("intermediate") }}; } write(value: TsType, into: RustBuffer): void { - const intermediate = {{ config.from_custom.render("value") }}; + const intermediate = {{ config.lower("value") }}; intermediateConverter.write(intermediate, into); } allocationSize(value: TsType): number { - const intermediate = {{ config.from_custom.render("value") }}; + const intermediate = {{ config.lower("value") }}; return intermediateConverter.allocationSize(intermediate); } } diff --git a/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/InitializationTemplate.ts b/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/InitializationTemplate.ts index 9adc8f16..c314e5a0 100644 --- a/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/InitializationTemplate.ts +++ b/crates/ubrn_bindgen/src/bindings/gen_typescript/templates/InitializationTemplate.ts @@ -24,7 +24,7 @@ function uniffiEnsureInitialized() { } {%- endfor %} - {% for fn in self.initialization_fns() -%} - {{ fn }}(); + {% for func in self.initialization_fns() -%} + {{ func }}(); {% endfor -%} } diff --git a/crates/ubrn_cli/Cargo.toml b/crates/ubrn_cli/Cargo.toml index 2792b1c2..51db6970 100644 --- a/crates/ubrn_cli/Cargo.toml +++ b/crates/ubrn_cli/Cargo.toml @@ -19,7 +19,7 @@ wasm = [] [dependencies] anyhow = { workspace = true } -rinja = { workspace = true } +askama = { workspace = true } camino = { workspace = true } clap = { workspace = true } extend = { workspace = true } diff --git a/crates/ubrn_cli/rinja.toml b/crates/ubrn_cli/askama.toml similarity index 100% rename from crates/ubrn_cli/rinja.toml rename to crates/ubrn_cli/askama.toml diff --git a/crates/ubrn_cli/src/codegen/mod.rs b/crates/ubrn_cli/src/codegen/mod.rs index b7f6812d..4d7abd03 100644 --- a/crates/ubrn_cli/src/codegen/mod.rs +++ b/crates/ubrn_cli/src/codegen/mod.rs @@ -6,8 +6,8 @@ use std::{cell::OnceCell, collections::BTreeMap, rc::Rc}; use anyhow::Result; +use askama::DynTemplate; use camino::{Utf8Path, Utf8PathBuf}; -use rinja::DynTemplate; use ubrn_bindgen::ModuleMetadata; use ubrn_common::{mk_dir, CrateMetadata}; @@ -111,7 +111,7 @@ fn render_templates( macro_rules! templated_file { ($T:ty, $filename:literal) => { paste::paste! { - #[derive(rinja::Template)] + #[derive(askama::Template)] #[template(path = $filename, escape = "none")] pub(crate) struct $T { #[allow(dead_code)] diff --git a/crates/ubrn_common/src/rust_crate.rs b/crates/ubrn_common/src/rust_crate.rs index aff15f88..68a3b6a8 100644 --- a/crates/ubrn_common/src/rust_crate.rs +++ b/crates/ubrn_common/src/rust_crate.rs @@ -7,7 +7,7 @@ use std::{path::PathBuf, process::Command}; use anyhow::Result; use camino::{Utf8Path, Utf8PathBuf}; -use cargo_metadata::{Metadata, MetadataCommand}; +use cargo_metadata::{Metadata, MetadataCommand, TargetKind}; use crate::{path_or_shim, run_cmd_quietly}; @@ -187,7 +187,7 @@ fn guess_library_name(metadata: &Metadata, manifest_path: &Utf8Path) -> String { fn find_library_name(metadata: &Metadata, manifest_path: &Utf8Path) -> Option { // Get the library name - let lib = "lib".to_owned(); + let lib = TargetKind::Lib; metadata .packages .iter() 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..eb5f2d39 --- /dev/null +++ b/fixtures/wasm-arc-futures/README.md @@ -0,0 +1,7 @@ +# A test against the `wasm-unstable-single-threaded` feature + +This fixture tests several situations where a compile time error would ordinarily occur, but enabling the `wasm-unstable-single-threaded` feature of `uniffi` allows this. + +It also demonstrates several patterns to use when building for WASM32. + +In this fixture, the API between JSI and WASM is identical. If you want to vary APIs between build targets, you should use a `feature` instead. diff --git a/fixtures/wasm-arc-futures/src/lib.rs b/fixtures/wasm-arc-futures/src/lib.rs new file mode 100644 index 00000000..6614e6ac --- /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; + +#[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..4592b99b --- /dev/null +++ b/fixtures/wasm-arc-futures/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.typescript] +logLevel = "debug"