mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 21:50:00 -05:00
perf(ext/ffi): switch from middleware to tasks (#21239)
Deno-side changes for https://github.com/denoland/deno_core/pull/350 --------- Co-authored-by: Aapo Alasuutari <aapo.alasuutari@gmail.com>
This commit is contained in:
parent
d13e45f2b3
commit
a4f45f7092
7 changed files with 300 additions and 97 deletions
|
@ -1,6 +1,6 @@
|
||||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
import { assertEquals, assertThrows } from "./test_util.ts";
|
import { assertEquals, assertRejects, assertThrows } from "./test_util.ts";
|
||||||
|
|
||||||
Deno.test({ permissions: { ffi: true } }, function dlopenInvalidArguments() {
|
Deno.test({ permissions: { ffi: true } }, function dlopenInvalidArguments() {
|
||||||
const filename = "/usr/lib/libc.so.6";
|
const filename = "/usr/lib/libc.so.6";
|
||||||
|
@ -98,3 +98,40 @@ Deno.test({ permissions: { ffi: true } }, function pointerOf() {
|
||||||
);
|
);
|
||||||
assertEquals(Number(baseAddress) + 80, float64AddressOffset);
|
assertEquals(Number(baseAddress) + 80, float64AddressOffset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test({ permissions: { ffi: true } }, function callWithError() {
|
||||||
|
const throwCb = () => {
|
||||||
|
throw new Error("Error");
|
||||||
|
};
|
||||||
|
const cb = new Deno.UnsafeCallback({
|
||||||
|
parameters: [],
|
||||||
|
result: "void",
|
||||||
|
}, throwCb);
|
||||||
|
const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
|
||||||
|
parameters: [],
|
||||||
|
result: "void",
|
||||||
|
});
|
||||||
|
assertThrows(() => fnPointer.call());
|
||||||
|
cb.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { ffi: true }, ignore: true },
|
||||||
|
async function callNonBlockingWithError() {
|
||||||
|
const throwCb = () => {
|
||||||
|
throw new Error("Error");
|
||||||
|
};
|
||||||
|
const cb = new Deno.UnsafeCallback({
|
||||||
|
parameters: [],
|
||||||
|
result: "void",
|
||||||
|
}, throwCb);
|
||||||
|
const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
|
||||||
|
parameters: [],
|
||||||
|
result: "void",
|
||||||
|
nonblocking: true,
|
||||||
|
});
|
||||||
|
// TODO(mmastrac): currently ignored as we do not thread callback exceptions through nonblocking pointers
|
||||||
|
await assertRejects(async () => await fnPointer.call());
|
||||||
|
cb.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -368,7 +368,7 @@ pub fn op_ffi_call_nonblocking(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2(reentrant)]
|
||||||
#[serde]
|
#[serde]
|
||||||
pub fn op_ffi_call_ptr<FP>(
|
pub fn op_ffi_call_ptr<FP>(
|
||||||
scope: &mut v8::HandleScope,
|
scope: &mut v8::HandleScope,
|
||||||
|
|
|
@ -3,21 +3,19 @@
|
||||||
use crate::check_unstable;
|
use crate::check_unstable;
|
||||||
use crate::symbol::NativeType;
|
use crate::symbol::NativeType;
|
||||||
use crate::FfiPermissions;
|
use crate::FfiPermissions;
|
||||||
use crate::FfiState;
|
|
||||||
use crate::ForeignFunction;
|
use crate::ForeignFunction;
|
||||||
use crate::PendingFfiAsyncWork;
|
|
||||||
use crate::MAX_SAFE_INTEGER;
|
use crate::MAX_SAFE_INTEGER;
|
||||||
use crate::MIN_SAFE_INTEGER;
|
use crate::MIN_SAFE_INTEGER;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::futures::channel::mpsc;
|
|
||||||
use deno_core::futures::task::AtomicWaker;
|
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use deno_core::v8;
|
use deno_core::v8;
|
||||||
|
use deno_core::v8::TryCatch;
|
||||||
use deno_core::CancelFuture;
|
use deno_core::CancelFuture;
|
||||||
use deno_core::CancelHandle;
|
use deno_core::CancelHandle;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
use deno_core::Resource;
|
use deno_core::Resource;
|
||||||
use deno_core::ResourceId;
|
use deno_core::ResourceId;
|
||||||
|
use deno_core::V8CrossThreadTaskSpawner;
|
||||||
use libffi::middle::Cif;
|
use libffi::middle::Cif;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
@ -31,8 +29,6 @@ use std::ptr::NonNull;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::atomic;
|
use std::sync::atomic;
|
||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
use std::sync::mpsc::sync_channel;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
|
|
||||||
static THREAD_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
|
static THREAD_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
|
||||||
|
@ -93,13 +89,12 @@ impl Resource for UnsafeCallbackResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CallbackInfo {
|
struct CallbackInfo {
|
||||||
pub async_work_sender: mpsc::UnboundedSender<PendingFfiAsyncWork>,
|
pub async_work_sender: V8CrossThreadTaskSpawner,
|
||||||
pub callback: NonNull<v8::Function>,
|
pub callback: NonNull<v8::Function>,
|
||||||
pub context: NonNull<v8::Context>,
|
pub context: NonNull<v8::Context>,
|
||||||
pub parameters: Box<[NativeType]>,
|
pub parameters: Box<[NativeType]>,
|
||||||
pub result: NativeType,
|
pub result: NativeType,
|
||||||
pub thread_id: u32,
|
pub thread_id: u32,
|
||||||
pub waker: Arc<AtomicWaker>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Future for CallbackInfo {
|
impl Future for CallbackInfo {
|
||||||
|
@ -113,6 +108,32 @@ impl Future for CallbackInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TaskArgs {
|
||||||
|
cif: NonNull<libffi::low::ffi_cif>,
|
||||||
|
result: NonNull<c_void>,
|
||||||
|
args: *const *const c_void,
|
||||||
|
info: NonNull<CallbackInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: we know these are valid Send-safe pointers as they are for FFI
|
||||||
|
unsafe impl Send for TaskArgs {}
|
||||||
|
|
||||||
|
impl TaskArgs {
|
||||||
|
fn run(&mut self, scope: &mut v8::HandleScope) {
|
||||||
|
// SAFETY: making a call using Send-safe pointers turned back into references. We know the
|
||||||
|
// lifetime of these will last because we block on the result of the spawn call.
|
||||||
|
unsafe {
|
||||||
|
do_ffi_callback(
|
||||||
|
scope,
|
||||||
|
self.cif.as_ref(),
|
||||||
|
self.info.as_ref(),
|
||||||
|
self.result.as_mut(),
|
||||||
|
self.args,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unsafe extern "C" fn deno_ffi_callback(
|
unsafe extern "C" fn deno_ffi_callback(
|
||||||
cif: &libffi::low::ffi_cif,
|
cif: &libffi::low::ffi_cif,
|
||||||
result: &mut c_void,
|
result: &mut c_void,
|
||||||
|
@ -121,39 +142,6 @@ unsafe extern "C" fn deno_ffi_callback(
|
||||||
) {
|
) {
|
||||||
LOCAL_THREAD_ID.with(|s| {
|
LOCAL_THREAD_ID.with(|s| {
|
||||||
if *s.borrow() == info.thread_id {
|
if *s.borrow() == info.thread_id {
|
||||||
// Own isolate thread, okay to call directly
|
|
||||||
do_ffi_callback(cif, info, result, args);
|
|
||||||
} else {
|
|
||||||
let async_work_sender = &info.async_work_sender;
|
|
||||||
// SAFETY: Safe as this function blocks until `do_ffi_callback` completes and a response message is received.
|
|
||||||
let cif: &'static libffi::low::ffi_cif = std::mem::transmute(cif);
|
|
||||||
let result: &'static mut c_void = std::mem::transmute(result);
|
|
||||||
let info: &'static CallbackInfo = std::mem::transmute(info);
|
|
||||||
let (response_sender, response_receiver) = sync_channel::<()>(0);
|
|
||||||
let fut = Box::new(move || {
|
|
||||||
do_ffi_callback(cif, info, result, args);
|
|
||||||
response_sender.send(()).unwrap();
|
|
||||||
});
|
|
||||||
async_work_sender.unbounded_send(fut).unwrap();
|
|
||||||
// Make sure event loop wakes up to receive our message before we start waiting for a response.
|
|
||||||
info.waker.wake();
|
|
||||||
response_receiver.recv().unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn do_ffi_callback(
|
|
||||||
cif: &libffi::low::ffi_cif,
|
|
||||||
info: &CallbackInfo,
|
|
||||||
result: &mut c_void,
|
|
||||||
args: *const *const c_void,
|
|
||||||
) {
|
|
||||||
let callback: NonNull<v8::Function> = info.callback;
|
|
||||||
let context: NonNull<v8::Context> = info.context;
|
|
||||||
let context = std::mem::transmute::<
|
|
||||||
NonNull<v8::Context>,
|
|
||||||
v8::Local<v8::Context>,
|
|
||||||
>(context);
|
|
||||||
// Call from main thread. If this callback is being triggered due to a
|
// Call from main thread. If this callback is being triggered due to a
|
||||||
// function call coming from Deno itself, then this callback will build
|
// function call coming from Deno itself, then this callback will build
|
||||||
// ontop of that stack.
|
// ontop of that stack.
|
||||||
|
@ -164,8 +152,45 @@ unsafe fn do_ffi_callback(
|
||||||
// The callback will even be called through from a `while (true)` liveloop, but
|
// The callback will even be called through from a `while (true)` liveloop, but
|
||||||
// it somehow cannot change the values that the loop sees, even if they both
|
// it somehow cannot change the values that the loop sees, even if they both
|
||||||
// refer the same `let bool_value`.
|
// refer the same `let bool_value`.
|
||||||
|
let context: NonNull<v8::Context> = info.context;
|
||||||
|
let context = std::mem::transmute::<
|
||||||
|
NonNull<v8::Context>,
|
||||||
|
v8::Local<v8::Context>,
|
||||||
|
>(context);
|
||||||
let mut cb_scope = v8::CallbackScope::new(context);
|
let mut cb_scope = v8::CallbackScope::new(context);
|
||||||
let scope = &mut v8::HandleScope::new(&mut cb_scope);
|
let scope = &mut v8::HandleScope::new(&mut cb_scope);
|
||||||
|
|
||||||
|
do_ffi_callback(scope, cif, info, result, args);
|
||||||
|
} else {
|
||||||
|
let async_work_sender = &info.async_work_sender;
|
||||||
|
|
||||||
|
let mut args = TaskArgs {
|
||||||
|
cif: NonNull::from(cif),
|
||||||
|
result: NonNull::from(result),
|
||||||
|
args,
|
||||||
|
info: NonNull::from(info),
|
||||||
|
};
|
||||||
|
|
||||||
|
async_work_sender.spawn_blocking(move |scope| {
|
||||||
|
// We don't have a lot of choice here, so just print an unhandled exception message
|
||||||
|
let tc_scope = &mut TryCatch::new(scope);
|
||||||
|
args.run(tc_scope);
|
||||||
|
if tc_scope.exception().is_some() {
|
||||||
|
eprintln!("Illegal unhandled exception in nonblocking callback.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn do_ffi_callback(
|
||||||
|
scope: &mut v8::HandleScope,
|
||||||
|
cif: &libffi::low::ffi_cif,
|
||||||
|
info: &CallbackInfo,
|
||||||
|
result: &mut c_void,
|
||||||
|
args: *const *const c_void,
|
||||||
|
) {
|
||||||
|
let callback: NonNull<v8::Function> = info.callback;
|
||||||
let func = std::mem::transmute::<
|
let func = std::mem::transmute::<
|
||||||
NonNull<v8::Function>,
|
NonNull<v8::Function>,
|
||||||
v8::Local<v8::Function>,
|
v8::Local<v8::Function>,
|
||||||
|
@ -562,25 +587,12 @@ where
|
||||||
panic!("Isolate ID counter overflowed u32");
|
panic!("Isolate ID counter overflowed u32");
|
||||||
}
|
}
|
||||||
|
|
||||||
let async_work_sender =
|
let async_work_sender = state.borrow::<V8CrossThreadTaskSpawner>().clone();
|
||||||
if let Some(ffi_state) = state.try_borrow_mut::<FfiState>() {
|
|
||||||
ffi_state.async_work_sender.clone()
|
|
||||||
} else {
|
|
||||||
let (async_work_sender, async_work_receiver) =
|
|
||||||
mpsc::unbounded::<PendingFfiAsyncWork>();
|
|
||||||
|
|
||||||
state.put(FfiState {
|
|
||||||
async_work_receiver,
|
|
||||||
async_work_sender: async_work_sender.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
async_work_sender
|
|
||||||
};
|
|
||||||
let callback = v8::Global::new(scope, cb).into_raw();
|
let callback = v8::Global::new(scope, cb).into_raw();
|
||||||
let current_context = scope.get_current_context();
|
let current_context = scope.get_current_context();
|
||||||
let context = v8::Global::new(scope, current_context).into_raw();
|
let context = v8::Global::new(scope, current_context).into_raw();
|
||||||
|
|
||||||
let waker = state.waker.clone();
|
|
||||||
let info: *mut CallbackInfo = Box::leak(Box::new(CallbackInfo {
|
let info: *mut CallbackInfo = Box::leak(Box::new(CallbackInfo {
|
||||||
async_work_sender,
|
async_work_sender,
|
||||||
callback,
|
callback,
|
||||||
|
@ -588,7 +600,6 @@ where
|
||||||
parameters: args.parameters.clone().into(),
|
parameters: args.parameters.clone().into(),
|
||||||
result: args.result.clone(),
|
result: args.result.clone(),
|
||||||
thread_id,
|
thread_id,
|
||||||
waker,
|
|
||||||
}));
|
}));
|
||||||
let cif = Cif::new(
|
let cif = Cif::new(
|
||||||
args
|
args
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::futures::channel::mpsc;
|
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
use std::os::raw::c_short;
|
use std::os::raw::c_short;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
mod call;
|
mod call;
|
||||||
mod callback;
|
mod callback;
|
||||||
|
@ -59,13 +56,6 @@ pub trait FfiPermissions {
|
||||||
fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError>;
|
fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type PendingFfiAsyncWork = Box<dyn FnOnce()>;
|
|
||||||
|
|
||||||
pub(crate) struct FfiState {
|
|
||||||
pub(crate) async_work_sender: mpsc::UnboundedSender<PendingFfiAsyncWork>,
|
|
||||||
pub(crate) async_work_receiver: mpsc::UnboundedReceiver<PendingFfiAsyncWork>,
|
|
||||||
}
|
|
||||||
|
|
||||||
deno_core::extension!(deno_ffi,
|
deno_core::extension!(deno_ffi,
|
||||||
deps = [ deno_web ],
|
deps = [ deno_web ],
|
||||||
parameters = [P: FfiPermissions],
|
parameters = [P: FfiPermissions],
|
||||||
|
@ -101,35 +91,4 @@ deno_core::extension!(deno_ffi,
|
||||||
op_ffi_unsafe_callback_ref,
|
op_ffi_unsafe_callback_ref,
|
||||||
],
|
],
|
||||||
esm = [ "00_ffi.js" ],
|
esm = [ "00_ffi.js" ],
|
||||||
event_loop_middleware = event_loop_middleware,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fn event_loop_middleware(
|
|
||||||
op_state_rc: Rc<RefCell<OpState>>,
|
|
||||||
_cx: &mut std::task::Context,
|
|
||||||
) -> bool {
|
|
||||||
// FFI callbacks coming in from other threads will call in and get queued.
|
|
||||||
let mut maybe_scheduling = false;
|
|
||||||
|
|
||||||
let mut op_state = op_state_rc.borrow_mut();
|
|
||||||
if let Some(ffi_state) = op_state.try_borrow_mut::<FfiState>() {
|
|
||||||
// TODO(mmastrac): This should be a SmallVec to avoid allocations in most cases
|
|
||||||
let mut work_items = Vec::with_capacity(1);
|
|
||||||
|
|
||||||
while let Ok(Some(async_work_fut)) =
|
|
||||||
ffi_state.async_work_receiver.try_next()
|
|
||||||
{
|
|
||||||
// Move received items to a temporary vector so that we can drop the `op_state` borrow before we do the work.
|
|
||||||
work_items.push(async_work_fut);
|
|
||||||
maybe_scheduling = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop the op_state and ffi_state borrows
|
|
||||||
drop(op_state);
|
|
||||||
for async_work_fut in work_items.into_iter() {
|
|
||||||
async_work_fut();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_scheduling
|
|
||||||
}
|
|
||||||
|
|
|
@ -258,6 +258,20 @@ pub extern "C" fn call_stored_function_thread_safe_and_log() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn call_stored_function_2_thread_safe(arg: u8) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||||
|
unsafe {
|
||||||
|
if STORED_FUNCTION_2.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("Calling");
|
||||||
|
STORED_FUNCTION_2.unwrap()(arg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn log_many_parameters(
|
pub extern "C" fn log_many_parameters(
|
||||||
a: u8,
|
a: u8,
|
||||||
|
|
141
test_ffi/tests/ffi_callback_errors.ts
Normal file
141
test_ffi/tests/ffi_callback_errors.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
const targetDir = Deno.execPath().replace(/[^\/\\]+$/, "");
|
||||||
|
const [libPrefix, libSuffix] = {
|
||||||
|
darwin: ["lib", "dylib"],
|
||||||
|
linux: ["lib", "so"],
|
||||||
|
windows: ["", "dll"],
|
||||||
|
}[Deno.build.os];
|
||||||
|
const libPath = `${targetDir}/${libPrefix}test_ffi.${libSuffix}`;
|
||||||
|
|
||||||
|
const dylib = Deno.dlopen(
|
||||||
|
libPath,
|
||||||
|
{
|
||||||
|
store_function_2: {
|
||||||
|
parameters: ["function"],
|
||||||
|
result: "void",
|
||||||
|
},
|
||||||
|
call_stored_function_2: {
|
||||||
|
parameters: ["u8"],
|
||||||
|
result: "void",
|
||||||
|
},
|
||||||
|
call_stored_function_2_from_other_thread: {
|
||||||
|
name: "call_stored_function_2",
|
||||||
|
parameters: ["u8"],
|
||||||
|
result: "void",
|
||||||
|
nonblocking: true,
|
||||||
|
},
|
||||||
|
call_stored_function_2_thread_safe: {
|
||||||
|
parameters: ["u8"],
|
||||||
|
result: "void",
|
||||||
|
},
|
||||||
|
} as const,
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.addEventListener("error", (data) => {
|
||||||
|
console.log("Unhandled error");
|
||||||
|
data.preventDefault();
|
||||||
|
});
|
||||||
|
globalThis.onerror = (data) => {
|
||||||
|
console.log("Unhandled error");
|
||||||
|
if (typeof data !== "string") {
|
||||||
|
data.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.addEventListener("unhandledrejection", (data) => {
|
||||||
|
console.log("Unhandled rejection");
|
||||||
|
data.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.error(
|
||||||
|
"Test failed, final callback did not get picked up by Deno event loop",
|
||||||
|
);
|
||||||
|
Deno.exit(-1);
|
||||||
|
}, 5_000);
|
||||||
|
|
||||||
|
Deno.unrefTimer(timer);
|
||||||
|
|
||||||
|
enum CallCase {
|
||||||
|
SyncSelf,
|
||||||
|
SyncFfi,
|
||||||
|
AsyncSelf,
|
||||||
|
AsyncSyncFfi,
|
||||||
|
AsyncFfi,
|
||||||
|
}
|
||||||
|
type U8CallCase = Deno.NativeU8Enum<CallCase>;
|
||||||
|
|
||||||
|
const throwCb = (c: CallCase): number => {
|
||||||
|
console.log("CallCase:", CallCase[c]);
|
||||||
|
if (c === CallCase.AsyncFfi) {
|
||||||
|
cb.unref();
|
||||||
|
}
|
||||||
|
throw new Error("Error");
|
||||||
|
};
|
||||||
|
|
||||||
|
const THROW_CB_DEFINITION = {
|
||||||
|
parameters: ["u8" as U8CallCase],
|
||||||
|
result: "u8",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const cb = new Deno.UnsafeCallback(THROW_CB_DEFINITION, throwCb);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, THROW_CB_DEFINITION);
|
||||||
|
|
||||||
|
fnPointer.call(CallCase.SyncSelf);
|
||||||
|
} catch (_err) {
|
||||||
|
console.log(
|
||||||
|
"Throwing errors from an UnsafeCallback called from a synchronous UnsafeFnPointer works. Terribly excellent.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dylib.symbols.store_function_2(cb.pointer);
|
||||||
|
try {
|
||||||
|
dylib.symbols.call_stored_function_2(CallCase.SyncFfi);
|
||||||
|
} catch (_err) {
|
||||||
|
console.log(
|
||||||
|
"Throwing errors from an UnsafeCallback called from a synchronous FFI symbol works. Terribly excellent.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fnPointer = new Deno.UnsafeFnPointer(cb.pointer, {
|
||||||
|
...THROW_CB_DEFINITION,
|
||||||
|
nonblocking: true,
|
||||||
|
});
|
||||||
|
await fnPointer.call(CallCase.AsyncSelf);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
"Nonblocking UnsafeFnPointer should not be threading through a JS error thrown on the other side of the call",
|
||||||
|
{
|
||||||
|
cause: err,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dylib.symbols.call_stored_function_2_from_other_thread(
|
||||||
|
CallCase.AsyncSyncFfi,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
"Nonblocking symbol call should not be threading through a JS error thrown on the other side of the call",
|
||||||
|
{
|
||||||
|
cause: err,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Ref the callback to make sure we do not exit before the call is done.
|
||||||
|
cb.ref();
|
||||||
|
dylib.symbols.call_stored_function_2_thread_safe(CallCase.AsyncFfi);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
"Blocking symbol call should not be travelling 1.5 seconds forward in time to figure out that it call will trigger a JS error to be thrown",
|
||||||
|
{
|
||||||
|
cause: err,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
|
@ -237,3 +237,44 @@ fn event_loop_integration() {
|
||||||
assert_eq!(stdout, expected);
|
assert_eq!(stdout, expected);
|
||||||
assert_eq!(stderr, "");
|
assert_eq!(stderr, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ffi_callback_errors_test() {
|
||||||
|
build();
|
||||||
|
|
||||||
|
let output = deno_cmd()
|
||||||
|
.arg("run")
|
||||||
|
.arg("--allow-ffi")
|
||||||
|
.arg("--allow-read")
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("--quiet")
|
||||||
|
.arg("tests/ffi_callback_errors.ts")
|
||||||
|
.env("NO_COLOR", "1")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
let stdout = std::str::from_utf8(&output.stdout).unwrap();
|
||||||
|
let stderr = std::str::from_utf8(&output.stderr).unwrap();
|
||||||
|
if !output.status.success() {
|
||||||
|
println!("stdout {stdout}");
|
||||||
|
println!("stderr {stderr}");
|
||||||
|
}
|
||||||
|
println!("{:?}", output.status);
|
||||||
|
assert!(output.status.success());
|
||||||
|
|
||||||
|
let expected = "\
|
||||||
|
CallCase: SyncSelf\n\
|
||||||
|
Throwing errors from an UnsafeCallback called from a synchronous UnsafeFnPointer works. Terribly excellent.\n\
|
||||||
|
CallCase: SyncFfi\n\
|
||||||
|
0\n\
|
||||||
|
Throwing errors from an UnsafeCallback called from a synchronous FFI symbol works. Terribly excellent.\n\
|
||||||
|
CallCase: AsyncSelf\n\
|
||||||
|
CallCase: AsyncSyncFfi\n\
|
||||||
|
0\n\
|
||||||
|
Calling\n\
|
||||||
|
CallCase: AsyncFfi\n";
|
||||||
|
assert_eq!(stdout, expected);
|
||||||
|
assert_eq!(
|
||||||
|
stderr,
|
||||||
|
"Illegal unhandled exception in nonblocking callback.\n".repeat(3)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue