diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-12 13:46:50 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-12 13:46:50 -0700 |
commit | f60720090c7bd8cdf91d7aebd0c42e01c86b3b83 (patch) | |
tree | 9becb7ff7e40d37769601fa049beccd101d58a98 /tests/ffi | |
parent | bd1358efab8ba7339a8e70034315fa7da840292e (diff) |
chore: move test_ffi and test_nap to tests/ [WIP] (#22394)
Moving some additional NAPI and. FFI tests out of the tree root.
Diffstat (limited to 'tests/ffi')
-rw-r--r-- | tests/ffi/Cargo.toml | 17 | ||||
-rw-r--r-- | tests/ffi/README.md | 1 | ||||
-rw-r--r-- | tests/ffi/src/lib.rs | 559 | ||||
-rw-r--r-- | tests/ffi/tests/bench.js | 687 | ||||
-rw-r--r-- | tests/ffi/tests/event_loop_integration.ts | 78 | ||||
-rw-r--r-- | tests/ffi/tests/ffi_callback_errors.ts | 141 | ||||
-rw-r--r-- | tests/ffi/tests/ffi_types.ts | 529 | ||||
-rw-r--r-- | tests/ffi/tests/integration_tests.rs | 302 | ||||
-rw-r--r-- | tests/ffi/tests/test.js | 802 | ||||
-rw-r--r-- | tests/ffi/tests/thread_safe_test.js | 105 | ||||
-rw-r--r-- | tests/ffi/tests/thread_safe_test_worker.js | 41 |
11 files changed, 3262 insertions, 0 deletions
diff --git a/tests/ffi/Cargo.toml b/tests/ffi/Cargo.toml new file mode 100644 index 000000000..a5d2883ef --- /dev/null +++ b/tests/ffi/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +[package] +name = "test_ffi" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +repository.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dev-dependencies] +pretty_assertions.workspace = true +test_util.workspace = true diff --git a/tests/ffi/README.md b/tests/ffi/README.md new file mode 100644 index 000000000..685385e4f --- /dev/null +++ b/tests/ffi/README.md @@ -0,0 +1 @@ +# `test_ffi` crate diff --git a/tests/ffi/src/lib.rs b/tests/ffi/src/lib.rs new file mode 100644 index 000000000..f6ee31eb8 --- /dev/null +++ b/tests/ffi/src/lib.rs @@ -0,0 +1,559 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +#![allow(clippy::undocumented_unsafe_blocks)] + +use std::os::raw::c_void; +use std::thread::sleep; +use std::time::Duration; + +static BUFFER: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + +#[no_mangle] +pub extern "C" fn print_something() { + println!("something"); +} + +/// # Safety +/// +/// The pointer to the buffer must be valid and initialized, and the length must +/// not be longer than the buffer's allocation. +#[no_mangle] +pub unsafe extern "C" fn print_buffer(ptr: *const u8, len: usize) { + let buf = std::slice::from_raw_parts(ptr, len); + println!("{buf:?}"); +} + +/// # Safety +/// +/// The pointer to the buffer must be valid and initialized, and the length must +/// not be longer than the buffer's allocation. +#[no_mangle] +pub unsafe extern "C" fn print_buffer2( + ptr1: *const u8, + len1: usize, + ptr2: *const u8, + len2: usize, +) { + let buf1 = std::slice::from_raw_parts(ptr1, len1); + let buf2 = std::slice::from_raw_parts(ptr2, len2); + println!("{buf1:?} {buf2:?}"); +} + +#[no_mangle] +pub extern "C" fn return_buffer() -> *const u8 { + BUFFER.as_ptr() +} + +#[no_mangle] +pub extern "C" fn is_null_ptr(ptr: *const u8) -> bool { + ptr.is_null() +} + +#[no_mangle] +pub extern "C" fn add_u32(a: u32, b: u32) -> u32 { + a + b +} + +#[no_mangle] +pub extern "C" fn add_i32(a: i32, b: i32) -> i32 { + a + b +} + +#[no_mangle] +pub extern "C" fn add_u64(a: u64, b: u64) -> u64 { + a + b +} + +#[no_mangle] +pub extern "C" fn add_i64(a: i64, b: i64) -> i64 { + a + b +} + +#[no_mangle] +pub extern "C" fn add_usize(a: usize, b: usize) -> usize { + a + b +} + +#[no_mangle] +pub extern "C" fn add_usize_fast(a: usize, b: usize) -> u32 { + (a + b) as u32 +} + +#[no_mangle] +pub extern "C" fn add_isize(a: isize, b: isize) -> isize { + a + b +} + +#[no_mangle] +pub extern "C" fn add_f32(a: f32, b: f32) -> f32 { + a + b +} + +#[no_mangle] +pub extern "C" fn add_f64(a: f64, b: f64) -> f64 { + a + b +} + +#[no_mangle] +pub extern "C" fn and(a: bool, b: bool) -> bool { + a && b +} + +#[no_mangle] +unsafe extern "C" fn hash(ptr: *const u8, length: u32) -> u32 { + let buf = std::slice::from_raw_parts(ptr, length as usize); + let mut hash: u32 = 0; + for byte in buf { + hash = hash.wrapping_mul(0x10001000).wrapping_add(*byte as u32); + } + hash +} + +#[no_mangle] +pub extern "C" fn sleep_blocking(ms: u64) { + let duration = Duration::from_millis(ms); + sleep(duration); +} + +/// # Safety +/// +/// The pointer to the buffer must be valid and initialized, and the length must +/// not be longer than the buffer's allocation. +#[no_mangle] +pub unsafe extern "C" fn fill_buffer(value: u8, buf: *mut u8, len: usize) { + let buf = std::slice::from_raw_parts_mut(buf, len); + for itm in buf.iter_mut() { + *itm = value; + } +} + +/// # Safety +/// +/// The pointer to the buffer must be valid and initialized, and the length must +/// not be longer than the buffer's allocation. +#[no_mangle] +pub unsafe extern "C" fn nonblocking_buffer(ptr: *const u8, len: usize) { + let buf = std::slice::from_raw_parts(ptr, len); + assert_eq!(buf, vec![1, 2, 3, 4, 5, 6, 7, 8]); +} + +#[no_mangle] +pub extern "C" fn get_add_u32_ptr() -> *const c_void { + add_u32 as *const c_void +} + +#[no_mangle] +pub extern "C" fn get_sleep_blocking_ptr() -> *const c_void { + sleep_blocking as *const c_void +} + +#[no_mangle] +pub extern "C" fn call_fn_ptr(func: Option<extern "C" fn()>) { + if func.is_none() { + return; + } + let func = func.unwrap(); + func(); +} + +#[no_mangle] +pub extern "C" fn call_fn_ptr_many_parameters( + func: Option< + extern "C" fn(u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, *const u8), + >, +) { + if func.is_none() { + return; + } + let func = func.unwrap(); + func(1, -1, 2, -2, 3, -3, 4, -4, 0.5, -0.5, BUFFER.as_ptr()); +} + +#[no_mangle] +pub extern "C" fn call_fn_ptr_return_u8(func: Option<extern "C" fn() -> u8>) { + if func.is_none() { + return; + } + let func = func.unwrap(); + println!("u8: {}", func()); +} + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn call_fn_ptr_return_buffer( + func: Option<extern "C" fn() -> *const u8>, +) { + if func.is_none() { + return; + } + let func = func.unwrap(); + let ptr = func(); + let buf = unsafe { std::slice::from_raw_parts(ptr, 8) }; + println!("buf: {buf:?}"); +} + +static mut STORED_FUNCTION: Option<extern "C" fn()> = None; +static mut STORED_FUNCTION_2: Option<extern "C" fn(u8) -> u8> = None; + +#[no_mangle] +pub extern "C" fn store_function(func: Option<extern "C" fn()>) { + unsafe { STORED_FUNCTION = func }; + if func.is_none() { + println!("STORED_FUNCTION cleared"); + } +} + +#[no_mangle] +pub extern "C" fn store_function_2(func: Option<extern "C" fn(u8) -> u8>) { + unsafe { STORED_FUNCTION_2 = func }; + if func.is_none() { + println!("STORED_FUNCTION_2 cleared"); + } +} + +#[no_mangle] +pub extern "C" fn call_stored_function() { + unsafe { + if STORED_FUNCTION.is_none() { + return; + } + STORED_FUNCTION.unwrap()(); + } +} + +#[no_mangle] +pub extern "C" fn call_stored_function_2(arg: u8) { + unsafe { + if STORED_FUNCTION_2.is_none() { + return; + } + println!("{}", STORED_FUNCTION_2.unwrap()(arg)); + } +} + +#[no_mangle] +pub extern "C" fn call_stored_function_thread_safe() { + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1500)); + unsafe { + if STORED_FUNCTION.is_none() { + return; + } + STORED_FUNCTION.unwrap()(); + } + }); +} + +#[no_mangle] +pub extern "C" fn call_stored_function_thread_safe_and_log() { + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1500)); + unsafe { + if STORED_FUNCTION.is_none() { + return; + } + STORED_FUNCTION.unwrap()(); + println!("STORED_FUNCTION called"); + } + }); +} + +#[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, + c: u32, + d: u64, + e: f64, + f: f32, + g: i64, + h: i32, + i: i16, + j: i8, + k: isize, + l: usize, + m: f64, + n: f32, + o: f64, + p: f32, + q: f64, + r: f32, + s: f64, +) { + println!("{a} {b} {c} {d} {e} {f} {g} {h} {i} {j} {k} {l} {m} {n} {o} {p} {q} {r} {s}"); +} + +#[no_mangle] +pub extern "C" fn cast_u8_u32(x: u8) -> u32 { + x as u32 +} + +#[no_mangle] +pub extern "C" fn cast_u32_u8(x: u32) -> u8 { + x as u8 +} + +#[no_mangle] +pub extern "C" fn add_many_u16( + a: u16, + b: u16, + c: u16, + d: u16, + e: u16, + f: u16, + g: u16, + h: u16, + i: u16, + j: u16, + k: u16, + l: u16, + m: u16, +) -> u16 { + a + b + c + d + e + f + g + h + i + j + k + l + m +} + +// FFI performance helper functions +#[no_mangle] +pub extern "C" fn nop() {} + +#[no_mangle] +pub extern "C" fn nop_bool(_a: bool) {} + +#[no_mangle] +pub extern "C" fn nop_u8(_a: u8) {} + +#[no_mangle] +pub extern "C" fn nop_i8(_a: i8) {} + +#[no_mangle] +pub extern "C" fn nop_u16(_a: u16) {} + +#[no_mangle] +pub extern "C" fn nop_i16(_a: i16) {} + +#[no_mangle] +pub extern "C" fn nop_u32(_a: u32) {} + +#[no_mangle] +pub extern "C" fn nop_i32(_a: i32) {} + +#[no_mangle] +pub extern "C" fn nop_u64(_a: u64) {} + +#[no_mangle] +pub extern "C" fn nop_i64(_a: i64) {} + +#[no_mangle] +pub extern "C" fn nop_usize(_a: usize) {} + +#[no_mangle] +pub extern "C" fn nop_isize(_a: isize) {} + +#[no_mangle] +pub extern "C" fn nop_f32(_a: f32) {} + +#[no_mangle] +pub extern "C" fn nop_f64(_a: f64) {} + +#[no_mangle] +pub extern "C" fn nop_buffer(_buffer: *mut [u8; 8]) {} + +#[no_mangle] +pub extern "C" fn return_bool() -> bool { + true +} + +#[no_mangle] +pub extern "C" fn return_u8() -> u8 { + 255 +} + +#[no_mangle] +pub extern "C" fn return_i8() -> i8 { + -128 +} + +#[no_mangle] +pub extern "C" fn return_u16() -> u16 { + 65535 +} + +#[no_mangle] +pub extern "C" fn return_i16() -> i16 { + -32768 +} + +#[no_mangle] +pub extern "C" fn return_u32() -> u32 { + 4294967295 +} + +#[no_mangle] +pub extern "C" fn return_i32() -> i32 { + -2147483648 +} + +#[no_mangle] +pub extern "C" fn return_u64() -> u64 { + 18446744073709551615 +} + +#[no_mangle] +pub extern "C" fn return_i64() -> i64 { + -9223372036854775808 +} + +#[no_mangle] +pub extern "C" fn return_usize() -> usize { + 18446744073709551615 +} + +#[no_mangle] +pub extern "C" fn return_isize() -> isize { + -9223372036854775808 +} + +#[no_mangle] +pub extern "C" fn return_f32() -> f32 { + #[allow(clippy::excessive_precision)] + 0.20298023223876953125 +} + +#[no_mangle] +pub extern "C" fn return_f64() -> f64 { + 1e-10 +} + +// Parameters iteration + +#[no_mangle] +pub extern "C" fn nop_many_parameters( + _: u8, + _: i8, + _: u16, + _: i16, + _: u32, + _: i32, + _: u64, + _: i64, + _: usize, + _: isize, + _: f32, + _: f64, + _: *mut [u8; 8], + _: u8, + _: i8, + _: u16, + _: i16, + _: u32, + _: i32, + _: u64, + _: i64, + _: usize, + _: isize, + _: f32, + _: f64, + _: *mut [u8; 8], +) { +} + +// Statics +#[no_mangle] +pub static static_u32: u32 = 42; + +#[no_mangle] +pub static static_i64: i64 = -1242464576485; + +#[repr(C)] +pub struct Structure { + _data: u32, +} + +#[no_mangle] +pub static mut static_ptr: Structure = Structure { _data: 42 }; + +static STRING: &str = "Hello, world!\0"; + +#[no_mangle] +extern "C" fn ffi_string() -> *const u8 { + STRING.as_ptr() +} + +/// Invalid UTF-8 characters, array of length 14 +#[no_mangle] +pub static static_char: [u8; 14] = [ + 0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, + 0x00, +]; + +#[derive(Debug)] +#[repr(C)] +pub struct Rect { + x: f64, + y: f64, + w: f64, + h: f64, +} + +#[no_mangle] +pub extern "C" fn make_rect(x: f64, y: f64, w: f64, h: f64) -> Rect { + Rect { x, y, w, h } +} + +#[no_mangle] +pub extern "C" fn print_rect(rect: Rect) { + println!("{rect:?}"); +} + +#[derive(Debug)] +#[repr(C)] +pub struct Mixed { + u8: u8, + f32: f32, + rect: Rect, + usize: usize, + array: [u32; 2], +} + +/// # Safety +/// +/// The array pointer to the buffer must be valid and initialized, and the length must +/// be 2. +#[no_mangle] +pub unsafe extern "C" fn create_mixed( + u8: u8, + f32: f32, + rect: Rect, + usize: usize, + array: *const [u32; 2], +) -> Mixed { + let array = *array + .as_ref() + .expect("Array parameter should contain value"); + Mixed { + u8, + f32, + rect, + usize, + array, + } +} + +#[no_mangle] +pub extern "C" fn print_mixed(mixed: Mixed) { + println!("{mixed:?}"); +} diff --git a/tests/ffi/tests/bench.js b/tests/ffi/tests/bench.js new file mode 100644 index 000000000..49884d32e --- /dev/null +++ b/tests/ffi/tests/bench.js @@ -0,0 +1,687 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +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, { + "nop": { parameters: [], result: "void" }, + "add_u32": { parameters: ["u32", "u32"], result: "u32" }, + "add_u64": { parameters: ["u64", "u64"], result: "u64" }, + "ffi_string": { parameters: [], result: "pointer" }, + "hash": { parameters: ["buffer", "u32"], result: "u32" }, + "nop_bool": { parameters: ["bool"], result: "void" }, + "nop_u8": { parameters: ["u8"], result: "void" }, + "nop_i8": { parameters: ["i8"], result: "void" }, + "nop_u16": { parameters: ["u16"], result: "void" }, + "nop_i16": { parameters: ["i16"], result: "void" }, + "nop_u32": { parameters: ["u32"], result: "void" }, + "nop_i32": { parameters: ["i32"], result: "void" }, + "nop_u64": { parameters: ["u64"], result: "void" }, + "nop_i64": { parameters: ["i64"], result: "void" }, + "nop_usize": { parameters: ["usize"], result: "void" }, + "nop_isize": { parameters: ["isize"], result: "void" }, + "nop_f32": { parameters: ["f32"], result: "void" }, + "nop_f64": { parameters: ["f64"], result: "void" }, + "nop_buffer": { parameters: ["buffer"], result: "void" }, + "return_bool": { parameters: [], result: "bool" }, + "return_u8": { parameters: [], result: "u8" }, + "return_i8": { parameters: [], result: "i8" }, + "return_u16": { parameters: [], result: "u16" }, + "return_i16": { parameters: [], result: "i16" }, + "return_u32": { parameters: [], result: "u32" }, + "return_i32": { parameters: [], result: "i32" }, + "return_u64": { parameters: [], result: "u64" }, + "return_i64": { parameters: [], result: "i64" }, + "return_usize": { parameters: [], result: "usize" }, + "return_isize": { parameters: [], result: "isize" }, + "return_f32": { parameters: [], result: "f32" }, + "return_f64": { parameters: [], result: "f64" }, + "return_buffer": { parameters: [], result: "buffer" }, + // Nonblocking calls + "nop_nonblocking": { name: "nop", parameters: [], result: "void" }, + "nop_bool_nonblocking": { + name: "nop_bool", + parameters: ["bool"], + result: "void", + }, + "nop_u8_nonblocking": { name: "nop_u8", parameters: ["u8"], result: "void" }, + "nop_i8_nonblocking": { name: "nop_i8", parameters: ["i8"], result: "void" }, + "nop_u16_nonblocking": { + name: "nop_u16", + parameters: ["u16"], + result: "void", + }, + "nop_i16_nonblocking": { + name: "nop_i16", + parameters: ["i16"], + result: "void", + }, + "nop_u32_nonblocking": { + name: "nop_u32", + parameters: ["u32"], + result: "void", + }, + "nop_i32_nonblocking": { + name: "nop_i32", + parameters: ["i32"], + result: "void", + }, + "nop_u64_nonblocking": { + name: "nop_u64", + parameters: ["u64"], + result: "void", + }, + "nop_i64_nonblocking": { + name: "nop_i64", + parameters: ["i64"], + result: "void", + }, + "nop_usize_nonblocking": { + name: "nop_usize", + parameters: ["usize"], + result: "void", + }, + "nop_isize_nonblocking": { + name: "nop_isize", + parameters: ["isize"], + result: "void", + }, + "nop_f32_nonblocking": { + name: "nop_f32", + parameters: ["f32"], + result: "void", + }, + "nop_f64_nonblocking": { + name: "nop_f64", + parameters: ["f64"], + result: "void", + }, + "nop_buffer_nonblocking": { + name: "nop_buffer", + parameters: ["buffer"], + result: "void", + }, + "return_bool_nonblocking": { + name: "return_bool", + parameters: [], + result: "bool", + }, + "return_u8_nonblocking": { name: "return_u8", parameters: [], result: "u8" }, + "return_i8_nonblocking": { name: "return_i8", parameters: [], result: "i8" }, + "return_u16_nonblocking": { + name: "return_u16", + parameters: [], + result: "u16", + }, + "return_i16_nonblocking": { + name: "return_i16", + parameters: [], + result: "i16", + }, + "return_u32_nonblocking": { + name: "return_u32", + parameters: [], + result: "u32", + }, + "return_i32_nonblocking": { + name: "return_i32", + parameters: [], + result: "i32", + }, + "return_u64_nonblocking": { + name: "return_u64", + parameters: [], + result: "u64", + }, + "return_i64_nonblocking": { + name: "return_i64", + parameters: [], + result: "i64", + }, + "return_usize_nonblocking": { + name: "return_usize", + parameters: [], + result: "usize", + }, + "return_isize_nonblocking": { + name: "return_isize", + parameters: [], + result: "isize", + }, + "return_f32_nonblocking": { + name: "return_f32", + parameters: [], + result: "f32", + }, + "return_f64_nonblocking": { + name: "return_f64", + parameters: [], + result: "f64", + }, + "return_buffer_nonblocking": { + name: "return_buffer", + parameters: [], + result: "buffer", + }, + // Parameter checking + "nop_many_parameters": { + parameters: [ + "u8", + "i8", + "u16", + "i16", + "u32", + "i32", + "u64", + "i64", + "usize", + "isize", + "f32", + "f64", + "buffer", + "u8", + "i8", + "u16", + "i16", + "u32", + "i32", + "u64", + "i64", + "usize", + "isize", + "f32", + "f64", + "buffer", + ], + result: "void", + }, + "nop_many_parameters_nonblocking": { + name: "nop_many_parameters", + parameters: [ + "u8", + "i8", + "u16", + "i16", + "u32", + "i32", + "u64", + "i64", + "usize", + "isize", + "f32", + "f64", + "pointer", + "u8", + "i8", + "u16", + "i16", + "u32", + "i32", + "u64", + "i64", + "usize", + "isize", + "f32", + "f64", + "pointer", + ], + result: "void", + nonblocking: true, + }, +}); + +const { nop } = dylib.symbols; +Deno.bench("nop()", () => { + nop(); +}); + +const bytes = new Uint8Array(64); + +const { hash } = dylib.symbols; +Deno.bench("hash()", () => { + hash(bytes, bytes.byteLength); +}); + +const { ffi_string } = dylib.symbols; +Deno.bench( + "c string", + () => Deno.UnsafePointerView.getCString(ffi_string()), +); + +const { add_u32 } = dylib.symbols; +Deno.bench("add_u32()", () => { + add_u32(1, 2); +}); + +const { return_buffer } = dylib.symbols; +Deno.bench("return_buffer()", () => { + return_buffer(); +}); + +const { add_u64 } = dylib.symbols; +Deno.bench("add_u64()", () => { + add_u64(1, 2); +}); + +const { return_u64 } = dylib.symbols; +Deno.bench("return_u64()", () => { + return_u64(); +}); + +const { return_i64 } = dylib.symbols; +Deno.bench("return_i64()", () => { + return_i64(); +}); + +const { nop_bool } = dylib.symbols; +Deno.bench("nop_bool()", () => { + nop_bool(true); +}); + +const { nop_u8 } = dylib.symbols; +Deno.bench("nop_u8()", () => { + nop_u8(100); +}); + +const { nop_i8 } = dylib.symbols; +Deno.bench("nop_i8()", () => { + nop_i8(100); +}); + +const { nop_u16 } = dylib.symbols; +Deno.bench("nop_u16()", () => { + nop_u16(100); +}); + +const { nop_i16 } = dylib.symbols; +Deno.bench("nop_i16()", () => { + nop_i16(100); +}); + +const { nop_u32 } = dylib.symbols; +Deno.bench("nop_u32()", () => { + nop_u32(100); +}); + +const { nop_i32 } = dylib.symbols; +Deno.bench("nop_i32()", () => { + nop_i32(100); +}); + +const { nop_u64 } = dylib.symbols; +Deno.bench("nop_u64()", () => { + nop_u64(100); +}); + +const { nop_i64 } = dylib.symbols; +Deno.bench("nop_i64()", () => { + nop_i64(100); +}); + +const { nop_usize } = dylib.symbols; +Deno.bench("nop_usize() number", () => { + nop_usize(100); +}); + +Deno.bench("nop_usize() bigint", () => { + nop_usize(100n); +}); + +const { nop_isize } = dylib.symbols; +Deno.bench("nop_isize() number", () => { + nop_isize(100); +}); + +Deno.bench("nop_isize() bigint", () => { + nop_isize(100n); +}); + +const { nop_f32 } = dylib.symbols; +Deno.bench("nop_f32()", () => { + nop_f32(100.1); +}); + +const { nop_f64 } = dylib.symbols; +Deno.bench("nop_f64()", () => { + nop_f64(100.1); +}); + +const { nop_buffer } = dylib.symbols; +const buffer = new Uint8Array(8).fill(5); +Deno.bench("nop_buffer()", () => { + nop_buffer(buffer); +}); + +const { return_bool } = dylib.symbols; +Deno.bench("return_bool()", () => { + return_bool(); +}); + +const { return_u8 } = dylib.symbols; +Deno.bench("return_u8()", () => { + return_u8(); +}); + +const { return_i8 } = dylib.symbols; +Deno.bench("return_i8()", () => { + return_i8(); +}); + +const { return_u16 } = dylib.symbols; +Deno.bench("return_u16()", () => { + return_u16(); +}); + +const { return_i16 } = dylib.symbols; +Deno.bench("return_i16()", () => { + return_i16(); +}); + +const { return_u32 } = dylib.symbols; +Deno.bench("return_u32()", () => { + return_u32(); +}); + +const { return_i32 } = dylib.symbols; +Deno.bench("return_i32()", () => { + return_i32(); +}); + +const { return_usize } = dylib.symbols; +Deno.bench("return_usize()", () => { + return_usize(); +}); + +const { return_isize } = dylib.symbols; +Deno.bench("return_isize()", () => { + return_isize(); +}); + +const { return_f32 } = dylib.symbols; +Deno.bench("return_f32()", () => { + return_f32(); +}); + +const { return_f64 } = dylib.symbols; +Deno.bench("return_f64()", () => { + return_f64(); +}); + +// Nonblocking calls + +const { nop_nonblocking } = dylib.symbols; +Deno.bench("nop_nonblocking()", async () => { + await nop_nonblocking(); +}); + +const { nop_bool_nonblocking } = dylib.symbols; +Deno.bench("nop_bool_nonblocking()", async () => { + await nop_bool_nonblocking(true); +}); + +const { nop_u8_nonblocking } = dylib.symbols; +Deno.bench("nop_u8_nonblocking()", async () => { + await nop_u8_nonblocking(100); +}); + +const { nop_i8_nonblocking } = dylib.symbols; +Deno.bench("nop_i8_nonblocking()", async () => { + await nop_i8_nonblocking(100); +}); + +const { nop_u16_nonblocking } = dylib.symbols; +Deno.bench("nop_u16_nonblocking()", async () => { + await nop_u16_nonblocking(100); +}); + +const { nop_i16_nonblocking } = dylib.symbols; +Deno.bench("nop_i16_nonblocking()", async () => { + await nop_i16_nonblocking(100); +}); + +const { nop_u32_nonblocking } = dylib.symbols; +Deno.bench("nop_u32_nonblocking()", async () => { + await nop_u32_nonblocking(100); +}); + +const { nop_i32_nonblocking } = dylib.symbols; + +Deno.bench("nop_i32_nonblocking()", async () => { + await nop_i32_nonblocking(100); +}); + +const { nop_u64_nonblocking } = dylib.symbols; +Deno.bench("nop_u64_nonblocking()", async () => { + await nop_u64_nonblocking(100); +}); + +const { nop_i64_nonblocking } = dylib.symbols; +Deno.bench("nop_i64_nonblocking()", async () => { + await nop_i64_nonblocking(100); +}); + +const { nop_usize_nonblocking } = dylib.symbols; +Deno.bench("nop_usize_nonblocking()", async () => { + await nop_usize_nonblocking(100); +}); + +const { nop_isize_nonblocking } = dylib.symbols; +Deno.bench("nop_isize_nonblocking()", async () => { + await nop_isize_nonblocking(100); +}); + +const { nop_f32_nonblocking } = dylib.symbols; +Deno.bench("nop_f32_nonblocking()", async () => { + await nop_f32_nonblocking(100); +}); + +const { nop_f64_nonblocking } = dylib.symbols; +Deno.bench("nop_f64_nonblocking()", async () => { + await nop_f64_nonblocking(100); +}); + +const { nop_buffer_nonblocking } = dylib.symbols; +Deno.bench("nop_buffer_nonblocking()", async () => { + await nop_buffer_nonblocking(buffer); +}); + +const { return_bool_nonblocking } = dylib.symbols; +Deno.bench("return_bool_nonblocking()", async () => { + await return_bool_nonblocking(); +}); + +const { return_u8_nonblocking } = dylib.symbols; +Deno.bench("return_u8_nonblocking()", async () => { + await return_u8_nonblocking(); +}); + +const { return_i8_nonblocking } = dylib.symbols; +Deno.bench("return_i8_nonblocking()", async () => { + await return_i8_nonblocking(); +}); + +const { return_u16_nonblocking } = dylib.symbols; +Deno.bench("return_u16_nonblocking()", async () => { + await return_u16_nonblocking(); +}); + +const { return_i16_nonblocking } = dylib.symbols; +Deno.bench("return_i16_nonblocking()", async () => { + await return_i16_nonblocking(); +}); + +const { return_u32_nonblocking } = dylib.symbols; +Deno.bench("return_u32_nonblocking()", async () => { + await return_u32_nonblocking(); +}); + +const { return_i32_nonblocking } = dylib.symbols; +Deno.bench("return_i32_nonblocking()", async () => { + await return_i32_nonblocking(); +}); + +const { return_u64_nonblocking } = dylib.symbols; +Deno.bench("return_u64_nonblocking()", async () => { + await return_u64_nonblocking(); +}); + +const { return_i64_nonblocking } = dylib.symbols; +Deno.bench("return_i64_nonblocking()", async () => { + await return_i64_nonblocking(); +}); + +const { return_usize_nonblocking } = dylib.symbols; +Deno.bench("return_usize_nonblocking()", async () => { + await return_usize_nonblocking(); +}); + +const { return_isize_nonblocking } = dylib.symbols; +Deno.bench("return_isize_nonblocking()", async () => { + await return_isize_nonblocking(); +}); + +const { return_f32_nonblocking } = dylib.symbols; +Deno.bench("return_f32_nonblocking()", async () => { + await return_f32_nonblocking(); +}); + +const { return_f64_nonblocking } = dylib.symbols; +Deno.bench("return_f64_nonblocking()", async () => { + await return_f64_nonblocking(); +}); + +const { return_buffer_nonblocking } = dylib.symbols; +Deno.bench("return_buffer_nonblocking()", async () => { + await return_buffer_nonblocking(); +}); + +const { nop_many_parameters } = dylib.symbols; +const buffer2 = new Uint8Array(8).fill(25); +Deno.bench("nop_many_parameters()", () => { + nop_many_parameters( + 135, + 47, + 356, + -236, + 7457, + -1356, + 16471468n, + -1334748136n, + 132658769535n, + -42745856824n, + 13567.26437, + 7.686234e-3, + buffer, + 64, + -42, + 83, + -136, + 3657, + -2376, + 3277918n, + -474628146n, + 344657895n, + -2436732n, + 135.26437e3, + 264.3576468623546834, + buffer2, + ); +}); + +const { nop_many_parameters_nonblocking } = dylib.symbols; +Deno.bench("nop_many_parameters_nonblocking()", () => { + nop_many_parameters_nonblocking( + 135, + 47, + 356, + -236, + 7457, + -1356, + 16471468n, + -1334748136n, + 132658769535n, + -42745856824n, + 13567.26437, + 7.686234e-3, + buffer, + 64, + -42, + 83, + -136, + 3657, + -2376, + 3277918n, + -474628146n, + 344657895n, + -2436732n, + 135.26437e3, + 264.3576468623546834, + buffer2, + ); +}); + +Deno.bench("Deno.UnsafePointer.of", () => { + Deno.UnsafePointer.of(buffer); +}); + +const cstringBuffer = new TextEncoder().encode("Best believe it!\0"); +const cstringPointerView = new Deno.UnsafePointerView( + Deno.UnsafePointer.of(cstringBuffer), +); +Deno.bench("Deno.UnsafePointerView#getCString", () => { + cstringPointerView.getCString(); +}); + +const bufferPointerView = new Deno.UnsafePointerView( + Deno.UnsafePointer.of(buffer), +); + +Deno.bench("Deno.UnsafePointerView#getBool", () => { + bufferPointerView.getBool(); +}); + +Deno.bench("Deno.UnsafePointerView#getUint8", () => { + bufferPointerView.getUint8(); +}); + +Deno.bench("Deno.UnsafePointerView#getInt8", () => { + bufferPointerView.getInt8(); +}); + +Deno.bench("Deno.UnsafePointerView#getUint16", () => { + bufferPointerView.getUint16(); +}); + +Deno.bench("Deno.UnsafePointerView#getInt16", () => { + bufferPointerView.getInt16(); +}); + +Deno.bench("Deno.UnsafePointerView#getUint32", () => { + bufferPointerView.getUint32(); +}); + +Deno.bench("Deno.UnsafePointerView#getInt32", () => { + bufferPointerView.getInt32(); +}); + +Deno.bench("Deno.UnsafePointerView#getBigUint64", () => { + bufferPointerView.getBigUint64(); +}); + +Deno.bench("Deno.UnsafePointerView#getBigInt64", () => { + bufferPointerView.getBigInt64(); +}); + +Deno.bench("Deno.UnsafePointerView#getFloat32", () => { + bufferPointerView.getFloat32(); +}); + +Deno.bench("Deno.UnsafePointerView#getFloat64", () => { + bufferPointerView.getFloat64(); +}); diff --git a/tests/ffi/tests/event_loop_integration.ts b/tests/ffi/tests/event_loop_integration.ts new file mode 100644 index 000000000..d9ada6027 --- /dev/null +++ b/tests/ffi/tests/event_loop_integration.ts @@ -0,0 +1,78 @@ +// Copyright 2018-2024 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: { + parameters: ["function"], + result: "void", + }, + call_stored_function: { + parameters: [], + result: "void", + }, + call_stored_function_thread_safe_and_log: { + parameters: [], + result: "void", + }, + } as const, +); + +let retry = false; +const tripleLogCallback = () => { + console.log("Sync"); + queueMicrotask(() => { + console.log("Async"); + callback.unref(); + }); + setTimeout(() => { + console.log("Timeout"); + callback.unref(); + + if (retry) { + // Re-ref and retry the call to make sure re-refing works. + console.log("RETRY THREAD SAFE"); + retry = false; + callback.ref(); + dylib.symbols.call_stored_function_thread_safe_and_log(); + } + }, 100); +}; + +const callback = Deno.UnsafeCallback.threadSafe( + { + parameters: [], + result: "void", + } as const, + tripleLogCallback, +); + +// Store function +dylib.symbols.store_function(callback.pointer); + +// Synchronous callback logging +console.log("SYNCHRONOUS"); +// This function only calls the callback, and does not log. +dylib.symbols.call_stored_function(); +console.log("STORED_FUNCTION called"); + +// Wait to make sure synch logging and async logging +await new Promise((res) => setTimeout(res, 200)); + +// Ref once to make sure both `queueMicrotask()` and `setTimeout()` +// must resolve and unref before isolate exists. +// One ref'ing has been done by `threadSafe` constructor. +callback.ref(); + +console.log("THREAD SAFE"); +retry = true; +// This function calls the callback and logs 'STORED_FUNCTION called' +dylib.symbols.call_stored_function_thread_safe_and_log(); diff --git a/tests/ffi/tests/ffi_callback_errors.ts b/tests/ffi/tests/ffi_callback_errors.ts new file mode 100644 index 000000000..dbd60636c --- /dev/null +++ b/tests/ffi/tests/ffi_callback_errors.ts @@ -0,0 +1,141 @@ +// Copyright 2018-2024 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/tests/ffi/tests/ffi_types.ts b/tests/ffi/tests/ffi_types.ts new file mode 100644 index 000000000..596662873 --- /dev/null +++ b/tests/ffi/tests/ffi_types.ts @@ -0,0 +1,529 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file +// Only for testing types. Invoke with `deno cache` + +const remote = Deno.dlopen( + "dummy_lib.so", + { + method1: { parameters: ["usize", "bool"], result: "void", callback: true }, + method2: { parameters: [], result: "void" }, + method3: { parameters: ["usize"], result: "void" }, + method4: { parameters: ["isize"], result: "void" }, + method5: { parameters: ["u8"], result: "void" }, + method6: { parameters: ["u16"], result: "void" }, + method7: { parameters: ["u32"], result: "void" }, + method8: { parameters: ["u64"], result: "void" }, + method9: { parameters: ["i8"], result: "void" }, + method10: { parameters: ["i16"], result: "void" }, + method11: { parameters: ["i32"], result: "void" }, + method12: { parameters: ["i64"], result: "void" }, + method13: { parameters: ["f32"], result: "void" }, + method14: { parameters: ["f64"], result: "void" }, + method15: { parameters: ["pointer"], result: "void" }, + method16: { parameters: [], result: "usize" }, + method17: { parameters: [], result: "usize", nonblocking: true }, + method18: { parameters: [], result: "pointer" }, + method19: { parameters: [], result: "pointer", nonblocking: true }, + method20: { + parameters: ["pointer"], + result: "void", + }, + method21: { + parameters: [ + "pointer", + ], + result: "void", + }, + method22: { + parameters: ["pointer"], + result: "void", + }, + method23: { + parameters: ["buffer"], + result: "void", + }, + method24: { + parameters: ["bool"], + result: "bool", + }, + method25: { + parameters: [], + result: "void", + optional: true, + }, + static1: { type: "usize" }, + static2: { type: "pointer" }, + static3: { type: "usize" }, + static4: { type: "isize" }, + static5: { type: "u8" }, + static6: { type: "u16" }, + static7: { type: "u32" }, + static8: { type: "u64" }, + static9: { type: "i8" }, + static10: { type: "i16" }, + static11: { type: "i32" }, + static12: { type: "i64" }, + static13: { type: "f32" }, + static14: { type: "f64" }, + static15: { type: "bool" }, + static16: { + type: "bool", + optional: true, + }, + }, +); + +Deno.dlopen( + "dummy_lib_2.so", + { + wrong_method1: { + parameters: [], + result: "function", + }, + }, +); + +// @ts-expect-error: Invalid argument +remote.symbols.method1(0); +// @ts-expect-error: Invalid argument +remote.symbols.method1(0, 0); +// @ts-expect-error: Invalid argument +remote.symbols.method1(true, true); +// @ts-expect-error: Invalid return type +<number> remote.symbols.method1(0, true); +<void> remote.symbols.method1(0n, true); + +// @ts-expect-error: Expected 0 arguments, but got 1. +remote.symbols.method2(null); +remote.symbols.method2(); + +// @ts-expect-error: Invalid argument +remote.symbols.method3(null); +remote.symbols.method3(0n); + +// @ts-expect-error: Invalid argument +remote.symbols.method4(null); +remote.symbols.method4(0n); + +// @ts-expect-error: Invalid argument +remote.symbols.method5(null); +remote.symbols.method5(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method6(null); +remote.symbols.method6(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method7(null); +remote.symbols.method7(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method8(null); +remote.symbols.method8(0n); + +// @ts-expect-error: Invalid argument +remote.symbols.method9(null); +remote.symbols.method9(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method10(null); +remote.symbols.method10(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method11(null); +remote.symbols.method11(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method12(null); +remote.symbols.method12(0n); + +// @ts-expect-error: Invalid argument +remote.symbols.method13(null); +remote.symbols.method13(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method14(null); +remote.symbols.method14(0); + +// @ts-expect-error: Invalid argument +remote.symbols.method15("foo"); +// @ts-expect-error: Invalid argument +remote.symbols.method15(new Uint16Array(1)); +remote.symbols.method15(null); +remote.symbols.method15({} as Deno.PointerValue); + +const result = remote.symbols.method16(); +// @ts-expect-error: Invalid argument +let r_0: string = result; +let r_1: number | bigint = result; + +const result2 = remote.symbols.method17(); +// @ts-expect-error: Invalid argument +result2.then((_0: string) => {}); +result2.then((_1: number | bigint) => {}); + +const result3 = remote.symbols.method18(); +// @ts-expect-error: Invalid argument +let r3_0: Deno.BufferSource = result3; +let r3_1: null | Deno.UnsafePointer = result3; + +const result4 = remote.symbols.method19(); +// @ts-expect-error: Invalid argument +result4.then((_0: Deno.BufferSource) => {}); +result4.then((_1: null | Deno.UnsafePointer) => {}); + +const fnptr = new Deno.UnsafeFnPointer( + {} as Deno.PointerObject, + { + parameters: ["u32", "pointer"], + result: "void", + }, +); +// @ts-expect-error: Invalid argument +fnptr.call(null, null); +fnptr.call(0, null); + +const unsafe_callback_wrong1 = new Deno.UnsafeCallback( + { + parameters: ["i8"], + result: "void", + }, + // @ts-expect-error: i8 is not a pointer + (_: bigint) => {}, +); +const unsafe_callback_wrong2 = new Deno.UnsafeCallback( + { + parameters: ["pointer"], + result: "u64", + }, + // @ts-expect-error: must return a number or bigint + (_: Deno.UnsafePointer) => {}, +); +const unsafe_callback_wrong3 = new Deno.UnsafeCallback( + { + parameters: [], + result: "void", + }, + // @ts-expect-error: no parameters + (_: Deno.UnsafePointer) => {}, +); +const unsafe_callback_wrong4 = new Deno.UnsafeCallback( + { + parameters: ["u64"], + result: "void", + }, + // @ts-expect-error: Callback's 64bit parameters are either number or bigint + (_: number) => {}, +); +const unsafe_callback_right1 = new Deno.UnsafeCallback( + { + parameters: ["u8", "u32", "pointer"], + result: "void", + }, + (_1: number, _2: number, _3: null | Deno.PointerValue) => {}, +); +const unsafe_callback_right2 = new Deno.UnsafeCallback( + { + parameters: [], + result: "u8", + }, + () => 3, +); +const unsafe_callback_right3 = new Deno.UnsafeCallback( + { + parameters: [], + result: "function", + }, + // Callbacks can return other callbacks' pointers, if really wanted. + () => unsafe_callback_right2.pointer, +); +const unsafe_callback_right4 = new Deno.UnsafeCallback( + { + parameters: ["u8", "u32", "pointer"], + result: "u8", + }, + (_1: number, _2: number, _3: null | Deno.PointerValue) => 3, +); +const unsafe_callback_right5 = new Deno.UnsafeCallback( + { + parameters: ["u8", "i32", "pointer"], + result: "void", + }, + (_1: number, _2: number, _3: null | Deno.PointerValue) => {}, +); + +// @ts-expect-error: Must pass callback +remote.symbols.method20(); +// nullptr is okay +remote.symbols.method20(null); +// @ts-expect-error: Callback cannot be passed directly +remote.symbols.method20(unsafe_callback_right2); +remote.symbols.method20(unsafe_callback_right1.pointer); + +remote.symbols.method23(new Uint8Array(1)); +remote.symbols.method23(new Uint32Array(1)); +remote.symbols.method23(new Uint8Array(1)); + +// @ts-expect-error: Cannot pass pointer values as buffer. +remote.symbols.method23({}); +// @ts-expect-error: Cannot pass pointer values as buffer. +remote.symbols.method23({}); +remote.symbols.method23(null); + +// @ts-expect-error: Cannot pass number as bool. +remote.symbols.method24(0); +// @ts-expect-error: Cannot pass number as bool. +remote.symbols.method24(1); +// @ts-expect-error: Cannot pass null as bool. +remote.symbols.method24(null); +remote.symbols.method24(true); +remote.symbols.method24(false); +// @ts-expect-error: Cannot assert return type as a number. +<number> remote.symbols.method24(true); +// @ts-expect-error: Cannot assert return type truthiness. +let r24_0: true = remote.symbols.method24(true); +// @ts-expect-error: Cannot assert return type as a number. +let r42_1: number = remote.symbols.method24(true); +<boolean> remote.symbols.method24(Math.random() > 0.5); + +// @ts-expect-error: Optional symbol; can be null. +remote.symbols.method25(); + +// @ts-expect-error: Invalid member type +const static1_wrong: null = remote.symbols.static1; +const static1_right: number | bigint = remote.symbols.static1; +// @ts-expect-error: Invalid member type +const static2_wrong: null = remote.symbols.static2; +const static2_right: null | Deno.UnsafePointer = remote.symbols.static2; +// @ts-expect-error: Invalid member type +const static3_wrong: null = remote.symbols.static3; +const static3_right: number | bigint = remote.symbols.static3; +// @ts-expect-error: Invalid member type +const static4_wrong: null = remote.symbols.static4; +const static4_right: number | bigint = remote.symbols.static4; +// @ts-expect-error: Invalid member type +const static5_wrong: null = remote.symbols.static5; +const static5_right: number = remote.symbols.static5; +// @ts-expect-error: Invalid member type +const static6_wrong: null = remote.symbols.static6; +const static6_right: number = remote.symbols.static6; +// @ts-expect-error: Invalid member type +const static7_wrong: null = remote.symbols.static7; +const static7_right: number = remote.symbols.static7; +// @ts-expect-error: Invalid member type +const static8_wrong: null = remote.symbols.static8; +const static8_right: number | bigint = remote.symbols.static8; +// @ts-expect-error: Invalid member type +const static9_wrong: null = remote.symbols.static9; +const static9_right: number = remote.symbols.static9; +// @ts-expect-error: Invalid member type +const static10_wrong: null = remote.symbols.static10; +const static10_right: number = remote.symbols.static10; +// @ts-expect-error: Invalid member type +const static11_wrong: null = remote.symbols.static11; +const static11_right: number = remote.symbols.static11; +// @ts-expect-error: Invalid member type +const static12_wrong: null = remote.symbols.static12; +const static12_right: number | bigint = remote.symbols.static12; +// @ts-expect-error: Invalid member type +const static13_wrong: null = remote.symbols.static13; +const static13_right: number = remote.symbols.static13; +// @ts-expect-error: Invalid member type +const static14_wrong: null = remote.symbols.static14; +const static14_right: number = remote.symbols.static14; +// @ts-expect-error: Invalid member type +const static15_wrong: number = remote.symbols.static15; +const static15_right: boolean = remote.symbols.static15; +// @ts-expect-error: Invalid member type +const static16_wrong: boolean = remote.symbols.static16; +const static16_right: boolean | null = remote.symbols.static16; + +// Adapted from https://stackoverflow.com/a/53808212/10873797 +type Equal<T, U> = (<G>() => G extends T ? 1 : 2) extends + (<G>() => G extends U ? 1 : 2) ? true + : false; + +type AssertEqual< + Expected extends $, + Got extends $$, + $ = [Equal<Got, Expected>] extends [true] ? Expected + : ([Expected] extends [Got] ? never : Got), + $$ = [Equal<Expected, Got>] extends [true] ? Got + : ([Got] extends [Expected] ? never : Got), +> = never; + +type AssertNotEqual< + Expected extends $, + Got, + $ = [Equal<Expected, Got>] extends [true] ? never : Expected, +> = never; + +const enum FooEnum { + Foo, + Bar, +} +const foo = "u8" as Deno.NativeU8Enum<FooEnum>; + +declare const brand: unique symbol; +class MyPointerClass {} +type MyPointer = Deno.PointerObject & { [brand]: MyPointerClass }; +const myPointer = "pointer" as Deno.NativeTypedPointer<MyPointer>; +type MyFunctionDefinition = Deno.UnsafeCallbackDefinition< + [typeof foo, "u32"], + typeof myPointer +>; +const myFunction = "function" as Deno.NativeTypedFunction< + MyFunctionDefinition +>; + +type __Tests__ = [ + empty: AssertEqual< + { symbols: Record<never, never>; close(): void }, + Deno.DynamicLibrary<Record<never, never>> + >, + basic: AssertEqual< + { symbols: { add: (n1: number, n2: number) => number }; close(): void }, + Deno.DynamicLibrary<{ add: { parameters: ["i32", "u8"]; result: "i32" } }> + >, + higher_order_params: AssertEqual< + { + symbols: { + pushBuf: ( + buf: BufferSource | null, + ptr: Deno.PointerValue | null, + func: Deno.PointerValue | null, + ) => void; + }; + close(): void; + }, + Deno.DynamicLibrary< + { + pushBuf: { + parameters: ["buffer", "pointer", "function"]; + result: "void"; + }; + } + > + >, + higher_order_returns: AssertEqual< + { + symbols: { + pushBuf: ( + buf: BufferSource | null, + ptr: Deno.PointerValue, + func: Deno.PointerValue, + ) => Deno.PointerValue; + }; + close(): void; + }, + Deno.DynamicLibrary< + { + pushBuf: { + parameters: ["buffer", "pointer", "function"]; + result: "pointer"; + }; + } + > + >, + non_exact_params: AssertEqual< + { + symbols: { + foo: ( + ...args: (number | Deno.PointerValue | null)[] + ) => number | bigint; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: ("i32" | "pointer")[]; result: "u64" } } + > + >, + non_exact_params_empty: AssertEqual< + { + symbols: { + foo: () => number; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: []; result: "i32" } } + > + >, + non_exact_params_empty: AssertNotEqual< + { + symbols: { + foo: (a: number) => number; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: []; result: "i32" } } + > + >, + enum_param: AssertEqual< + { + symbols: { + foo: (arg: FooEnum) => void; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: [typeof foo]; result: "void" } } + > + >, + enum_return: AssertEqual< + { + symbols: { + foo: () => FooEnum; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: []; result: typeof foo } } + > + >, + typed_pointer_param: AssertEqual< + { + symbols: { + foo: (arg: MyPointer | null) => void; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: [typeof myPointer]; result: "void" } } + > + >, + typed_pointer_return: AssertEqual< + { + symbols: { + foo: () => MyPointer | null; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: []; result: typeof myPointer } } + > + >, + typed_function_param: AssertEqual< + { + symbols: { + foo: (arg: Deno.PointerObject<MyFunctionDefinition> | null) => void; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: [typeof myFunction]; result: "void" } } + > + >, + typed_function_return: AssertEqual< + { + symbols: { + foo: () => Deno.PointerObject<MyFunctionDefinition> | null; + }; + close(): void; + }, + Deno.DynamicLibrary< + { foo: { parameters: []; result: typeof myFunction } } + > + >, +]; diff --git a/tests/ffi/tests/integration_tests.rs b/tests/ffi/tests/integration_tests.rs new file mode 100644 index 000000000..0ad95254c --- /dev/null +++ b/tests/ffi/tests/integration_tests.rs @@ -0,0 +1,302 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use pretty_assertions::assert_eq; +use std::process::Command; +use test_util::deno_cmd; +use test_util::deno_config_path; +use test_util::ffi_tests_path; + +#[cfg(debug_assertions)] +const BUILD_VARIANT: &str = "debug"; + +#[cfg(not(debug_assertions))] +const BUILD_VARIANT: &str = "release"; + +fn build() { + let mut build_plugin_base = Command::new("cargo"); + let mut build_plugin = + build_plugin_base.arg("build").arg("-p").arg("test_ffi"); + if BUILD_VARIANT == "release" { + build_plugin = build_plugin.arg("--release"); + } + let build_plugin_output = build_plugin.output().unwrap(); + assert!(build_plugin_output.status.success()); +} + +#[test] +fn basic() { + build(); + + let output = deno_cmd() + .current_dir(ffi_tests_path()) + .arg("run") + .arg("--config") + .arg(deno_config_path()) + .arg("--no-lock") + .arg("--allow-ffi") + .arg("--allow-read") + .arg("--unstable-ffi") + .arg("--quiet") + .arg(r#"--v8-flags=--allow-natives-syntax"#) + .arg("tests/test.js") + .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 = "\ + something\n\ + [1, 2, 3, 4, 5, 6, 7, 8]\n\ + [4, 5, 6]\n\ + [1, 2, 3, 4, 5, 6, 7, 8] [9, 10]\n\ + [1, 2, 3, 4, 5, 6, 7, 8]\n\ + [ 1, 2, 3, 4, 5, 6 ]\n\ + [ 4, 5, 6 ]\n\ + [ 4, 5, 6 ]\n\ + Hello from pointer!\n\ + pointer!\n\ + false false\n\ + true true\n\ + false false\n\ + true true\n\ + false false\n\ + 579\n\ + true\n\ + 579\n\ + 579\n\ + 5\n\ + 5\n\ + 579\n\ + 8589934590\n\ + -8589934590\n\ + 8589934590\n\ + -8589934590\n\ + 9007199254740992n\n\ + 9007199254740992n\n\ + -9007199254740992n\n\ + 9007199254740992n\n\ + 9007199254740992n\n\ + -9007199254740992n\n\ + 579.9119873046875\n\ + 579.912\n\ + true\n\ + false\n\ + 579.9119873046875\n\ + 579.9119873046875\n\ + 579.912\n\ + 579.912\n\ + 579\n\ + 8589934590\n\ + -8589934590\n\ + 8589934590\n\ + -8589934590\n\ + 9007199254740992n\n\ + 9007199254740992n\n\ + -9007199254740992n\n\ + 9007199254740992n\n\ + 9007199254740992n\n\ + -9007199254740992n\n\ + 579.9119873046875\n\ + 579.912\n\ + Before\n\ + After\n\ + logCallback\n\ + 1 -1 2 -2 3 -3 4 -4 0.5 -0.5 1 2 3 4 5 6 7 8\n\ + u8: 8\n\ + buf: [1, 2, 3, 4, 5, 6, 7, 8]\n\ + logCallback\n\ + 30\n\ + 255 65535 4294967295 4294967296 123.456 789.876 -1 -2 -3 -4 -1000 1000 12345.67891 12345.679 12345.67891 12345.679 12345.67891 12345.679 12345.67891\n\ + 255 65535 4294967295 4294967296 123.456 789.876 -1 -2 -3 -4 -1000 1000 12345.67891 12345.679 12345.67891 12345.679 12345.67891 12345.679 12345.67891\n\ + 0\n\ + 0\n\ + 0\n\ + 0\n\ + 78\n\ + 78\n\ + STORED_FUNCTION cleared\n\ + STORED_FUNCTION_2 cleared\n\ + logCallback\n\ + u8: 8\n\ + Rect { x: 10.0, y: 20.0, w: 100.0, h: 200.0 }\n\ + Rect { x: 10.0, y: 20.0, w: 100.0, h: 200.0 }\n\ + Rect { x: 20.0, y: 20.0, w: 100.0, h: 200.0 }\n\ + Mixed { u8: 3, f32: 12.515, rect: Rect { x: 10.0, y: 20.0, w: 100.0, h: 200.0 }, usize: 12456789, array: [8, 32] }\n\ + 2264956937\n\ + 2264956937\n\ + Correct number of resources\n"; + assert_eq!(stdout, expected); + assert_eq!(stderr, ""); +} + +#[test] +fn symbol_types() { + build(); + + let output = deno_cmd() + .current_dir(ffi_tests_path()) + .arg("check") + .arg("--config") + .arg(deno_config_path()) + .arg("--no-lock") + .arg("--unstable-ffi") + .arg("--quiet") + .arg("tests/ffi_types.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()); + assert_eq!(stderr, ""); +} + +#[test] +fn thread_safe_callback() { + build(); + + let output = deno_cmd() + .current_dir(ffi_tests_path()) + .arg("run") + .arg("--config") + .arg(deno_config_path()) + .arg("--no-lock") + .arg("--allow-ffi") + .arg("--allow-read") + .arg("--unstable-ffi") + .arg("--quiet") + .arg("tests/thread_safe_test.js") + .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 = "\ + Callback on main thread\n\ + Callback on worker thread\n\ + STORED_FUNCTION cleared\n\ + Calling callback, isolate should stay asleep until callback is called\n\ + Callback being called\n\ + STORED_FUNCTION cleared\n\ + Isolate should now exit\n"; + assert_eq!(stdout, expected); + assert_eq!(stderr, ""); +} + +#[test] +fn event_loop_integration() { + build(); + + let output = deno_cmd() + .current_dir(ffi_tests_path()) + .arg("run") + .arg("--config") + .arg(deno_config_path()) + .arg("--no-lock") + .arg("--allow-ffi") + .arg("--allow-read") + .arg("--unstable-ffi") + .arg("--quiet") + .arg("tests/event_loop_integration.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()); + // TODO(aapoalas): The order of logging in thread safe callbacks is + // unexpected: The callback logs synchronously and creates an asynchronous + // logging task, which then gets called synchronously before the callback + // actually yields to the calling thread. This is in contrast to what the + // logging would look like if the call was coming from within Deno itself, + // and may lead users to unknowingly run heavy asynchronous tasks from thread + // safe callbacks synchronously. + // The fix would be to make sure microtasks are only run after the event loop + // middleware that polls them has completed its work. This just does not seem + // to work properly with Linux release builds. + let expected = "\ + SYNCHRONOUS\n\ + Sync\n\ + STORED_FUNCTION called\n\ + Async\n\ + Timeout\n\ + THREAD SAFE\n\ + Sync\n\ + Async\n\ + STORED_FUNCTION called\n\ + Timeout\n\ + RETRY THREAD SAFE\n\ + Sync\n\ + Async\n\ + STORED_FUNCTION called\n\ + Timeout\n"; + assert_eq!(stdout, expected); + assert_eq!(stderr, ""); +} + +#[test] +fn ffi_callback_errors_test() { + build(); + + let output = deno_cmd() + .current_dir(ffi_tests_path()) + .arg("run") + .arg("--config") + .arg(deno_config_path()) + .arg("--no-lock") + .arg("--allow-ffi") + .arg("--allow-read") + .arg("--unstable-ffi") + .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) + ); +} diff --git a/tests/ffi/tests/test.js b/tests/ffi/tests/test.js new file mode 100644 index 000000000..6b8e509c0 --- /dev/null +++ b/tests/ffi/tests/test.js @@ -0,0 +1,802 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +// Run using cargo test or `--v8-flags=--allow-natives-syntax` + +import { + assertThrows, + assert, + assertNotEquals, + assertInstanceOf, + assertEquals, + assertFalse, +} from "@std/assert/mod.ts"; + +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 resourcesPre = Deno[Deno.internal].core.resources(); + +// dlopen shouldn't panic +assertThrows(() => { + Deno.dlopen("cli/src/main.rs", {}); +}); + +assertThrows( + () => { + Deno.dlopen(libPath, { + non_existent_symbol: { + parameters: [], + result: "void", + }, + }); + }, + Error, + "Failed to register symbol non_existent_symbol", +); + +assertThrows(() => { + Deno.dlopen(libPath, { + print_something: { + parameters: [], + result: { struct: [] } + }, + }), + TypeError, + "Struct must have at least one field" +}); + +assertThrows(() => { + Deno.dlopen(libPath, { + print_something: { + parameters: [ { struct: [] } ], + result: "void", + }, + }), + TypeError, + "Struct must have at least one field" +}); + +const Empty = { struct: [] } +assertThrows(() => { + Deno.dlopen(libPath, { + print_something: { + parameters: [ { struct: [Empty] } ], + result: "void", + }, + }), + TypeError, + "Struct must have at least one field" +}); + +const Point = ["f64", "f64"]; +const Size = ["f64", "f64"]; +const Rect = ["f64", "f64", "f64", "f64"]; +const RectNested = [{ struct: Point }, { struct: Size }]; +const RectNestedCached = [{ struct: Size }, { struct: Size }]; +const Mixed = ["u8", "f32", { struct: Rect }, "usize", { struct: ["u32", "u32"] }]; + +const dylib = Deno.dlopen(libPath, { + "printSomething": { + name: "print_something", + parameters: [], + result: "void", + }, + "print_buffer": { parameters: ["buffer", "usize"], result: "void" }, + "print_pointer": { name: "print_buffer", parameters: ["pointer", "usize"], result: "void" }, + "print_buffer2": { + parameters: ["buffer", "usize", "buffer", "usize"], + result: "void", + }, + "return_buffer": { parameters: [], result: "buffer" }, + "is_null_ptr": { parameters: ["pointer"], result: "bool" }, + "is_null_buf": { name: "is_null_ptr", parameters: ["buffer"], result: "bool" }, + "add_u32": { parameters: ["u32", "u32"], result: "u32" }, + "add_i32": { parameters: ["i32", "i32"], result: "i32" }, + "add_u64": { parameters: ["u64", "u64"], result: "u64" }, + "add_i64": { parameters: ["i64", "i64"], result: "i64" }, + "add_usize": { parameters: ["usize", "usize"], result: "usize" }, + "add_usize_fast": { parameters: ["usize", "usize"], result: "u32" }, + "add_isize": { parameters: ["isize", "isize"], result: "isize" }, + "add_f32": { parameters: ["f32", "f32"], result: "f32" }, + "add_f64": { parameters: ["f64", "f64"], result: "f64" }, + "and": { parameters: ["bool", "bool"], result: "bool" }, + "add_u32_nonblocking": { + name: "add_u32", + parameters: ["u32", "u32"], + result: "u32", + nonblocking: true, + }, + "add_i32_nonblocking": { + name: "add_i32", + parameters: ["i32", "i32"], + result: "i32", + nonblocking: true, + }, + "add_u64_nonblocking": { + name: "add_u64", + parameters: ["u64", "u64"], + result: "u64", + nonblocking: true, + }, + "add_i64_nonblocking": { + name: "add_i64", + parameters: ["i64", "i64"], + result: "i64", + nonblocking: true, + }, + "add_usize_nonblocking": { + name: "add_usize", + parameters: ["usize", "usize"], + result: "usize", + nonblocking: true, + }, + "add_isize_nonblocking": { + name: "add_isize", + parameters: ["isize", "isize"], + result: "isize", + nonblocking: true, + }, + "add_f32_nonblocking": { + name: "add_f32", + parameters: ["f32", "f32"], + result: "f32", + nonblocking: true, + }, + "add_f64_nonblocking": { + name: "add_f64", + parameters: ["f64", "f64"], + result: "f64", + nonblocking: true, + }, + "fill_buffer": { parameters: ["u8", "buffer", "usize"], result: "void" }, + "sleep_nonblocking": { + name: "sleep_blocking", + parameters: ["u64"], + result: "void", + nonblocking: true, + }, + "sleep_blocking": { parameters: ["u64"], result: "void" }, + "nonblocking_buffer": { + parameters: ["buffer", "usize"], + result: "void", + nonblocking: true, + }, + "get_add_u32_ptr": { + parameters: [], + result: "pointer", + }, + "get_sleep_blocking_ptr": { + parameters: [], + result: "pointer", + }, + // Callback function + call_fn_ptr: { + parameters: ["function"], + result: "void", + }, + call_fn_ptr_thread_safe: { + name: "call_fn_ptr", + parameters: ["function"], + result: "void", + nonblocking: true, + }, + call_fn_ptr_many_parameters: { + parameters: ["function"], + result: "void", + }, + call_fn_ptr_return_u8: { + parameters: ["function"], + result: "void", + }, + call_fn_ptr_return_u8_thread_safe: { + name: "call_fn_ptr_return_u8", + parameters: ["function"], + result: "void", + }, + call_fn_ptr_return_buffer: { + parameters: ["function"], + result: "void", + }, + store_function: { + parameters: ["function"], + result: "void", + }, + store_function_2: { + parameters: ["function"], + result: "void", + }, + call_stored_function: { + parameters: [], + result: "void", + callback: true, + }, + call_stored_function_2: { + parameters: ["u8"], + result: "void", + callback: true, + }, + log_many_parameters: { + parameters: ["u8", "u16", "u32", "u64", "f64", "f32", "i64", "i32", "i16", "i8", "isize", "usize", "f64", "f32", "f64", "f32", "f64", "f32", "f64"], + result: "void", + }, + cast_u8_u32: { + parameters: ["u8"], + result: "u32", + }, + cast_u32_u8: { + parameters: ["u32"], + result: "u8", + }, + add_many_u16: { + parameters: ["u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16"], + result: "u16", + }, + // Statics + "static_u32": { + type: "u32", + }, + "static_i64": { + type: "i64", + }, + "static_ptr": { + type: "pointer", + }, + /** + * Invalid UTF-8 characters, buffer of length 14 + */ + "static_char": { + type: "pointer", + optional: true, + }, + "hash": { parameters: ["buffer", "u32"], result: "u32" }, + make_rect: { + parameters: ["f64", "f64", "f64", "f64"], + result: { struct: Rect }, + }, + make_rect_async: { + name: "make_rect", + nonblocking: true, + parameters: ["f64", "f64", "f64", "f64"], + result: { struct: RectNested }, + }, + print_rect: { + parameters: [{ struct: RectNestedCached }], + result: "void", + }, + print_rect_async: { + name: "print_rect", + nonblocking: true, + parameters: [{ struct: Rect }], + result: "void", + }, + create_mixed: { + parameters: ["u8", "f32", { struct: Rect }, "pointer", "buffer"], + result: { struct: Mixed } + }, + print_mixed: { + parameters: [{ struct: Mixed }], + result: "void", + optional: true, + }, + non_existent_symbol: { + parameters: [], + result: "void", + optional: true, + }, + non_existent_nonblocking_symbol: { + parameters: [], + result: "void", + nonblocking: true, + optional: true, + }, + non_existent_static: { + type: "u32", + optional: true, + }, +}); +const { symbols } = dylib; + +symbols.printSomething(); +const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +const buffer2 = new Uint8Array([9, 10]); +dylib.symbols.print_buffer(buffer, buffer.length); +// Test subarrays +const subarray = buffer.subarray(3); +dylib.symbols.print_buffer(subarray, subarray.length - 2); +dylib.symbols.print_buffer2(buffer, buffer.length, buffer2, buffer2.length); + +const { return_buffer } = symbols; +function returnBuffer() { return return_buffer(); }; + +%PrepareFunctionForOptimization(returnBuffer); +returnBuffer(); +%OptimizeFunctionOnNextCall(returnBuffer); +const ptr0 = returnBuffer(); +assertIsOptimized(returnBuffer); + +dylib.symbols.print_pointer(ptr0, 8); +const ptrView = new Deno.UnsafePointerView(ptr0); +const into = new Uint8Array(6); +const into2 = new Uint8Array(3); +const into2ptr = Deno.UnsafePointer.of(into2); +const into2ptrView = new Deno.UnsafePointerView(into2ptr); +const into3 = new Uint8Array(3); +const into4 = new Uint16Array(3); +ptrView.copyInto(into4); +ptrView.copyInto(into); +console.log([...into]); +ptrView.copyInto(into2, 3); +console.log([...into2]); +into2ptrView.copyInto(into3); +console.log([...into3]); +const string = new Uint8Array([ + ...new TextEncoder().encode("Hello from pointer!"), + 0, +]); +const stringPtr = Deno.UnsafePointer.of(string); +const stringPtrview = new Deno.UnsafePointerView(stringPtr); +console.log(stringPtrview.getCString()); +console.log(stringPtrview.getCString(11)); +console.log("false", dylib.symbols.is_null_ptr(ptr0)); +console.log("true", dylib.symbols.is_null_ptr(null)); +console.log("false", dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(into))); +const emptyBuffer = new Uint8Array(0); +console.log("true", dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(emptyBuffer))); +const emptySlice = into.subarray(6); +console.log("false", dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(emptySlice))); + +const { is_null_buf } = symbols; +function isNullBuffer(buffer) { return is_null_buf(buffer); }; +function isNullBufferDeopt(buffer) { return is_null_buf(buffer); }; +%PrepareFunctionForOptimization(isNullBuffer); +isNullBuffer(emptyBuffer); +%NeverOptimizeFunction(isNullBufferDeopt); +%OptimizeFunctionOnNextCall(isNullBuffer); +isNullBuffer(emptyBuffer); +assertIsOptimized(isNullBuffer); + +// ==== ZERO LENGTH BUFFER TESTS ==== +assertEquals(isNullBuffer(emptyBuffer), true, "isNullBuffer(emptyBuffer) !== true"); +assertEquals(isNullBufferDeopt(emptyBuffer), true, "isNullBufferDeopt(emptyBuffer) !== true"); +assertEquals(isNullBuffer(emptySlice), false, "isNullBuffer(emptySlice) !== false"); +assertEquals(isNullBufferDeopt(emptySlice), false, "isNullBufferDeopt(emptySlice) !== false"); +assertEquals(isNullBufferDeopt(new Uint8Array()), true, "isNullBufferDeopt(new Uint8Array()) !== true"); + +// ==== V8 ZERO LENGTH BUFFER ANOMALIES ==== + +// V8 bug: inline Uint8Array creation to fast call sees non-null pointer +// https://bugs.chromium.org/p/v8/issues/detail?id=13489 +if (Deno.build.os != "linux" || Deno.build.arch != "aarch64") { + assertEquals(isNullBuffer(new Uint8Array()), false, "isNullBuffer(new Uint8Array()) !== false"); +} + +// Externally backed ArrayBuffer has a non-null data pointer, even though its length is zero. +const externalZeroBuffer = new Uint8Array(Deno.UnsafePointerView.getArrayBuffer(ptr0, 0)); +// V8 Fast calls used to get null pointers for all zero-sized buffers no matter their external backing. +assertEquals(isNullBuffer(externalZeroBuffer), false, "isNullBuffer(externalZeroBuffer) !== false"); +// V8's `Local<ArrayBuffer>->Data()` method also used to similarly return null pointers for all +// zero-sized buffers which would not match what `Local<ArrayBuffer>->GetBackingStore()->Data()` +// API returned. These issues have been fixed in https://bugs.chromium.org/p/v8/issues/detail?id=13488. +assertEquals(isNullBufferDeopt(externalZeroBuffer), false, "isNullBufferDeopt(externalZeroBuffer) !== false"); + +// The same pointer with a non-zero byte length for the buffer will return non-null pointers in +// both Fast call and V8 API calls. +const externalOneBuffer = new Uint8Array(Deno.UnsafePointerView.getArrayBuffer(ptr0, 1)); +assertEquals(isNullBuffer(externalOneBuffer), false, "isNullBuffer(externalOneBuffer) !== false"); +assertEquals(isNullBufferDeopt(externalOneBuffer), false, "isNullBufferDeopt(externalOneBuffer) !== false"); + +// UnsafePointer.of uses an exact-pointer fallback for zero-length buffers and slices to ensure that it always gets +// the underlying pointer right. +assertNotEquals(Deno.UnsafePointer.of(externalZeroBuffer), null, "Deno.UnsafePointer.of(externalZeroBuffer) === null"); +assertNotEquals(Deno.UnsafePointer.of(externalOneBuffer), null, "Deno.UnsafePointer.of(externalOneBuffer) === null"); + +const addU32Ptr = dylib.symbols.get_add_u32_ptr(); +const addU32 = new Deno.UnsafeFnPointer(addU32Ptr, { + parameters: ["u32", "u32"], + result: "u32", +}); +console.log(addU32.call(123, 456)); + +const sleepBlockingPtr = dylib.symbols.get_sleep_blocking_ptr(); +const sleepNonBlocking = new Deno.UnsafeFnPointer(sleepBlockingPtr, { + nonblocking: true, + parameters: ["u64"], + result: "void", +}); +const before = performance.now(); +await sleepNonBlocking.call(100); +console.log(performance.now() - before >= 100); + +const { add_u32, add_usize_fast } = symbols; +function addU32Fast(a, b) { + return add_u32(a, b); +}; +testOptimized(addU32Fast, () => addU32Fast(123, 456)); + +function addU64Fast(a, b) { return add_usize_fast(a, b); }; +testOptimized(addU64Fast, () => addU64Fast(2, 3)); + +console.log(dylib.symbols.add_i32(123, 456)); +console.log(dylib.symbols.add_u64(0xffffffffn, 0xffffffffn)); +console.log(dylib.symbols.add_i64(-0xffffffffn, -0xffffffffn)); +console.log(dylib.symbols.add_usize(0xffffffffn, 0xffffffffn)); +console.log(dylib.symbols.add_isize(-0xffffffffn, -0xffffffffn)); +console.log(dylib.symbols.add_u64(Number.MAX_SAFE_INTEGER, 1)); +console.log(dylib.symbols.add_i64(Number.MAX_SAFE_INTEGER, 1)); +console.log(dylib.symbols.add_i64(Number.MIN_SAFE_INTEGER, -1)); +console.log(dylib.symbols.add_usize(Number.MAX_SAFE_INTEGER, 1)); +console.log(dylib.symbols.add_isize(Number.MAX_SAFE_INTEGER, 1)); +console.log(dylib.symbols.add_isize(Number.MIN_SAFE_INTEGER, -1)); +console.log(dylib.symbols.add_f32(123.123, 456.789)); +console.log(dylib.symbols.add_f64(123.123, 456.789)); +console.log(dylib.symbols.and(true, true)); +console.log(dylib.symbols.and(true, false)); + +function addF32Fast(a, b) { + return dylib.symbols.add_f32(a, b); +}; +testOptimized(addF32Fast, () => addF32Fast(123.123, 456.789)); + +function addF64Fast(a, b) { + return dylib.symbols.add_f64(a, b); +}; +testOptimized(addF64Fast, () => addF64Fast(123.123, 456.789)); + +// Test adders as nonblocking calls +console.log(await dylib.symbols.add_i32_nonblocking(123, 456)); +console.log(await dylib.symbols.add_u64_nonblocking(0xffffffffn, 0xffffffffn)); +console.log( + await dylib.symbols.add_i64_nonblocking(-0xffffffffn, -0xffffffffn), +); +console.log( + await dylib.symbols.add_usize_nonblocking(0xffffffffn, 0xffffffffn), +); +console.log( + await dylib.symbols.add_isize_nonblocking(-0xffffffffn, -0xffffffffn), +); +console.log(await dylib.symbols.add_u64_nonblocking(Number.MAX_SAFE_INTEGER, 1)); +console.log(await dylib.symbols.add_i64_nonblocking(Number.MAX_SAFE_INTEGER, 1)); +console.log(await dylib.symbols.add_i64_nonblocking(Number.MIN_SAFE_INTEGER, -1)); +console.log(await dylib.symbols.add_usize_nonblocking(Number.MAX_SAFE_INTEGER, 1)); +console.log(await dylib.symbols.add_isize_nonblocking(Number.MAX_SAFE_INTEGER, 1)); +console.log(await dylib.symbols.add_isize_nonblocking(Number.MIN_SAFE_INTEGER, -1)); +console.log(await dylib.symbols.add_f32_nonblocking(123.123, 456.789)); +console.log(await dylib.symbols.add_f64_nonblocking(123.123, 456.789)); + +// test mutating sync calls + +function test_fill_buffer(fillValue, arr) { + let buf = new Uint8Array(arr); + dylib.symbols.fill_buffer(fillValue, buf, buf.length); + for (let i = 0; i < buf.length; i++) { + if (buf[i] !== fillValue) { + throw new Error(`Found '${buf[i]}' in buffer, expected '${fillValue}'.`); + } + } +} + +test_fill_buffer(0, [2, 3, 4]); +test_fill_buffer(5, [2, 7, 3, 2, 1]); + +const deferred = Promise.withResolvers(); +const buffer3 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); +dylib.symbols.nonblocking_buffer(buffer3, buffer3.length).then(() => { + deferred.resolve(); +}); +await deferred.promise; + +let start = performance.now(); +dylib.symbols.sleep_blocking(100); +assert(performance.now() - start >= 100); + +start = performance.now(); +const promise_2 = dylib.symbols.sleep_nonblocking(100).then(() => { + console.log("After"); + assert(performance.now() - start >= 100); +}); +console.log("Before"); +assert(performance.now() - start < 100); + +// Await to make sure `sleep_nonblocking` calls and logs before we proceed +await promise_2; + +// Test calls with callback parameters +const logCallback = new Deno.UnsafeCallback( + { parameters: [], result: "void" }, + () => console.log("logCallback"), +); +const logManyParametersCallback = new Deno.UnsafeCallback({ + parameters: [ + "u8", + "i8", + "u16", + "i16", + "u32", + "i32", + "u64", + "i64", + "f32", + "f64", + "pointer", + ], + result: "void", +}, (u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, pointer) => { + const view = new Deno.UnsafePointerView(pointer); + const copy_buffer = new Uint8Array(8); + view.copyInto(copy_buffer); + console.log(u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, ...copy_buffer); +}); +const returnU8Callback = new Deno.UnsafeCallback( + { parameters: [], result: "u8" }, + () => 8, +); +const returnBufferCallback = new Deno.UnsafeCallback({ + parameters: [], + result: "buffer", +}, () => { + return buffer; +}); +const add10Callback = new Deno.UnsafeCallback({ + parameters: ["u8"], + result: "u8", +}, (value) => value + 10); +const throwCallback = new Deno.UnsafeCallback({ + parameters: [], + result: "void", +}, () => { + throw new TypeError("hi"); +}); + +assertThrows( + () => { + dylib.symbols.call_fn_ptr(throwCallback.pointer); + }, + TypeError, + "hi", +); + +const { call_stored_function } = dylib.symbols; + +dylib.symbols.call_fn_ptr(logCallback.pointer); +dylib.symbols.call_fn_ptr_many_parameters(logManyParametersCallback.pointer); +dylib.symbols.call_fn_ptr_return_u8(returnU8Callback.pointer); +dylib.symbols.call_fn_ptr_return_buffer(returnBufferCallback.pointer); +dylib.symbols.store_function(logCallback.pointer); +call_stored_function(); +dylib.symbols.store_function_2(add10Callback.pointer); +dylib.symbols.call_stored_function_2(20); + +function logManyParametersFast(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) { + return symbols.log_many_parameters(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s); +}; +testOptimized( + logManyParametersFast, + () => logManyParametersFast( + 255, 65535, 4294967295, 4294967296, 123.456, 789.876, -1, -2, -3, -4, -1000, 1000, + 12345.678910, 12345.678910, 12345.678910, 12345.678910, 12345.678910, 12345.678910, 12345.678910 + ) +); + +// Some ABIs rely on the convention to zero/sign-extend arguments by the caller to optimize the callee function. +// If the trampoline did not zero/sign-extend arguments, this would return 256 instead of the expected 0 (in optimized builds) +function castU8U32Fast(x) { return symbols.cast_u8_u32(x); }; +testOptimized(castU8U32Fast, () => castU8U32Fast(256)); + +// Some ABIs rely on the convention to expect garbage in the bits beyond the size of the return value to optimize the callee function. +// If the trampoline did not zero/sign-extend the return value, this would return 256 instead of the expected 0 (in optimized builds) +function castU32U8Fast(x) { return symbols.cast_u32_u8(x); }; +testOptimized(castU32U8Fast, () => castU32U8Fast(256)); + +// Generally the trampoline tail-calls into the FFI function, but in certain cases (e.g. when returning 8 or 16 bit integers) +// the tail call is not possible and a new stack frame must be created. We need enough parameters to have some on the stack +function addManyU16Fast(a, b, c, d, e, f, g, h, i, j, k, l, m) { + return symbols.add_many_u16(a, b, c, d, e, f, g, h, i, j, k, l, m); +}; +// N.B. V8 does not currently follow Aarch64 Apple's calling convention. +// The current implementation of the JIT trampoline follows the V8 incorrect calling convention. This test covers the use-case +// and is expected to fail once Deno uses a V8 version with the bug fixed. +// The V8 bug is being tracked in https://bugs.chromium.org/p/v8/issues/detail?id=13171 +testOptimized(addManyU16Fast, () => addManyU16Fast(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)); + + +const nestedCallback = new Deno.UnsafeCallback( + { parameters: [], result: "void" }, + () => { + dylib.symbols.call_stored_function_2(10); + }, +); +dylib.symbols.store_function(nestedCallback.pointer); + +dylib.symbols.store_function(null); +dylib.symbols.store_function_2(null); + +let counter = 0; +const addToFooCallback = new Deno.UnsafeCallback({ + parameters: [], + result: "void", +}, () => counter++); + +// Test thread safe callbacks +assertEquals(counter, 0); +addToFooCallback.ref(); +await dylib.symbols.call_fn_ptr_thread_safe(addToFooCallback.pointer); +addToFooCallback.unref(); +logCallback.ref(); +await dylib.symbols.call_fn_ptr_thread_safe(logCallback.pointer); +logCallback.unref(); +assertEquals(counter, 1); +returnU8Callback.ref(); +await dylib.symbols.call_fn_ptr_return_u8_thread_safe(returnU8Callback.pointer); +// Purposefully do not unref returnU8Callback: Instead use it to test close() unrefing. + +// Test statics +assertEquals(dylib.symbols.static_u32, 42); +assertEquals(dylib.symbols.static_i64, -1242464576485); +assert( + typeof dylib.symbols.static_ptr === "object" +); +assertEquals( + Object.keys(dylib.symbols.static_ptr).length, 0 +); +const view = new Deno.UnsafePointerView(dylib.symbols.static_ptr); +assertEquals(view.getUint32(), 42); + +// Test struct returning +const rect_sync = dylib.symbols.make_rect(10, 20, 100, 200); +assertInstanceOf(rect_sync, Uint8Array); +assertEquals(rect_sync.length, 4 * 8); +assertEquals(Array.from(new Float64Array(rect_sync.buffer)), [10, 20, 100, 200]); +// Test struct passing +dylib.symbols.print_rect(rect_sync); +// Test struct passing asynchronously +await dylib.symbols.print_rect_async(rect_sync); +dylib.symbols.print_rect(new Float64Array([20, 20, 100, 200])); +// Test struct returning asynchronously +const rect_async = await dylib.symbols.make_rect_async(10, 20, 100, 200); +assertInstanceOf(rect_async, Uint8Array); +assertEquals(rect_async.length, 4 * 8); +assertEquals(Array.from(new Float64Array(rect_async.buffer)), [10, 20, 100, 200]); + +// Test complex, mixed struct returning and passing +const mixedStruct = dylib.symbols.create_mixed(3, 12.515000343322754, rect_async, Deno.UnsafePointer.create(12456789), new Uint32Array([8, 32])); +assertEquals(mixedStruct.length, 56); +assertEquals(Array.from(mixedStruct.subarray(0, 4)), [3, 0, 0, 0]); +assertEquals(new Float32Array(mixedStruct.buffer, 4, 1)[0], 12.515000343322754); +assertEquals(new Float64Array(mixedStruct.buffer, 8, 4), new Float64Array(rect_async.buffer)); +assertEquals(new BigUint64Array(mixedStruct.buffer, 40, 1)[0], 12456789n); +assertEquals(new Uint32Array(mixedStruct.buffer, 48, 2), new Uint32Array([8, 32])); +dylib.symbols.print_mixed(mixedStruct); + +const cb = new Deno.UnsafeCallback({ + parameters: [{ struct: Rect }], + result: { struct: Rect }, +}, (innerRect) => { + innerRect = new Float64Array(innerRect.buffer); + return new Float64Array([innerRect[0] + 10, innerRect[1] + 10, innerRect[2] + 10, innerRect[3] + 10]); +}); + +const cbFfi = new Deno.UnsafeFnPointer(cb.pointer, cb.definition); +const cbResult = new Float64Array(cbFfi.call(rect_async).buffer); +assertEquals(Array.from(cbResult), [20, 30, 110, 210]); + +cb.close(); + +const arrayBuffer = view.getArrayBuffer(4); +const uint32Array = new Uint32Array(arrayBuffer); +assertEquals(arrayBuffer.byteLength, 4); +assertEquals(uint32Array.length, 1); +assertEquals(uint32Array[0], 42); +uint32Array[0] = 55; // MUTATES! +assertEquals(uint32Array[0], 55); +assertEquals(view.getUint32(), 55); + + +{ + // Test UnsafePointer APIs + assertEquals(Deno.UnsafePointer.create(0), null); + const createdPointer = Deno.UnsafePointer.create(1); + assertNotEquals(createdPointer, null); + assertEquals(typeof createdPointer, "object"); + assertEquals(Deno.UnsafePointer.value(null), 0); + assertEquals(Deno.UnsafePointer.value(createdPointer), 1); + assert(Deno.UnsafePointer.equals(null, null)); + assertFalse(Deno.UnsafePointer.equals(null, createdPointer)); + assertFalse(Deno.UnsafePointer.equals(Deno.UnsafePointer.create(2), createdPointer)); + // Do not allow offsetting from null, `create` function should be used instead. + assertThrows(() => Deno.UnsafePointer.offset(null, 5)); + const offsetPointer = Deno.UnsafePointer.offset(createdPointer, 5); + assertEquals(Deno.UnsafePointer.value(offsetPointer), 6); + const zeroPointer = Deno.UnsafePointer.offset(offsetPointer, -6); + assertEquals(Deno.UnsafePointer.value(zeroPointer), 0); + assertEquals(zeroPointer, null); +} + +// Test non-UTF-8 characters + +const charView = new Deno.UnsafePointerView(dylib.symbols.static_char); + +const charArrayBuffer = charView.getArrayBuffer(14); +const uint8Array = new Uint8Array(charArrayBuffer); +assertEquals([...uint8Array], [ + 0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, + 0x00 +]); + +// Check that `getCString` works equally to `TextDecoder` +assertEquals(charView.getCString(), new TextDecoder().decode(uint8Array.subarray(0, uint8Array.length - 1))); + +// Check a selection of various invalid UTF-8 sequences in C strings and verify +// that the `getCString` API does not cause unexpected behaviour. +for (const charBuffer of [ + Uint8Array.from([0xA0, 0xA1, 0x00]), + Uint8Array.from([0xE2, 0x28, 0xA1, 0x00]), + Uint8Array.from([0xE2, 0x82, 0x28, 0x00]), + Uint8Array.from([0xF0, 0x28, 0x8C, 0xBC, 0x00]), + Uint8Array.from([0xF0, 0x90, 0x28, 0xBC, 0x00]), + Uint8Array.from([0xF0, 0x28, 0x8C, 0x28, 0x00]), + Uint8Array.from([0xF8, 0xA1, 0xA1, 0xA1, 0xA1, 0x00]), + Uint8Array.from([0xFC, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0x00]), +]) { + const charBufferPointer = Deno.UnsafePointer.of(charBuffer); + const charString = Deno.UnsafePointerView.getCString(charBufferPointer); + const charBufferPointerArrayBuffer = new Uint8Array(Deno.UnsafePointerView.getArrayBuffer(charBufferPointer, charBuffer.length - 1)); + assertEquals(charString, new TextDecoder().decode(charBufferPointerArrayBuffer)); + assertEquals([...charBuffer.subarray(0, charBuffer.length - 1)], [...charBufferPointerArrayBuffer]); +} + + +const bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); +function hash() { return dylib.symbols.hash(bytes, bytes.byteLength); }; + +testOptimized(hash, () => hash()); + +(function cleanup() { + dylib.close(); + throwCallback.close(); + logCallback.close(); + logManyParametersCallback.close(); + returnU8Callback.close(); + returnBufferCallback.close(); + add10Callback.close(); + nestedCallback.close(); + addToFooCallback.close(); + + const resourcesPost = Deno[Deno.internal].core.resources(); + + const preStr = JSON.stringify(resourcesPre, null, 2); + const postStr = JSON.stringify(resourcesPost, null, 2); + if (preStr !== postStr) { + throw new Error( + `Difference in open resources before dlopen and after closing: +Before: ${preStr} +After: ${postStr}`, + ); + } + + console.log("Correct number of resources"); +})(); + +function assertIsOptimized(fn) { + const status = %GetOptimizationStatus(fn); + assert(status & (1 << 4), `expected ${fn.name} to be optimized, but wasn't`); +} + +function testOptimized(fn, callback) { + %PrepareFunctionForOptimization(fn); + const r1 = callback(); + if (r1 !== undefined) { + console.log(r1); + } + %OptimizeFunctionOnNextCall(fn); + const r2 = callback(); + if (r2 !== undefined) { + console.log(r2); + } + assertIsOptimized(fn); +} diff --git a/tests/ffi/tests/thread_safe_test.js b/tests/ffi/tests/thread_safe_test.js new file mode 100644 index 000000000..fffa61a04 --- /dev/null +++ b/tests/ffi/tests/thread_safe_test.js @@ -0,0 +1,105 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +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: { + parameters: ["function"], + result: "void", + }, + call_stored_function: { + parameters: [], + result: "void", + }, + call_stored_function_thread_safe: { + parameters: [], + result: "void", + }, +}); + +let resolveWorker; +let workerResponsePromise; + +const worker = new Worker( + new URL("./thread_safe_test_worker.js", import.meta.url).href, + { type: "module" }, +); + +worker.addEventListener("message", () => { + if (resolveWorker) { + resolveWorker(); + } +}); + +const sendWorkerMessage = async (data) => { + workerResponsePromise = new Promise((res) => { + resolveWorker = res; + }); + worker.postMessage(data); + await workerResponsePromise; +}; + +// Test step 1: Register main thread callback, trigger on worker thread + +const mainThreadCallback = new Deno.UnsafeCallback( + { parameters: [], result: "void" }, + () => { + console.log("Callback on main thread"); + }, +); + +mainThreadCallback.ref(); + +dylib.symbols.store_function(mainThreadCallback.pointer); + +await sendWorkerMessage("call"); + +// Test step 2: Register on worker thread, trigger on main thread + +await sendWorkerMessage("register"); + +dylib.symbols.call_stored_function(); + +// Unref both main and worker thread callbacks and terminate the worker: Note, the stored function pointer in lib is now dangling. + +dylib.symbols.store_function(null); + +mainThreadCallback.unref(); +await sendWorkerMessage("unref"); +worker.terminate(); + +// Test step 3: Register a callback that will be the only thing left keeping the isolate from exiting. +// Rely on it to keep Deno running until the callback comes in and unrefs the callback, after which Deno should exit. + +const cleanupCallback = new Deno.UnsafeCallback( + { parameters: [], result: "void" }, + () => { + console.log("Callback being called"); + // Defer the cleanup to give the spawned thread all the time it needs to properly shut down + setTimeout(() => cleanup(), 100); + }, +); + +cleanupCallback.ref(); + +function cleanup() { + cleanupCallback.unref(); + dylib.symbols.store_function(null); + mainThreadCallback.close(); + cleanupCallback.close(); + console.log("Isolate should now exit"); +} + +dylib.symbols.store_function(cleanupCallback.pointer); + +console.log( + "Calling callback, isolate should stay asleep until callback is called", +); +dylib.symbols.call_stored_function_thread_safe(); diff --git a/tests/ffi/tests/thread_safe_test_worker.js b/tests/ffi/tests/thread_safe_test_worker.js new file mode 100644 index 000000000..fc4854436 --- /dev/null +++ b/tests/ffi/tests/thread_safe_test_worker.js @@ -0,0 +1,41 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +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: { + parameters: ["function"], + result: "void", + }, + call_stored_function: { + parameters: [], + result: "void", + }, +}); + +const callback = new Deno.UnsafeCallback( + { parameters: [], result: "void" }, + () => { + console.log("Callback on worker thread"); + }, +); + +callback.ref(); + +self.addEventListener("message", ({ data }) => { + if (data === "register") { + dylib.symbols.store_function(callback.pointer); + } else if (data === "call") { + dylib.symbols.call_stored_function(); + } else if (data === "unref") { + callback.close(); + } + self.postMessage("done"); +}); |