summaryrefslogtreecommitdiff
path: root/test_ffi
diff options
context:
space:
mode:
Diffstat (limited to 'test_ffi')
-rw-r--r--test_ffi/src/lib.rs14
-rw-r--r--test_ffi/tests/ffi_callback_errors.ts141
-rw-r--r--test_ffi/tests/integration_tests.rs41
3 files changed, 196 insertions, 0 deletions
diff --git a/test_ffi/src/lib.rs b/test_ffi/src/lib.rs
index b1a210417..c5c2c2d7a 100644
--- a/test_ffi/src/lib.rs
+++ b/test_ffi/src/lib.rs
@@ -259,6 +259,20 @@ pub extern "C" fn call_stored_function_thread_safe_and_log() {
}
#[no_mangle]
+pub extern "C" fn call_stored_function_2_thread_safe(arg: u8) {
+ std::thread::spawn(move || {
+ std::thread::sleep(std::time::Duration::from_millis(1500));
+ unsafe {
+ if STORED_FUNCTION_2.is_none() {
+ return;
+ }
+ println!("Calling");
+ STORED_FUNCTION_2.unwrap()(arg);
+ }
+ });
+}
+
+#[no_mangle]
pub extern "C" fn log_many_parameters(
a: u8,
b: u16,
diff --git a/test_ffi/tests/ffi_callback_errors.ts b/test_ffi/tests/ffi_callback_errors.ts
new file mode 100644
index 000000000..dda4de5fb
--- /dev/null
+++ b/test_ffi/tests/ffi_callback_errors.ts
@@ -0,0 +1,141 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+const targetDir = Deno.execPath().replace(/[^\/\\]+$/, "");
+const [libPrefix, libSuffix] = {
+ darwin: ["lib", "dylib"],
+ linux: ["lib", "so"],
+ windows: ["", "dll"],
+}[Deno.build.os];
+const libPath = `${targetDir}/${libPrefix}test_ffi.${libSuffix}`;
+
+const dylib = Deno.dlopen(
+ libPath,
+ {
+ store_function_2: {
+ parameters: ["function"],
+ result: "void",
+ },
+ call_stored_function_2: {
+ parameters: ["u8"],
+ result: "void",
+ },
+ call_stored_function_2_from_other_thread: {
+ name: "call_stored_function_2",
+ parameters: ["u8"],
+ result: "void",
+ nonblocking: true,
+ },
+ call_stored_function_2_thread_safe: {
+ parameters: ["u8"],
+ result: "void",
+ },
+ } as const,
+);
+
+globalThis.addEventListener("error", (data) => {
+ console.log("Unhandled error");
+ data.preventDefault();
+});
+globalThis.onerror = (data) => {
+ console.log("Unhandled error");
+ if (typeof data !== "string") {
+ data.preventDefault();
+ }
+};
+
+globalThis.addEventListener("unhandledrejection", (data) => {
+ console.log("Unhandled rejection");
+ data.preventDefault();
+});
+
+const timer = setTimeout(() => {
+ console.error(
+ "Test failed, final callback did not get picked up by Deno event loop",
+ );
+ Deno.exit(-1);
+}, 5_000);
+
+Deno.unrefTimer(timer);
+
+enum CallCase {
+ SyncSelf,
+ SyncFfi,
+ AsyncSelf,
+ AsyncSyncFfi,
+ AsyncFfi,
+}
+type U8CallCase = Deno.NativeU8Enum<CallCase>;
+
+const throwCb = (c: CallCase): number => {
+ console.log("CallCase:", CallCase[c]);
+ if (c === CallCase.AsyncFfi) {
+ cb.unref();
+ }
+ throw new Error("Error");
+};
+
+const THROW_CB_DEFINITION = {
+ parameters: ["u8" as U8CallCase],
+ result: "u8",
+} as const;
+
+const cb = new Deno.UnsafeCallback(THROW_CB_DEFINITION, throwCb);
+
+try {
+ const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, THROW_CB_DEFINITION);
+
+ fnPointer.call(CallCase.SyncSelf);
+} catch (_err) {
+ console.log(
+ "Throwing errors from an UnsafeCallback called from a synchronous UnsafeFnPointer works. Terribly excellent.",
+ );
+}
+
+dylib.symbols.store_function_2(cb.pointer);
+try {
+ dylib.symbols.call_stored_function_2(CallCase.SyncFfi);
+} catch (_err) {
+ console.log(
+ "Throwing errors from an UnsafeCallback called from a synchronous FFI symbol works. Terribly excellent.",
+ );
+}
+
+try {
+ const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
+ ...THROW_CB_DEFINITION,
+ nonblocking: true,
+ });
+ await fnPointer.call(CallCase.AsyncSelf);
+} catch (err) {
+ throw new Error(
+ "Nonblocking UnsafeFnPointer should not be threading through a JS error thrown on the other side of the call",
+ {
+ cause: err,
+ },
+ );
+}
+
+try {
+ await dylib.symbols.call_stored_function_2_from_other_thread(
+ CallCase.AsyncSyncFfi,
+ );
+} catch (err) {
+ throw new Error(
+ "Nonblocking symbol call should not be threading through a JS error thrown on the other side of the call",
+ {
+ cause: err,
+ },
+ );
+}
+try {
+ // Ref the callback to make sure we do not exit before the call is done.
+ cb.ref();
+ dylib.symbols.call_stored_function_2_thread_safe(CallCase.AsyncFfi);
+} catch (err) {
+ throw new Error(
+ "Blocking symbol call should not be travelling 1.5 seconds forward in time to figure out that it call will trigger a JS error to be thrown",
+ {
+ cause: err,
+ },
+ );
+}
diff --git a/test_ffi/tests/integration_tests.rs b/test_ffi/tests/integration_tests.rs
index 99707438e..446f6774d 100644
--- a/test_ffi/tests/integration_tests.rs
+++ b/test_ffi/tests/integration_tests.rs
@@ -237,3 +237,44 @@ fn event_loop_integration() {
assert_eq!(stdout, expected);
assert_eq!(stderr, "");
}
+
+#[test]
+fn ffi_callback_errors_test() {
+ build();
+
+ let output = deno_cmd()
+ .arg("run")
+ .arg("--allow-ffi")
+ .arg("--allow-read")
+ .arg("--unstable")
+ .arg("--quiet")
+ .arg("tests/ffi_callback_errors.ts")
+ .env("NO_COLOR", "1")
+ .output()
+ .unwrap();
+ let stdout = std::str::from_utf8(&output.stdout).unwrap();
+ let stderr = std::str::from_utf8(&output.stderr).unwrap();
+ if !output.status.success() {
+ println!("stdout {stdout}");
+ println!("stderr {stderr}");
+ }
+ println!("{:?}", output.status);
+ assert!(output.status.success());
+
+ let expected = "\
+ CallCase: SyncSelf\n\
+ Throwing errors from an UnsafeCallback called from a synchronous UnsafeFnPointer works. Terribly excellent.\n\
+ CallCase: SyncFfi\n\
+ 0\n\
+ Throwing errors from an UnsafeCallback called from a synchronous FFI symbol works. Terribly excellent.\n\
+ CallCase: AsyncSelf\n\
+ CallCase: AsyncSyncFfi\n\
+ 0\n\
+ Calling\n\
+ CallCase: AsyncFfi\n";
+ assert_eq!(stdout, expected);
+ assert_eq!(
+ stderr,
+ "Illegal unhandled exception in nonblocking callback.\n".repeat(3)
+ );
+}