1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 07:44:48 -05:00
denoland-deno/test_ffi/tests/test.js
Aapo Alasuutari 75acec0aea
fix(ext/ffi): Fix UnsafeCallback ref'ing making Deno enter a live-loop (#16216)
Fixes #15136

Currently `UnsafeCallback` class' `ref()` and `unref()` methods rely on
the `event_loop_middleware` implementation in core. If even a single
`UnsafeCallback` is ref'ed, then the FFI event loop middleware will
always return `true` to signify that there may still be more work for
the event loop to do.

The middleware handling in core does not wait a moment to check again,
but will instead synchronously directly re-poll the event loop and
middlewares for more work. This becomes a live-loop.

This PR introduces a `Future` implementation for the `CallbackInfo`
struct that acts as the intermediary data storage between an
`UnsafeCallback` and the `libffi` C callback. Ref'ing a callback now
means calling an async op that binds to the `CallbackInfo` Future and
only resolves once the callback is unref'ed. The `libffi` C callback
will call the waker of this Future when it fires to make sure that the
main thread wakes up to receive the callback.
2022-10-15 19:19:46 +05:30

605 lines
No EOL
18 KiB
JavaScript

// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file
// Run using cargo test or `--v8-options=--allow-natives-syntax`
import { assertEquals } from "https://deno.land/std@0.149.0/testing/asserts.ts";
import {
assertThrows,
assert,
} from "../../test_util/std/testing/asserts.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.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",
);
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: "u8" },
"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",
},
"hash": { parameters: ["buffer", "u32"], result: "u32" },
});
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);
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(Boolean(dylib.symbols.is_null_ptr(ptr0)));
console.log(Boolean(dylib.symbols.is_null_ptr(null)));
console.log(Boolean(dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(into))));
const emptyBuffer = new BigUint64Array(0);
console.log(Boolean(dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(emptyBuffer))));
const emptySlice = into.subarray(6);
console.log(Boolean(dylib.symbols.is_null_ptr(Deno.UnsafePointer.of(emptySlice))));
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]);
// Test non blocking calls
function deferred() {
let methods;
const promise = new Promise((resolve, reject) => {
methods = {
async resolve(value) {
await value;
resolve(value);
},
reject(reason) {
reject(reason);
},
};
});
return Object.assign(promise, methods);
}
const promise = deferred();
const buffer3 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
dylib.symbols.nonblocking_buffer(buffer3, buffer3.length).then(() => {
promise.resolve();
});
await promise;
let start = performance.now();
dylib.symbols.sleep_blocking(100);
console.log("After sleep_blocking");
console.log(performance.now() - start >= 100);
start = performance.now();
const promise_2 = dylib.symbols.sleep_nonblocking(100).then(() => {
console.log("After");
console.log(performance.now() - start >= 100);
});
console.log("Before");
console.log(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: "pointer",
}, () => {
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
console.log("Thread safe call counter:", counter);
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();
console.log("Thread safe call counter:", counter);
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
console.log("Static u32:", dylib.symbols.static_u32);
console.log("Static i64:", dylib.symbols.static_i64);
console.log(
"Static ptr:",
typeof dylib.symbols.static_ptr === "number",
);
const view = new Deno.UnsafePointerView(dylib.symbols.static_ptr);
console.log("Static ptr value:", view.getUint32());
const arrayBuffer = view.getArrayBuffer(4);
const uint32Array = new Uint32Array(arrayBuffer);
console.log("arrayBuffer.byteLength:", arrayBuffer.byteLength);
console.log("uint32Array.length:", uint32Array.length);
console.log("uint32Array[0]:", uint32Array[0]);
uint32Array[0] = 55; // MUTATES!
console.log("uint32Array[0] after mutation:", uint32Array[0]);
console.log("Static ptr value after mutation:", view.getUint32());
// 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
]);
try {
assertThrows(() => charView.getCString(), TypeError, "Invalid CString pointer, not valid UTF-8");
} catch (_err) {
console.log("Invalid UTF-8 characters to `v8::String`:", charView.getCString());
}
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.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);
}