fix: Rewrite Node-API (#24101)

Phase 1 node-api rewrite
This commit is contained in:
snek 2024-06-10 09:20:44 -07:00 committed by GitHub
parent 7c5dbd5d54
commit e3b2ee183b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 4797 additions and 3317 deletions

View file

@ -8,18 +8,14 @@
use core::ptr::NonNull;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::futures::channel::mpsc;
use deno_core::op2;
use deno_core::parking_lot::Mutex;
use deno_core::ExternalOpsTracker;
use deno_core::OpState;
use deno_core::V8CrossThreadTaskSpawner;
use std::cell::RefCell;
use std::ffi::CString;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::thread_local;
#[cfg(unix)]
@ -32,7 +28,6 @@ use libloading::os::windows::*;
// `use deno_napi::*`
pub use deno_core::v8;
pub use std::ffi::CStr;
pub use std::mem::transmute;
pub use std::os::raw::c_char;
pub use std::os::raw::c_void;
pub use std::ptr;
@ -52,6 +47,7 @@ pub type napi_callback_scope = *mut c_void;
pub type napi_escapable_handle_scope = *mut c_void;
pub type napi_async_cleanup_hook_handle = *mut c_void;
pub type napi_async_work = *mut c_void;
pub type napi_async_context = *mut c_void;
pub const napi_ok: napi_status = 0;
pub const napi_invalid_arg: napi_status = 1;
@ -75,6 +71,35 @@ pub const napi_date_expected: napi_status = 18;
pub const napi_arraybuffer_expected: napi_status = 19;
pub const napi_detachable_arraybuffer_expected: napi_status = 20;
pub const napi_would_deadlock: napi_status = 21;
pub const napi_no_external_buffers_allowed: napi_status = 22;
pub const napi_cannot_run_js: napi_status = 23;
pub static ERROR_MESSAGES: &[&CStr] = &[
c"",
c"Invalid argument",
c"An object was expected",
c"A string was expected",
c"A string or symbol was expected",
c"A function was expected",
c"A number was expected",
c"A boolean was expected",
c"An array was expected",
c"Unknown failure",
c"An exception is pending",
c"The async work item was cancelled",
c"napi_escape_handle already called on scope",
c"Invalid handle scope usage",
c"Invalid callback scope usage",
c"Thread-safe function queue is full",
c"Thread-safe function handle is closing",
c"A bigint was expected",
c"A date was expected",
c"An arraybuffer was expected",
c"A detachable arraybuffer was expected",
c"Main thread would deadlock",
c"External buffers are not allowed",
c"Cannot run JavaScript",
];
pub const NAPI_AUTO_LENGTH: usize = usize::MAX;
@ -83,7 +108,9 @@ thread_local! {
}
type napi_addon_register_func =
extern "C" fn(env: napi_env, exports: napi_value) -> napi_value;
unsafe extern "C" fn(env: napi_env, exports: napi_value) -> napi_value;
type napi_register_module_v1 =
unsafe extern "C" fn(env: napi_env, exports: napi_value) -> napi_value;
#[repr(C)]
#[derive(Clone)]
@ -113,7 +140,7 @@ pub const napi_bigint: napi_valuetype = 9;
pub type napi_threadsafe_function_release_mode = i32;
pub const napi_tsfn_release: napi_threadsafe_function_release_mode = 0;
pub const napi_tsfn_abortext: napi_threadsafe_function_release_mode = 1;
pub const napi_tsfn_abort: napi_threadsafe_function_release_mode = 1;
pub type napi_threadsafe_function_call_mode = i32;
@ -153,6 +180,8 @@ pub const napi_float64_array: napi_typedarray_type = 8;
pub const napi_bigint64_array: napi_typedarray_type = 9;
pub const napi_biguint64_array: napi_typedarray_type = 10;
#[repr(C)]
#[derive(Clone, Copy, PartialEq)]
pub struct napi_type_tag {
pub lower: u64,
pub upper: u64,
@ -187,6 +216,8 @@ pub type napi_threadsafe_function_call_js = unsafe extern "C" fn(
pub type napi_async_cleanup_hook =
unsafe extern "C" fn(env: napi_env, data: *mut c_void);
pub type napi_cleanup_hook = unsafe extern "C" fn(data: *mut c_void);
pub type napi_property_attributes = i32;
pub const napi_default: napi_property_attributes = 0;
@ -233,17 +264,9 @@ pub struct napi_node_version {
pub trait PendingNapiAsyncWork: FnOnce() + Send + 'static {}
impl<T> PendingNapiAsyncWork for T where T: FnOnce() + Send + 'static {}
pub type ThreadsafeFunctionRefCounters = Vec<(usize, Arc<AtomicUsize>)>;
pub struct NapiState {
// Thread safe functions.
pub active_threadsafe_functions: usize,
pub threadsafe_function_receiver:
mpsc::UnboundedReceiver<ThreadSafeFunctionStatus>,
pub threadsafe_function_sender:
mpsc::UnboundedSender<ThreadSafeFunctionStatus>,
pub env_cleanup_hooks:
Rc<RefCell<Vec<(extern "C" fn(*const c_void), *const c_void)>>>,
pub tsfn_ref_counters: Arc<Mutex<ThreadsafeFunctionRefCounters>>,
pub env_cleanup_hooks: Rc<RefCell<Vec<(napi_cleanup_hook, *mut c_void)>>>,
}
impl Drop for NapiState {
@ -267,7 +290,10 @@ impl Drop for NapiState {
continue;
}
(hook.0)(hook.1);
unsafe {
(hook.0)(hook.1);
}
{
self
.env_cleanup_hooks
@ -277,38 +303,44 @@ impl Drop for NapiState {
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct InstanceData {
pub data: *mut c_void,
pub finalize_cb: Option<napi_finalize>,
pub finalize_hint: *mut c_void,
}
#[repr(C)]
#[derive(Debug)]
/// Env that is shared between all contexts in same native module.
pub struct EnvShared {
pub instance_data: *mut c_void,
pub data_finalize: Option<napi_finalize>,
pub data_finalize_hint: *mut c_void,
pub instance_data: Option<InstanceData>,
pub napi_wrap: v8::Global<v8::Private>,
pub type_tag: v8::Global<v8::Private>,
pub finalize: Option<napi_finalize>,
pub finalize_hint: *mut c_void,
pub filename: *const c_char,
pub filename: String,
}
impl EnvShared {
pub fn new(napi_wrap: v8::Global<v8::Private>) -> Self {
pub fn new(
napi_wrap: v8::Global<v8::Private>,
type_tag: v8::Global<v8::Private>,
filename: String,
) -> Self {
Self {
instance_data: std::ptr::null_mut(),
data_finalize: None,
data_finalize_hint: std::ptr::null_mut(),
instance_data: None,
napi_wrap,
type_tag,
finalize: None,
finalize_hint: std::ptr::null_mut(),
filename: std::ptr::null(),
filename,
}
}
}
pub enum ThreadSafeFunctionStatus {
Alive,
Dead,
}
#[repr(C)]
pub struct Env {
context: NonNull<v8::Context>,
@ -316,46 +348,48 @@ pub struct Env {
pub open_handle_scopes: usize,
pub shared: *mut EnvShared,
pub async_work_sender: V8CrossThreadTaskSpawner,
pub threadsafe_function_sender:
mpsc::UnboundedSender<ThreadSafeFunctionStatus>,
pub cleanup_hooks:
Rc<RefCell<Vec<(extern "C" fn(*const c_void), *const c_void)>>>,
pub tsfn_ref_counters: Arc<Mutex<ThreadsafeFunctionRefCounters>>,
pub cleanup_hooks: Rc<RefCell<Vec<(napi_cleanup_hook, *mut c_void)>>>,
pub external_ops_tracker: ExternalOpsTracker,
pub last_error: napi_extended_error_info,
pub last_exception: Option<v8::Global<v8::Value>>,
pub global: NonNull<v8::Value>,
pub buffer_constructor: NonNull<v8::Function>,
pub report_error: NonNull<v8::Function>,
}
unsafe impl Send for Env {}
unsafe impl Sync for Env {}
impl Env {
#[allow(clippy::too_many_arguments)]
pub fn new(
isolate_ptr: *mut v8::OwnedIsolate,
context: v8::Global<v8::Context>,
global: v8::Global<v8::Value>,
buffer_constructor: v8::Global<v8::Function>,
report_error: v8::Global<v8::Function>,
sender: V8CrossThreadTaskSpawner,
threadsafe_function_sender: mpsc::UnboundedSender<ThreadSafeFunctionStatus>,
cleanup_hooks: Rc<
RefCell<Vec<(extern "C" fn(*const c_void), *const c_void)>>,
>,
tsfn_ref_counters: Arc<Mutex<ThreadsafeFunctionRefCounters>>,
cleanup_hooks: Rc<RefCell<Vec<(napi_cleanup_hook, *mut c_void)>>>,
external_ops_tracker: ExternalOpsTracker,
) -> Self {
Self {
isolate_ptr,
context: context.into_raw(),
global: global.into_raw(),
buffer_constructor: buffer_constructor.into_raw(),
report_error: report_error.into_raw(),
shared: std::ptr::null_mut(),
open_handle_scopes: 0,
async_work_sender: sender,
threadsafe_function_sender,
cleanup_hooks,
tsfn_ref_counters,
external_ops_tracker,
last_error: napi_extended_error_info {
error_message: std::ptr::null(),
engine_reserved: std::ptr::null_mut(),
engine_error_code: 0,
error_code: napi_ok,
},
last_exception: None,
}
}
@ -384,7 +418,9 @@ impl Env {
// SAFETY: `v8::Local` is always non-null pointer; the `HandleScope` is
// already on the stack, but we don't have access to it.
let context = unsafe {
transmute::<NonNull<v8::Context>, v8::Local<v8::Context>>(self.context)
std::mem::transmute::<NonNull<v8::Context>, v8::Local<v8::Context>>(
self.context,
)
};
// SAFETY: there must be a `HandleScope` on the stack, this is ensured because
// we are in a V8 callback or the module has already opened a `HandleScope`
@ -392,20 +428,12 @@ impl Env {
unsafe { v8::CallbackScope::new(context) }
}
pub fn add_threadsafe_function_ref_counter(
&mut self,
id: usize,
counter: Arc<AtomicUsize>,
) {
let mut counters = self.tsfn_ref_counters.lock();
assert!(!counters.iter().any(|(i, _)| *i == id));
counters.push((id, counter));
pub fn threadsafe_function_ref(&mut self) {
self.external_ops_tracker.ref_op();
}
pub fn remove_threadsafe_function_ref_counter(&mut self, id: usize) {
let mut counters = self.tsfn_ref_counters.lock();
let index = counters.iter().position(|(i, _)| *i == id).unwrap();
counters.remove(index);
pub fn threadsafe_function_unref(&mut self) {
self.external_ops_tracker.unref_op();
}
}
@ -415,14 +443,8 @@ deno_core::extension!(deno_napi,
op_napi_open<P>
],
state = |state| {
let (threadsafe_function_sender, threadsafe_function_receiver) =
mpsc::unbounded::<ThreadSafeFunctionStatus>();
state.put(NapiState {
threadsafe_function_sender,
threadsafe_function_receiver,
active_threadsafe_functions: 0,
env_cleanup_hooks: Rc::new(RefCell::new(vec![])),
tsfn_ref_counters: Arc::new(Mutex::new(vec![])),
});
},
);
@ -441,69 +463,21 @@ impl NapiPermissions for deno_permissions::PermissionsContainer {
}
}
/// # Safety
///
/// This function is unsafe because it dereferences raw pointer Env.
/// - The caller must ensure that the pointer is valid.
/// - The caller must ensure that the pointer is not freed.
pub unsafe fn weak_local(
env_ptr: *mut Env,
value: v8::Local<v8::Value>,
data: *mut c_void,
finalize_cb: napi_finalize,
finalize_hint: *mut c_void,
) -> Option<v8::Local<v8::Value>> {
use std::cell::Cell;
let env = &mut *env_ptr;
let weak_ptr = Rc::new(Cell::new(None));
let scope = &mut env.scope();
let weak = v8::Weak::with_finalizer(
scope,
value,
Box::new({
let weak_ptr = weak_ptr.clone();
move |isolate| {
finalize_cb(env_ptr as _, data as _, finalize_hint as _);
// Self-deleting weak.
if let Some(weak_ptr) = weak_ptr.get() {
let weak: v8::Weak<v8::Value> =
unsafe { v8::Weak::from_raw(isolate, Some(weak_ptr)) };
drop(weak);
}
}
}),
);
let value = weak.to_local(scope);
let raw = weak.into_raw();
weak_ptr.set(raw);
value
}
#[op2]
#[op2(reentrant)]
fn op_napi_open<NP, 'scope>(
scope: &mut v8::HandleScope<'scope>,
op_state: Rc<RefCell<OpState>>,
#[string] path: String,
global: v8::Local<'scope, v8::Value>,
buffer_constructor: v8::Local<'scope, v8::Function>,
report_error: v8::Local<'scope, v8::Function>,
) -> std::result::Result<v8::Local<'scope, v8::Value>, AnyError>
where
NP: NapiPermissions + 'static,
{
// We must limit the OpState borrow because this function can trigger a
// re-borrow through the NAPI module.
let (
async_work_sender,
tsfn_sender,
isolate_ptr,
cleanup_hooks,
tsfn_ref_counters,
) = {
let (async_work_sender, isolate_ptr, cleanup_hooks, external_ops_tracker) = {
let mut op_state = op_state.borrow_mut();
let permissions = op_state.borrow_mut::<NP>();
permissions.check(Some(&PathBuf::from(&path)))?;
@ -511,10 +485,9 @@ where
let isolate_ptr = op_state.borrow::<*mut v8::OwnedIsolate>();
(
op_state.borrow::<V8CrossThreadTaskSpawner>().clone(),
napi_state.threadsafe_function_sender.clone(),
*isolate_ptr,
napi_state.env_cleanup_hooks.clone(),
napi_state.tsfn_ref_counters.clone(),
op_state.external_ops_tracker.clone(),
)
};
@ -522,23 +495,25 @@ where
let napi_wrap = v8::Private::new(scope, Some(napi_wrap_name));
let napi_wrap = v8::Global::new(scope, napi_wrap);
let type_tag_name = v8::String::new(scope, "type_tag").unwrap();
let type_tag = v8::Private::new(scope, Some(type_tag_name));
let type_tag = v8::Global::new(scope, type_tag);
// The `module.exports` object.
let exports = v8::Object::new(scope);
let mut env_shared = EnvShared::new(napi_wrap);
let cstr = CString::new(&*path).unwrap();
env_shared.filename = cstr.as_ptr();
std::mem::forget(cstr);
let env_shared = EnvShared::new(napi_wrap, type_tag, path.clone());
let ctx = scope.get_current_context();
let mut env = Env::new(
isolate_ptr,
v8::Global::new(scope, ctx),
v8::Global::new(scope, global),
v8::Global::new(scope, buffer_constructor),
v8::Global::new(scope, report_error),
async_work_sender,
tsfn_sender,
cleanup_hooks,
tsfn_ref_counters,
external_ops_tracker,
);
env.shared = Box::into_raw(Box::new(env_shared));
let env_ptr = Box::into_raw(Box::new(env)) as _;
@ -567,63 +542,30 @@ where
slot.take()
});
if let Some(module_to_register) = maybe_module {
let maybe_exports = if let Some(module_to_register) = maybe_module {
// SAFETY: napi_register_module guarantees that `module_to_register` is valid.
let nm = unsafe { &*module_to_register };
assert_eq!(nm.nm_version, 1);
// SAFETY: we are going blind, calling the register function on the other side.
let maybe_exports = unsafe {
(nm.nm_register_func)(
env_ptr,
std::mem::transmute::<v8::Local<v8::Value>, napi_value>(exports.into()),
)
};
let exports = if maybe_exports.is_some() {
// SAFETY: v8::Local is a pointer to a value and napi_value is also a pointer
// to a value, they have the same layout
unsafe {
std::mem::transmute::<napi_value, v8::Local<v8::Value>>(maybe_exports)
}
} else {
exports.into()
};
// NAPI addons can't be unloaded, so we're going to "forget" the library
// object so it lives till the program exit.
std::mem::forget(library);
return Ok(exports);
}
// Initializer callback.
// SAFETY: we are going blind, calling the register function on the other side.
let maybe_exports = unsafe {
let Ok(init) = library
.get::<unsafe extern "C" fn(
env: napi_env,
exports: napi_value,
) -> napi_value>(b"napi_register_module_v1") else {
return Err(type_error(format!("Unable to find napi_register_module_v1 symbol in {}", path)));
};
init(
env_ptr,
std::mem::transmute::<v8::Local<v8::Value>, napi_value>(exports.into()),
)
};
let exports = if maybe_exports.is_some() {
// SAFETY: v8::Local is a pointer to a value and napi_value is also a pointer
// to a value, they have the same layout
unsafe {
std::mem::transmute::<napi_value, v8::Local<v8::Value>>(maybe_exports)
}
unsafe { (nm.nm_register_func)(env_ptr, exports.into()) }
} else if let Ok(init) = unsafe {
library.get::<napi_register_module_v1>(b"napi_register_module_v1")
} {
// Initializer callback.
// SAFETY: we are going blind, calling the register function on the other side.
unsafe { init(env_ptr, exports.into()) }
} else {
exports.into()
return Err(type_error(format!(
"Unable to find register Node-API module at {}",
path
)));
};
let exports = maybe_exports.unwrap_or(exports.into());
// NAPI addons can't be unloaded, so we're going to "forget" the library
// object so it lives till the program exit.
std::mem::forget(library);
Ok(exports)
}