diff options
Diffstat (limited to 'test_ffi')
-rw-r--r-- | test_ffi/src/lib.rs | 14 | ||||
-rw-r--r-- | test_ffi/tests/ffi_callback_errors.ts | 141 | ||||
-rw-r--r-- | test_ffi/tests/integration_tests.rs | 41 |
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) + ); +} |