2024-01-01 14:58:21 -05:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2021-08-06 17:28:10 -04:00
|
|
|
// deno-lint-ignore-file
|
|
|
|
|
2023-02-22 12:32:38 -05:00
|
|
|
// Run using cargo test or `--v8-flags=--allow-natives-syntax`
|
2022-07-11 21:03:05 -04:00
|
|
|
|
2022-07-23 13:11:06 -04:00
|
|
|
import {
|
|
|
|
assertThrows,
|
2022-09-07 02:53:56 -04:00
|
|
|
assert,
|
2023-02-22 12:32:38 -05:00
|
|
|
assertNotEquals,
|
|
|
|
assertInstanceOf,
|
|
|
|
assertEquals,
|
|
|
|
assertFalse,
|
2023-12-01 21:20:06 -05:00
|
|
|
} from "../../test_util/std/assert/mod.ts";
|
2022-01-05 02:25:31 -05:00
|
|
|
|
2021-08-06 17:28:10 -04:00
|
|
|
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}`;
|
|
|
|
|
2024-01-23 18:27:29 -05:00
|
|
|
const resourcesPre = Deno[Deno.internal].core.resources();
|
2021-10-07 11:03:00 -04:00
|
|
|
|
|
|
|
// dlopen shouldn't panic
|
2022-01-05 02:25:31 -05:00
|
|
|
assertThrows(() => {
|
2021-10-07 11:03:00 -04:00
|
|
|
Deno.dlopen("cli/src/main.rs", {});
|
2022-01-05 02:25:31 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
assertThrows(
|
|
|
|
() => {
|
|
|
|
Deno.dlopen(libPath, {
|
|
|
|
non_existent_symbol: {
|
|
|
|
parameters: [],
|
|
|
|
result: "void",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
Error,
|
|
|
|
"Failed to register symbol non_existent_symbol",
|
|
|
|
);
|
2021-10-07 11:03:00 -04:00
|
|
|
|
2023-01-21 10:51:14 -05:00
|
|
|
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"
|
|
|
|
});
|
|
|
|
|
2023-01-07 22:58:10 -05:00
|
|
|
const Point = ["f64", "f64"];
|
|
|
|
const Size = ["f64", "f64"];
|
|
|
|
const Rect = ["f64", "f64", "f64", "f64"];
|
|
|
|
const RectNested = [{ struct: Point }, { struct: Size }];
|
2023-03-31 23:56:02 -04:00
|
|
|
const RectNestedCached = [{ struct: Size }, { struct: Size }];
|
2023-01-07 22:58:10 -05:00
|
|
|
const Mixed = ["u8", "f32", { struct: Rect }, "usize", { struct: ["u32", "u32"] }];
|
|
|
|
|
2021-08-06 17:28:10 -04:00
|
|
|
const dylib = Deno.dlopen(libPath, {
|
2022-01-11 01:21:16 -05:00
|
|
|
"printSomething": {
|
|
|
|
name: "print_something",
|
|
|
|
parameters: [],
|
|
|
|
result: "void",
|
|
|
|
},
|
2022-08-22 23:46:43 -04:00
|
|
|
"print_buffer": { parameters: ["buffer", "usize"], result: "void" },
|
|
|
|
"print_pointer": { name: "print_buffer", parameters: ["pointer", "usize"], result: "void" },
|
2021-10-10 09:18:02 -04:00
|
|
|
"print_buffer2": {
|
2022-08-22 23:46:43 -04:00
|
|
|
parameters: ["buffer", "usize", "buffer", "usize"],
|
2021-10-10 09:18:02 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
2022-08-22 23:46:43 -04:00
|
|
|
"return_buffer": { parameters: [], result: "buffer" },
|
fix(ext/ffi): Null buffer pointer value is inconsistent (#16625)
Currently, slow call path will always create a dangling pointer to
replace a null pointer when called with eg. a `new Uint8Array()`
parameter, which V8 initialises as a null pointer backed buffer.
However, the fast call path will never change the pointer value and will
thus expose a null pointer. Thus, it's possible that the pointer value
that a native call sees coming from Deno changes between two sequential
invocations of the same function with the exact same parameters.
Since null pointers can be quite important, and `Uint8Array` is the
chosen fast path for Deno FFI `"buffer"` parameters, I think it is
fairly important that the null pointer be properly exposed to the native
code. Thus this PR.
### `*mut c_void`
While here, I also changed the type of our pointer values to `*mut
c_void`. This is mainly due to JS buffers always being `*mut`, and
because we offer a way to turn a pointer into a JS `ArrayBuffer`
(`op_ffi_get_buf`) which is read-write. I'm not exactly sure which way
we should really go here, we have pointers that are definitely mut but
we also cannot assume all of our pointers are. So, do we go with the
maxima or the minima?
### `optimisedCall(new Uint8Array())`
V8 seems to have a bug where calling an optimised function with a newly
created empty `Uint8Array` (no argument or 0) will not see the data
pointer being null but instead it's some stable pointer, perhaps
pointing to some internal null-backing-store. The pointer value is also
an odd (not even) number, so it might specifically be a tagged pointer.
This will probably be an issue for some users, if they try to use eg.
`method(cstr("something"), new Uint8Array())` as a way to do a fast call
to `method` with a null pointer as the second parameter.
If instead of a `new Uint8Array()` the user instead uses some `const
NULL = new Uint8Array()` where the `NULL` buffer has been passed to a
slow call previously, then the fast call will properly see a null
pointer.
I'll take this up with some V8 engineers to see if this couldn't be
fixed.
2022-11-27 09:38:54 -05:00
|
|
|
"is_null_ptr": { parameters: ["pointer"], result: "bool" },
|
|
|
|
"is_null_buf": { name: "is_null_ptr", parameters: ["buffer"], result: "bool" },
|
2021-09-20 14:38:28 -04:00
|
|
|
"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" },
|
2022-07-11 22:50:20 -04:00
|
|
|
"add_usize_fast": { parameters: ["usize", "usize"], result: "u32" },
|
2021-09-20 14:38:28 -04:00
|
|
|
"add_isize": { parameters: ["isize", "isize"], result: "isize" },
|
|
|
|
"add_f32": { parameters: ["f32", "f32"], result: "f32" },
|
|
|
|
"add_f64": { parameters: ["f64", "f64"], result: "f64" },
|
2022-09-04 23:26:52 -04:00
|
|
|
"and": { parameters: ["bool", "bool"], result: "bool" },
|
2022-06-20 07:06:04 -04:00
|
|
|
"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,
|
|
|
|
},
|
2022-08-22 23:46:43 -04:00
|
|
|
"fill_buffer": { parameters: ["u8", "buffer", "usize"], result: "void" },
|
2022-01-11 01:21:16 -05:00
|
|
|
"sleep_nonblocking": {
|
|
|
|
name: "sleep_blocking",
|
|
|
|
parameters: ["u64"],
|
|
|
|
result: "void",
|
|
|
|
nonblocking: true,
|
|
|
|
},
|
|
|
|
"sleep_blocking": { parameters: ["u64"], result: "void" },
|
2021-10-05 18:27:05 -04:00
|
|
|
"nonblocking_buffer": {
|
2022-08-22 23:46:43 -04:00
|
|
|
parameters: ["buffer", "usize"],
|
2021-10-05 18:27:05 -04:00
|
|
|
result: "void",
|
|
|
|
nonblocking: true,
|
|
|
|
},
|
2022-01-12 06:38:26 -05:00
|
|
|
"get_add_u32_ptr": {
|
|
|
|
parameters: [],
|
|
|
|
result: "pointer",
|
|
|
|
},
|
|
|
|
"get_sleep_blocking_ptr": {
|
|
|
|
parameters: [],
|
|
|
|
result: "pointer",
|
|
|
|
},
|
2022-06-20 07:06:04 -04:00
|
|
|
// Callback function
|
|
|
|
call_fn_ptr: {
|
2022-06-20 22:50:33 -04:00
|
|
|
parameters: ["function"],
|
2022-06-20 07:06:04 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
2022-06-28 05:23:36 -04:00
|
|
|
call_fn_ptr_thread_safe: {
|
|
|
|
name: "call_fn_ptr",
|
|
|
|
parameters: ["function"],
|
|
|
|
result: "void",
|
|
|
|
nonblocking: true,
|
|
|
|
},
|
2022-06-20 07:06:04 -04:00
|
|
|
call_fn_ptr_many_parameters: {
|
2022-06-20 22:50:33 -04:00
|
|
|
parameters: ["function"],
|
2022-06-20 07:06:04 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
|
|
|
call_fn_ptr_return_u8: {
|
2022-06-20 22:50:33 -04:00
|
|
|
parameters: ["function"],
|
2022-06-20 07:06:04 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
2022-06-28 05:23:36 -04:00
|
|
|
call_fn_ptr_return_u8_thread_safe: {
|
|
|
|
name: "call_fn_ptr_return_u8",
|
|
|
|
parameters: ["function"],
|
|
|
|
result: "void",
|
|
|
|
},
|
2022-06-20 07:06:04 -04:00
|
|
|
call_fn_ptr_return_buffer: {
|
2022-06-20 22:50:33 -04:00
|
|
|
parameters: ["function"],
|
2022-06-20 07:06:04 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
|
|
|
store_function: {
|
2022-06-20 22:50:33 -04:00
|
|
|
parameters: ["function"],
|
2022-06-20 07:06:04 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
|
|
|
store_function_2: {
|
2022-06-20 22:50:33 -04:00
|
|
|
parameters: ["function"],
|
2022-06-20 07:06:04 -04:00
|
|
|
result: "void",
|
|
|
|
},
|
|
|
|
call_stored_function: {
|
|
|
|
parameters: [],
|
|
|
|
result: "void",
|
2022-07-09 09:11:07 -04:00
|
|
|
callback: true,
|
2022-06-20 07:06:04 -04:00
|
|
|
},
|
|
|
|
call_stored_function_2: {
|
|
|
|
parameters: ["u8"],
|
|
|
|
result: "void",
|
2022-07-09 09:11:07 -04:00
|
|
|
callback: true,
|
2022-06-20 07:06:04 -04:00
|
|
|
},
|
2022-09-07 02:53:56 -04:00
|
|
|
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",
|
|
|
|
},
|
2022-06-20 07:06:04 -04:00
|
|
|
// Statics
|
2022-02-18 07:21:19 -05:00
|
|
|
"static_u32": {
|
|
|
|
type: "u32",
|
|
|
|
},
|
2022-06-20 07:06:04 -04:00
|
|
|
"static_i64": {
|
|
|
|
type: "i64",
|
|
|
|
},
|
2022-02-18 07:21:19 -05:00
|
|
|
"static_ptr": {
|
|
|
|
type: "pointer",
|
|
|
|
},
|
2022-08-05 12:26:54 -04:00
|
|
|
/**
|
|
|
|
* Invalid UTF-8 characters, buffer of length 14
|
|
|
|
*/
|
|
|
|
"static_char": {
|
|
|
|
type: "pointer",
|
2023-04-03 14:32:21 -04:00
|
|
|
optional: true,
|
2022-08-05 12:26:54 -04:00
|
|
|
},
|
2022-09-07 02:53:56 -04:00
|
|
|
"hash": { parameters: ["buffer", "u32"], result: "u32" },
|
2023-01-07 22:58:10 -05:00
|
|
|
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: {
|
2023-03-31 23:56:02 -04:00
|
|
|
parameters: [{ struct: RectNestedCached }],
|
2023-01-07 22:58:10 -05:00
|
|
|
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",
|
2023-04-03 14:32:21 -04:00
|
|
|
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,
|
2023-01-07 22:58:10 -05:00
|
|
|
},
|
2021-08-06 17:28:10 -04:00
|
|
|
});
|
2022-07-11 21:03:05 -04:00
|
|
|
const { symbols } = dylib;
|
2021-08-06 17:28:10 -04:00
|
|
|
|
2022-07-11 21:03:05 -04:00
|
|
|
symbols.printSomething();
|
2021-10-05 18:27:05 -04:00
|
|
|
const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
2021-10-10 09:18:02 -04:00
|
|
|
const buffer2 = new Uint8Array([9, 10]);
|
2021-10-05 18:27:05 -04:00
|
|
|
dylib.symbols.print_buffer(buffer, buffer.length);
|
2022-06-20 23:46:59 -04:00
|
|
|
// Test subarrays
|
|
|
|
const subarray = buffer.subarray(3);
|
|
|
|
dylib.symbols.print_buffer(subarray, subarray.length - 2);
|
2021-10-10 09:18:02 -04:00
|
|
|
dylib.symbols.print_buffer2(buffer, buffer.length, buffer2, buffer2.length);
|
2022-07-28 08:38:22 -04:00
|
|
|
|
|
|
|
const { return_buffer } = symbols;
|
|
|
|
function returnBuffer() { return return_buffer(); };
|
|
|
|
|
|
|
|
%PrepareFunctionForOptimization(returnBuffer);
|
|
|
|
returnBuffer();
|
|
|
|
%OptimizeFunctionOnNextCall(returnBuffer);
|
|
|
|
const ptr0 = returnBuffer();
|
2022-09-07 02:53:56 -04:00
|
|
|
assertIsOptimized(returnBuffer);
|
2022-07-28 08:38:22 -04:00
|
|
|
|
2022-08-22 23:46:43 -04:00
|
|
|
dylib.symbols.print_pointer(ptr0, 8);
|
2022-06-20 09:38:10 -04:00
|
|
|
const ptrView = new Deno.UnsafePointerView(ptr0);
|
2021-12-15 09:41:49 -05:00
|
|
|
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);
|
2023-10-28 03:02:57 -04:00
|
|
|
const into4 = new Uint16Array(3);
|
|
|
|
ptrView.copyInto(into4);
|
2021-12-15 09:41:49 -05:00
|
|
|
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));
|
2023-10-05 09:35:21 -04:00
|
|
|
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)));
|
fix(ext/ffi): Null buffer pointer value is inconsistent (#16625)
Currently, slow call path will always create a dangling pointer to
replace a null pointer when called with eg. a `new Uint8Array()`
parameter, which V8 initialises as a null pointer backed buffer.
However, the fast call path will never change the pointer value and will
thus expose a null pointer. Thus, it's possible that the pointer value
that a native call sees coming from Deno changes between two sequential
invocations of the same function with the exact same parameters.
Since null pointers can be quite important, and `Uint8Array` is the
chosen fast path for Deno FFI `"buffer"` parameters, I think it is
fairly important that the null pointer be properly exposed to the native
code. Thus this PR.
### `*mut c_void`
While here, I also changed the type of our pointer values to `*mut
c_void`. This is mainly due to JS buffers always being `*mut`, and
because we offer a way to turn a pointer into a JS `ArrayBuffer`
(`op_ffi_get_buf`) which is read-write. I'm not exactly sure which way
we should really go here, we have pointers that are definitely mut but
we also cannot assume all of our pointers are. So, do we go with the
maxima or the minima?
### `optimisedCall(new Uint8Array())`
V8 seems to have a bug where calling an optimised function with a newly
created empty `Uint8Array` (no argument or 0) will not see the data
pointer being null but instead it's some stable pointer, perhaps
pointing to some internal null-backing-store. The pointer value is also
an odd (not even) number, so it might specifically be a tagged pointer.
This will probably be an issue for some users, if they try to use eg.
`method(cstr("something"), new Uint8Array())` as a way to do a fast call
to `method` with a null pointer as the second parameter.
If instead of a `new Uint8Array()` the user instead uses some `const
NULL = new Uint8Array()` where the `NULL` buffer has been passed to a
slow call previously, then the fast call will properly see a null
pointer.
I'll take this up with some V8 engineers to see if this couldn't be
fixed.
2022-11-27 09:38:54 -05:00
|
|
|
const emptyBuffer = new Uint8Array(0);
|
2023-10-05 09:35:21 -04:00
|
|
|
console.log("true", dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(emptyBuffer)));
|
2022-06-29 11:00:29 -04:00
|
|
|
const emptySlice = into.subarray(6);
|
2023-10-05 09:35:21 -04:00
|
|
|
console.log("false", dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(emptySlice)));
|
fix(ext/ffi): Null buffer pointer value is inconsistent (#16625)
Currently, slow call path will always create a dangling pointer to
replace a null pointer when called with eg. a `new Uint8Array()`
parameter, which V8 initialises as a null pointer backed buffer.
However, the fast call path will never change the pointer value and will
thus expose a null pointer. Thus, it's possible that the pointer value
that a native call sees coming from Deno changes between two sequential
invocations of the same function with the exact same parameters.
Since null pointers can be quite important, and `Uint8Array` is the
chosen fast path for Deno FFI `"buffer"` parameters, I think it is
fairly important that the null pointer be properly exposed to the native
code. Thus this PR.
### `*mut c_void`
While here, I also changed the type of our pointer values to `*mut
c_void`. This is mainly due to JS buffers always being `*mut`, and
because we offer a way to turn a pointer into a JS `ArrayBuffer`
(`op_ffi_get_buf`) which is read-write. I'm not exactly sure which way
we should really go here, we have pointers that are definitely mut but
we also cannot assume all of our pointers are. So, do we go with the
maxima or the minima?
### `optimisedCall(new Uint8Array())`
V8 seems to have a bug where calling an optimised function with a newly
created empty `Uint8Array` (no argument or 0) will not see the data
pointer being null but instead it's some stable pointer, perhaps
pointing to some internal null-backing-store. The pointer value is also
an odd (not even) number, so it might specifically be a tagged pointer.
This will probably be an issue for some users, if they try to use eg.
`method(cstr("something"), new Uint8Array())` as a way to do a fast call
to `method` with a null pointer as the second parameter.
If instead of a `new Uint8Array()` the user instead uses some `const
NULL = new Uint8Array()` where the `NULL` buffer has been passed to a
slow call previously, then the fast call will properly see a null
pointer.
I'll take this up with some V8 engineers to see if this couldn't be
fixed.
2022-11-27 09:38:54 -05:00
|
|
|
|
|
|
|
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
|
2024-02-07 11:06:33 -05:00
|
|
|
if (Deno.build.os != "linux" || Deno.build.arch != "aarch64") {
|
|
|
|
assertEquals(isNullBuffer(new Uint8Array()), false, "isNullBuffer(new Uint8Array()) !== false");
|
|
|
|
}
|
fix(ext/ffi): Null buffer pointer value is inconsistent (#16625)
Currently, slow call path will always create a dangling pointer to
replace a null pointer when called with eg. a `new Uint8Array()`
parameter, which V8 initialises as a null pointer backed buffer.
However, the fast call path will never change the pointer value and will
thus expose a null pointer. Thus, it's possible that the pointer value
that a native call sees coming from Deno changes between two sequential
invocations of the same function with the exact same parameters.
Since null pointers can be quite important, and `Uint8Array` is the
chosen fast path for Deno FFI `"buffer"` parameters, I think it is
fairly important that the null pointer be properly exposed to the native
code. Thus this PR.
### `*mut c_void`
While here, I also changed the type of our pointer values to `*mut
c_void`. This is mainly due to JS buffers always being `*mut`, and
because we offer a way to turn a pointer into a JS `ArrayBuffer`
(`op_ffi_get_buf`) which is read-write. I'm not exactly sure which way
we should really go here, we have pointers that are definitely mut but
we also cannot assume all of our pointers are. So, do we go with the
maxima or the minima?
### `optimisedCall(new Uint8Array())`
V8 seems to have a bug where calling an optimised function with a newly
created empty `Uint8Array` (no argument or 0) will not see the data
pointer being null but instead it's some stable pointer, perhaps
pointing to some internal null-backing-store. The pointer value is also
an odd (not even) number, so it might specifically be a tagged pointer.
This will probably be an issue for some users, if they try to use eg.
`method(cstr("something"), new Uint8Array())` as a way to do a fast call
to `method` with a null pointer as the second parameter.
If instead of a `new Uint8Array()` the user instead uses some `const
NULL = new Uint8Array()` where the `NULL` buffer has been passed to a
slow call previously, then the fast call will properly see a null
pointer.
I'll take this up with some V8 engineers to see if this couldn't be
fixed.
2022-11-27 09:38:54 -05:00
|
|
|
|
|
|
|
// Externally backed ArrayBuffer has a non-null data pointer, even though its length is zero.
|
|
|
|
const externalZeroBuffer = new Uint8Array(Deno.UnsafePointerView.getArrayBuffer(ptr0, 0));
|
2023-11-01 15:25:09 -04:00
|
|
|
// 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");
|
fix(ext/ffi): Null buffer pointer value is inconsistent (#16625)
Currently, slow call path will always create a dangling pointer to
replace a null pointer when called with eg. a `new Uint8Array()`
parameter, which V8 initialises as a null pointer backed buffer.
However, the fast call path will never change the pointer value and will
thus expose a null pointer. Thus, it's possible that the pointer value
that a native call sees coming from Deno changes between two sequential
invocations of the same function with the exact same parameters.
Since null pointers can be quite important, and `Uint8Array` is the
chosen fast path for Deno FFI `"buffer"` parameters, I think it is
fairly important that the null pointer be properly exposed to the native
code. Thus this PR.
### `*mut c_void`
While here, I also changed the type of our pointer values to `*mut
c_void`. This is mainly due to JS buffers always being `*mut`, and
because we offer a way to turn a pointer into a JS `ArrayBuffer`
(`op_ffi_get_buf`) which is read-write. I'm not exactly sure which way
we should really go here, we have pointers that are definitely mut but
we also cannot assume all of our pointers are. So, do we go with the
maxima or the minima?
### `optimisedCall(new Uint8Array())`
V8 seems to have a bug where calling an optimised function with a newly
created empty `Uint8Array` (no argument or 0) will not see the data
pointer being null but instead it's some stable pointer, perhaps
pointing to some internal null-backing-store. The pointer value is also
an odd (not even) number, so it might specifically be a tagged pointer.
This will probably be an issue for some users, if they try to use eg.
`method(cstr("something"), new Uint8Array())` as a way to do a fast call
to `method` with a null pointer as the second parameter.
If instead of a `new Uint8Array()` the user instead uses some `const
NULL = new Uint8Array()` where the `NULL` buffer has been passed to a
slow call previously, then the fast call will properly see a null
pointer.
I'll take this up with some V8 engineers to see if this couldn't be
fixed.
2022-11-27 09:38:54 -05:00
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
2023-10-05 09:35:21 -04:00
|
|
|
// 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");
|
2023-02-22 12:32:38 -05:00
|
|
|
assertNotEquals(Deno.UnsafePointer.of(externalOneBuffer), null, "Deno.UnsafePointer.of(externalOneBuffer) === null");
|
2022-01-12 06:38:26 -05:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2022-07-11 22:50:20 -04:00
|
|
|
const { add_u32, add_usize_fast } = symbols;
|
2022-07-11 21:03:05 -04:00
|
|
|
function addU32Fast(a, b) {
|
|
|
|
return add_u32(a, b);
|
|
|
|
};
|
2022-09-07 02:53:56 -04:00
|
|
|
testOptimized(addU32Fast, () => addU32Fast(123, 456));
|
2022-06-20 09:38:10 -04:00
|
|
|
|
2022-07-11 22:50:20 -04:00
|
|
|
function addU64Fast(a, b) { return add_usize_fast(a, b); };
|
2022-09-07 02:53:56 -04:00
|
|
|
testOptimized(addU64Fast, () => addU64Fast(2, 3));
|
2022-07-11 22:50:20 -04:00
|
|
|
|
2021-09-20 14:38:28 -04:00
|
|
|
console.log(dylib.symbols.add_i32(123, 456));
|
2022-06-08 07:13:10 -04:00
|
|
|
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));
|
2022-07-24 06:41:11 -04:00
|
|
|
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));
|
2021-09-20 14:38:28 -04:00
|
|
|
console.log(dylib.symbols.add_f32(123.123, 456.789));
|
|
|
|
console.log(dylib.symbols.add_f64(123.123, 456.789));
|
2022-09-04 23:26:52 -04:00
|
|
|
console.log(dylib.symbols.and(true, true));
|
|
|
|
console.log(dylib.symbols.and(true, false));
|
2021-08-06 17:28:10 -04:00
|
|
|
|
2022-09-07 02:53:56 -04:00
|
|
|
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));
|
|
|
|
|
2022-06-20 07:06:04 -04:00
|
|
|
// 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),
|
|
|
|
);
|
2022-07-24 06:41:11 -04:00
|
|
|
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));
|
2022-06-20 07:06:04 -04:00
|
|
|
console.log(await dylib.symbols.add_f32_nonblocking(123.123, 456.789));
|
|
|
|
console.log(await dylib.symbols.add_f64_nonblocking(123.123, 456.789));
|
|
|
|
|
2021-11-10 08:55:46 -05:00
|
|
|
// 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]);
|
|
|
|
|
2023-11-22 06:11:20 -05:00
|
|
|
const deferred = Promise.withResolvers();
|
2021-10-10 09:18:02 -04:00
|
|
|
const buffer3 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
|
|
dylib.symbols.nonblocking_buffer(buffer3, buffer3.length).then(() => {
|
2023-11-22 06:11:20 -05:00
|
|
|
deferred.resolve();
|
2021-10-05 18:27:05 -04:00
|
|
|
});
|
2023-11-22 06:11:20 -05:00
|
|
|
await deferred.promise;
|
2021-10-05 18:27:05 -04:00
|
|
|
|
2022-01-11 01:21:16 -05:00
|
|
|
let start = performance.now();
|
|
|
|
dylib.symbols.sleep_blocking(100);
|
2023-02-22 12:32:38 -05:00
|
|
|
assert(performance.now() - start >= 100);
|
2022-01-11 01:21:16 -05:00
|
|
|
|
|
|
|
start = performance.now();
|
2022-06-28 05:23:36 -04:00
|
|
|
const promise_2 = dylib.symbols.sleep_nonblocking(100).then(() => {
|
2021-10-05 08:50:00 -04:00
|
|
|
console.log("After");
|
2023-02-22 12:32:38 -05:00
|
|
|
assert(performance.now() - start >= 100);
|
2021-10-05 08:50:00 -04:00
|
|
|
});
|
|
|
|
console.log("Before");
|
2023-02-22 12:32:38 -05:00
|
|
|
assert(performance.now() - start < 100);
|
2021-10-05 08:50:00 -04:00
|
|
|
|
2022-06-28 05:23:36 -04:00
|
|
|
// Await to make sure `sleep_nonblocking` calls and logs before we proceed
|
|
|
|
await promise_2;
|
|
|
|
|
2022-06-20 07:06:04 -04:00
|
|
|
// 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) => {
|
2022-06-20 09:38:10 -04:00
|
|
|
const view = new Deno.UnsafePointerView(pointer);
|
2022-06-20 07:06:04 -04:00
|
|
|
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: [],
|
2023-02-22 12:32:38 -05:00
|
|
|
result: "buffer",
|
2022-06-20 07:06:04 -04:00
|
|
|
}, () => {
|
|
|
|
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(
|
|
|
|
() => {
|
2022-08-11 09:56:56 -04:00
|
|
|
dylib.symbols.call_fn_ptr(throwCallback.pointer);
|
2022-06-20 07:06:04 -04:00
|
|
|
},
|
|
|
|
TypeError,
|
|
|
|
"hi",
|
|
|
|
);
|
|
|
|
|
2022-07-09 09:11:07 -04:00
|
|
|
const { call_stored_function } = dylib.symbols;
|
|
|
|
|
2022-08-11 09:56:56 -04:00
|
|
|
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);
|
2022-07-09 09:11:07 -04:00
|
|
|
call_stored_function();
|
2022-08-11 09:56:56 -04:00
|
|
|
dylib.symbols.store_function_2(add10Callback.pointer);
|
2022-06-20 07:06:04 -04:00
|
|
|
dylib.symbols.call_stored_function_2(20);
|
|
|
|
|
2022-09-07 02:53:56 -04:00
|
|
|
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
|
2023-01-02 16:00:42 -05:00
|
|
|
function addManyU16Fast(a, b, c, d, e, f, g, h, i, j, k, l, m) {
|
2022-09-07 02:53:56 -04:00
|
|
|
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));
|
|
|
|
|
|
|
|
|
2022-06-20 07:06:04 -04:00
|
|
|
const nestedCallback = new Deno.UnsafeCallback(
|
|
|
|
{ parameters: [], result: "void" },
|
|
|
|
() => {
|
|
|
|
dylib.symbols.call_stored_function_2(10);
|
|
|
|
},
|
|
|
|
);
|
2022-08-11 09:56:56 -04:00
|
|
|
dylib.symbols.store_function(nestedCallback.pointer);
|
2022-06-20 07:06:04 -04:00
|
|
|
|
|
|
|
dylib.symbols.store_function(null);
|
|
|
|
dylib.symbols.store_function_2(null);
|
|
|
|
|
2022-06-28 05:23:36 -04:00
|
|
|
let counter = 0;
|
|
|
|
const addToFooCallback = new Deno.UnsafeCallback({
|
|
|
|
parameters: [],
|
|
|
|
result: "void",
|
|
|
|
}, () => counter++);
|
|
|
|
|
|
|
|
// Test thread safe callbacks
|
2023-02-22 12:32:38 -05:00
|
|
|
assertEquals(counter, 0);
|
2022-06-28 05:23:36 -04:00
|
|
|
addToFooCallback.ref();
|
2022-08-11 09:56:56 -04:00
|
|
|
await dylib.symbols.call_fn_ptr_thread_safe(addToFooCallback.pointer);
|
2022-06-28 05:23:36 -04:00
|
|
|
addToFooCallback.unref();
|
|
|
|
logCallback.ref();
|
2022-08-11 09:56:56 -04:00
|
|
|
await dylib.symbols.call_fn_ptr_thread_safe(logCallback.pointer);
|
2022-06-28 05:23:36 -04:00
|
|
|
logCallback.unref();
|
2023-02-22 12:32:38 -05:00
|
|
|
assertEquals(counter, 1);
|
2022-06-28 05:23:36 -04:00
|
|
|
returnU8Callback.ref();
|
2022-08-11 09:56:56 -04:00
|
|
|
await dylib.symbols.call_fn_ptr_return_u8_thread_safe(returnU8Callback.pointer);
|
2022-10-15 09:49:46 -04:00
|
|
|
// Purposefully do not unref returnU8Callback: Instead use it to test close() unrefing.
|
2022-06-28 05:23:36 -04:00
|
|
|
|
2022-06-20 07:06:04 -04:00
|
|
|
// Test statics
|
2023-02-22 12:32:38 -05:00
|
|
|
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
|
2022-02-18 07:21:19 -05:00
|
|
|
);
|
|
|
|
const view = new Deno.UnsafePointerView(dylib.symbols.static_ptr);
|
2023-02-22 12:32:38 -05:00
|
|
|
assertEquals(view.getUint32(), 42);
|
2022-02-18 07:21:19 -05:00
|
|
|
|
2023-01-07 22:58:10 -05:00
|
|
|
// 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
|
2023-02-22 12:32:38 -05:00
|
|
|
const mixedStruct = dylib.symbols.create_mixed(3, 12.515000343322754, rect_async, Deno.UnsafePointer.create(12456789), new Uint32Array([8, 32]));
|
2023-01-07 22:58:10 -05:00
|
|
|
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();
|
|
|
|
|
2022-07-23 13:11:06 -04:00
|
|
|
const arrayBuffer = view.getArrayBuffer(4);
|
|
|
|
const uint32Array = new Uint32Array(arrayBuffer);
|
2023-02-22 12:32:38 -05:00
|
|
|
assertEquals(arrayBuffer.byteLength, 4);
|
|
|
|
assertEquals(uint32Array.length, 1);
|
|
|
|
assertEquals(uint32Array[0], 42);
|
2022-07-23 13:11:06 -04:00
|
|
|
uint32Array[0] = 55; // MUTATES!
|
2023-02-22 12:32:38 -05:00
|
|
|
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);
|
2023-10-05 09:35:21 -04:00
|
|
|
const zeroPointer = Deno.UnsafePointer.offset(offsetPointer, -6);
|
|
|
|
assertEquals(Deno.UnsafePointer.value(zeroPointer), 0);
|
|
|
|
assertEquals(zeroPointer, null);
|
2023-02-22 12:32:38 -05:00
|
|
|
}
|
2022-07-23 13:11:06 -04:00
|
|
|
|
2022-08-05 12:26:54 -04:00
|
|
|
// 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
|
|
|
|
]);
|
|
|
|
|
2023-02-12 11:42:35 -05:00
|
|
|
// 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]);
|
2022-08-05 12:26:54 -04:00
|
|
|
}
|
|
|
|
|
2022-09-07 02:53:56 -04:00
|
|
|
|
|
|
|
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());
|
|
|
|
|
2022-06-28 05:23:36 -04:00
|
|
|
(function cleanup() {
|
2021-10-05 08:50:00 -04:00
|
|
|
dylib.close();
|
2022-06-20 07:06:04 -04:00
|
|
|
throwCallback.close();
|
|
|
|
logCallback.close();
|
|
|
|
logManyParametersCallback.close();
|
|
|
|
returnU8Callback.close();
|
|
|
|
returnBufferCallback.close();
|
|
|
|
add10Callback.close();
|
|
|
|
nestedCallback.close();
|
2022-06-28 05:23:36 -04:00
|
|
|
addToFooCallback.close();
|
2021-08-06 17:28:10 -04:00
|
|
|
|
2024-01-23 18:27:29 -05:00
|
|
|
const resourcesPost = Deno[Deno.internal].core.resources();
|
2021-10-05 08:50:00 -04:00
|
|
|
|
|
|
|
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:
|
2021-08-06 17:28:10 -04:00
|
|
|
Before: ${preStr}
|
|
|
|
After: ${postStr}`,
|
2021-10-05 08:50:00 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log("Correct number of resources");
|
2022-09-07 02:53:56 -04:00
|
|
|
})();
|
|
|
|
|
|
|
|
function assertIsOptimized(fn) {
|
2022-10-15 09:49:46 -04:00
|
|
|
const status = %GetOptimizationStatus(fn);
|
2022-09-07 02:53:56 -04:00
|
|
|
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);
|
2023-01-21 10:51:14 -05:00
|
|
|
}
|