diff --git a/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/macros.ts b/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/macros.ts index 7cd5ed72..16b0891f 100644 --- a/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/macros.ts +++ b/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/macros.ts @@ -39,7 +39,7 @@ {%- endmatch %} {%- if func.return_type().is_some() %} return - {%- endif %} nativeModule().{{ func.ffi_func().name() }}( + {%- endif %} {% call native_method_handle(func.ffi_func().name()) %}( {%- if func.takes_self() %}{{ obj_factory }}.clonePointer(this), {% endif %} {%- call arg_list_lowered(func) %} callStatus); @@ -100,7 +100,15 @@ {%- macro call_body(obj_factory, callable) %} {%- if callable.is_async() %} - return {# space #}{%- call call_async(obj_factory, callable) %}; + const __stack = uniffiIsDebug ? new Error().stack : undefined; + try { + return {# space #}{%- call call_async(obj_factory, callable) %}; + } catch (__error: any) { + if (uniffiIsDebug && __error instanceof Error) { + __error.stack = __stack; + } + throw __error; + } {%- else %} {%- match callable.return_type() -%} {%- when Some with (return_type) %} @@ -116,7 +124,7 @@ {{- self.import_infra("uniffiRustCallAsync", "async-rust-call") -}} await uniffiRustCallAsync( /*rustFutureFunc:*/ () => { - return nativeModule().{{ callable.ffi_func().name() }}( + return {% call native_method_handle(callable.ffi_func().name()) %}( {%- if callable.takes_self() %} {{ obj_factory }}.clonePointer(this){% if !callable.arguments().is_empty() %},{% endif %} {% endif %} @@ -125,10 +133,10 @@ {%- endfor %} ); }, - /*pollFunc:*/ nativeModule().{{ callable.ffi_rust_future_poll(ci) }}, - /*cancelFunc:*/ nativeModule().{{ callable.ffi_rust_future_cancel(ci) }}, - /*completeFunc:*/ nativeModule().{{ callable.ffi_rust_future_complete(ci) }}, - /*freeFunc:*/ nativeModule().{{ callable.ffi_rust_future_free(ci) }}, + /*pollFunc:*/ {% call native_method_handle_poll(callable.ffi_rust_future_poll(ci)) %}, + /*cancelFunc:*/ {% call native_method_handle_cancel(callable.ffi_rust_future_cancel(ci)) %}, + /*completeFunc:*/ {% call native_method_handle_complete(callable.ffi_rust_future_complete(ci)) %}, + /*freeFunc:*/ {% call native_method_handle_free(callable.ffi_rust_future_free(ci)) %}, {%- match callable.return_type() %} {%- when Some(return_type) %} /*liftFunc:*/ {{ return_type|lift_fn(self) }}, @@ -262,3 +270,122 @@ export type {{ type_name }} = InstanceType< typeof {{ decl_type_name }}[keyof Omit] >; {%- endmacro %} + +{# +// Verbose logging. +#} + +{# +// Most function calls use the `native_method_handle` macro, which if the loglevel in uniffi.toml +// is not set to `verbose`, then a call into Rust is: +// ```ts +// nativeModule().uniffi_uniffi_futures_fn_func_use_shared_resource +// ``` +// If set to verbose, then it is: +// ```ts +// (() => { +// console.debug("-- uniffi_uniffi_futures_fn_func_use_shared_resource"); +// return nativeModule().uniffi_uniffi_futures_fn_func_use_shared_resource; +// })() +// ``` +// When this IIFE is called, it is usually immediately before the function is being inovked: +// when not verbose: +// ```ts +// nativeModule().uniffi_uniffi_futures_fn_func_use_shared_resource(theArgument) +// ``` +// and when verbose: +// ```ts +// (() => { +// console.debug("-- uniffi_uniffi_futures_fn_func_use_shared_resource"); +// return nativeModule().uniffi_uniffi_futures_fn_func_use_shared_resource; +// })()(theArgument) +// ``` +#} + +{%- macro native_method_handle(method_name) %} +{%- if config.is_verbose() -%} +(() => { + {% call log_call(method_name) %} + return nativeModule().{{ method_name }}; +})() +{%- else -%} +nativeModule().{{ method_name }} +{%- endif %} +{%- endmacro %} + + +{# +// uniffiRustCallAsync calls take several function handles as arguments, then call +// into Rust in several stages. +// +// There now follow several macros which generate anonymous functions which log +// the method call, and then forward the arguments on to Rust. +// +// It is these function literals that are passed to uniffiRustCallAsync instead of the bare +// function handles. +#} + +{%- macro native_method_handle_poll(method_name) %} +{%- if config.is_verbose() -%} +{{- self.import_infra_type("UniffiHandle", "handle-map") }} +(rustFuture: bigint, cb: UniffiRustFutureContinuationCallback, handle: UniffiHandle): void => { + {% call log_message(" poll : ", method_name, "") %} + return nativeModule().{{ method_name }}(rustFuture, cb, handle); +} +{%- else -%} +nativeModule().{{ method_name }} +{%- endif %} +{%- endmacro %} + +{%- macro native_method_handle_cancel(method_name) %} +{%- if config.is_verbose() -%} +(rustFuture: bigint): void => { + {% call log_message(" cancel : ", method_name, "") %} + return nativeModule().{{ method_name }}(rustFuture); +} +{%- else -%} +nativeModule().{{ method_name }} +{%- endif %} +{%- endmacro %} + +{%- macro native_method_handle_complete(method_name) %} +{%- if config.is_verbose() -%} +{{- self.import_infra_type("UniffiRustCallStatus", "rust-call")}} +(rustFuture: bigint, status: UniffiRustCallStatus) => { + {% call log_message(" complete: ", method_name, "") %} + return nativeModule().{{ method_name }}(rustFuture, status); +} +{%- else -%} +nativeModule().{{ method_name }} +{%- endif %} +{%- endmacro %} + +{%- macro native_method_handle_free(method_name) %} +{%- if config.is_verbose() -%} +(rustFuture: bigint) => { + {% call log_message(" free : ", method_name, "") %} + return nativeModule().{{ method_name }}(rustFuture); +} +{%- else -%} +nativeModule().{{ method_name }} +{%- endif %} +{%- endmacro %} + +{%- macro log_call(method_name) %} +{%- call log_message("", method_name, "") %} +{%- endmacro %} + +{%- macro log_message(prefix, middle, suffix) %} +{%- if config.is_verbose() %} +{%- call set_up_console_log %} +console.debug(`-- {{ prefix }}{{ middle }}{{ suffix }}`); +{%- endif %} +{%- endmacro %} + +{%- macro set_up_console_log() %} +{%- match config.console_import %} +{%- when Some(module) %} +{{- self.import_custom("console", module) }} +{%- else %} +{%- endmatch %} +{%- endmacro %} diff --git a/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/wrapper.ts b/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/wrapper.ts index 724ad4ce..1055429c 100644 --- a/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/wrapper.ts +++ b/crates/ubrn_bindgen/src/bindings/react_native/gen_typescript/templates/wrapper.ts @@ -40,6 +40,8 @@ const { } = {{ entry.0.1 }}.converters; {%- endfor %} +const uniffiIsDebug = (process.env.uniffiIsDebug !== "production" || {{ config.is_debug() }}); + {%- call ts::docstring_value(ci.namespace_docstring(), 0) %} {%- import "macros.ts" as ts %} diff --git a/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs b/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs index 7db51788..a90b69a1 100644 --- a/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs +++ b/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs @@ -20,10 +20,41 @@ pub(crate) struct ReactNativeConfig { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub(crate) struct TsConfig { + #[serde(default)] + pub(crate) log_level: LogLevel, + #[serde(default)] + pub(crate) console_import: Option, #[serde(default)] pub(crate) custom_types: HashMap, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) enum LogLevel { + #[default] + None, + Debug, + Verbose, +} + +impl LogLevel { + fn is_verbose(&self) -> bool { + matches!(self, Self::Verbose) + } + fn is_debug(&self) -> bool { + matches!(self, Self::Debug | Self::Verbose) + } +} + +impl TsConfig { + pub(crate) fn is_verbose(&self) -> bool { + self.log_level.is_verbose() + } + pub(crate) fn is_debug(&self) -> bool { + self.log_level.is_debug() + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub(crate) struct CustomTypeConfig { diff --git a/fixtures/custom-types-example/uniffi.toml b/fixtures/custom-types-example/uniffi.toml index 4befa30b..a061bff2 100644 --- a/fixtures/custom-types-example/uniffi.toml +++ b/fixtures/custom-types-example/uniffi.toml @@ -1,3 +1,7 @@ +[bindings.typescript] +logLevel = "debug" +consoleImport = "@/hermes" + [bindings.typescript.customTypes.Url] # Modules that need to be imported imports = [ [ "URL", "@/hermes" ] ] diff --git a/fixtures/ext-types/subcrates/sub-lib/uniffi.toml b/fixtures/ext-types/subcrates/sub-lib/uniffi.toml index 94337b22..e90e2659 100644 --- a/fixtures/ext-types/subcrates/sub-lib/uniffi.toml +++ b/fixtures/ext-types/subcrates/sub-lib/uniffi.toml @@ -1,3 +1,3 @@ -[bindings.python.external_packages] -# This fixture does not create a Python package, so we want all modules to be top-level modules. -uniffi_one_ns = "" +[bindings.typescript] +logLevel = "debug" +consoleImport = "@/hermes" diff --git a/fixtures/ext-types/uniffi.toml b/fixtures/ext-types/uniffi.toml index 5b9c6d20..9d90db0c 100644 --- a/fixtures/ext-types/uniffi.toml +++ b/fixtures/ext-types/uniffi.toml @@ -1,3 +1,7 @@ +[bindings.typescript] +logLevel = "debug" +consoleImport = "@/hermes" + [bindings.python.external_packages] # This fixture does not create a Python package, so we want all modules to be top-level modules. custom_types = "" diff --git a/fixtures/futures/tests/bindings/test_futures.ts b/fixtures/futures/tests/bindings/test_futures.ts index 0b33d1b3..40bacf23 100644 --- a/fixtures/futures/tests/bindings/test_futures.ts +++ b/fixtures/futures/tests/bindings/test_futures.ts @@ -44,9 +44,7 @@ import "@/polyfills"; myModule.initialize(); function delayPromise(delayMs: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, delayMs); - }); + return new Promise((resolve) => setTimeout(resolve, delayMs)); } function cancellableDelayPromise( @@ -76,6 +74,18 @@ function checkRemainingFutures(t: Asserts) { } (async () => { + await asyncTest("Test delay promise", async (t) => { + console.info("Starting delay"); + await delayPromise(0); + const start = Date.now(); + await delayPromise(1000); + const actual = Date.now() - start; + console.info(`Ending delay, measured: ${actual} ms`); + t.assertInRange(actual, 900, 1100); + + t.end(); + }); + await asyncTest("alwaysReady", async (t) => { const result = await alwaysReady(); t.assertTrue(result); @@ -141,7 +151,7 @@ function checkRemainingFutures(t: Asserts) { let helloAlice = await t.asyncMeasure( async () => megaphone.sayAfter(500, "Alice"), 500, - 20, + 50, ); t.assertEqual("HELLO, ALICE!", helloAlice); checkRemainingFutures(t); @@ -589,4 +599,26 @@ function checkRemainingFutures(t: Asserts) { t.end(); }, ); + + await asyncTest("Test error stack traces", async (t) => { + t.assertEqual(42, await fallibleMe(false)); + await t.assertThrowsAsync( + (err) => { + if (!MyError.Foo.instanceOf(err)) { + return false; + } + if (!(err instanceof Error)) { + return false; + } + t.assertNotNull(err.stack); + t.assertTrue( + err.stack!.indexOf("fallibleMe") >= 0, + `STACK does not contain fallibleMe: ${err.stack!}`, + ); + return true; + }, + async () => await fallibleMe(true), + ); + t.end(); + }); })(); diff --git a/fixtures/futures/uniffi.toml b/fixtures/futures/uniffi.toml new file mode 100644 index 00000000..e90e2659 --- /dev/null +++ b/fixtures/futures/uniffi.toml @@ -0,0 +1,3 @@ +[bindings.typescript] +logLevel = "debug" +consoleImport = "@/hermes" diff --git a/typescript/src/async-rust-call.ts b/typescript/src/async-rust-call.ts index f37db17b..46bc15cd 100644 --- a/typescript/src/async-rust-call.ts +++ b/typescript/src/async-rust-call.ts @@ -115,7 +115,9 @@ const UNIFFI_RUST_FUTURE_RESOLVER_MAP = new UniffiHandleMap< // pollRust makes a new promise, stores the resolver in the resolver map, // then calls the pollFunc with the handle. -function pollRust(pollFunc: (handle: UniffiHandle) => void): Promise { +async function pollRust( + pollFunc: (handle: UniffiHandle) => void, +): Promise { return new Promise((resolve) => { const handle = UNIFFI_RUST_FUTURE_RESOLVER_MAP.insert(resolve); pollFunc(handle);