From 12e0e5d2ae1ea348d66d62ca98984c829b40afbe Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Wed, 26 Aug 2026 09:36:41 +0200 Subject: [PATCH 1/4] runtime: add Node-API host core --- crates/perry-runtime/Cargo.toml | 3 + crates/perry-runtime/src/gc/mod.rs | 2 + crates/perry-runtime/src/lib.rs | 2 + .../src/node_api_host/functions.rs | 280 ++++ crates/perry-runtime/src/node_api_host/mod.rs | 399 +++++ .../perry-runtime/src/node_api_host/scopes.rs | 336 +++++ .../perry-runtime/src/node_api_host/tests.rs | 527 +++++++ .../perry-runtime/src/node_api_host/values.rs | 1296 +++++++++++++++++ docs/src/internals/node-api-host.md | 19 +- 9 files changed, 2862 insertions(+), 2 deletions(-) create mode 100644 crates/perry-runtime/src/node_api_host/functions.rs create mode 100644 crates/perry-runtime/src/node_api_host/mod.rs create mode 100644 crates/perry-runtime/src/node_api_host/scopes.rs create mode 100644 crates/perry-runtime/src/node_api_host/tests.rs create mode 100644 crates/perry-runtime/src/node_api_host/values.rs diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index f948f9752f..c1059ebe41 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -237,6 +237,9 @@ ohos-napi = [] # this on only when the user's program references `WebAssembly.*`, so non-wasm # programs don't pay an unresolvable-symbol penalty at link time. wasm-host = [] +# #8523: opt-in Node-API ABI and host-core symbols. The compiler enables this +# only for an allowlisted native-addon graph, preserving the default size gate. +node-api-host = [] # #6559: runtime dynamic-code evaluation — `new Function(p1, …, body)` with a # RUNTIME-constructed body parses the generated source with perry-parser (SWC) # and runs it through a scoped tree-walking interpreter (`src/dyn_eval/`). diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 2b0552a4ed..371bd18ace 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -924,6 +924,8 @@ pub fn gc_init() { reg_scanner!(crate::object::scan_arguments_object_roots_mut); // bun:ffi (#6562): the cached FFIType enum object. reg_scanner!(crate::bun_ffi::scan_bun_ffi_roots_mut); + #[cfg(feature = "node-api-host")] + reg_scanner!(crate::node_api_host::scan_node_api_roots_mut); reg_budgeted_scanner!( crate::object::scan_class_side_table_roots_mut, crate::object::scan_class_side_table_roots_mut_step, diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index 202d0f25b2..c58f1bf176 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -103,6 +103,8 @@ pub mod native_handle; pub mod native_value_profile; pub mod navigator; pub mod net_validate; +#[cfg(feature = "node-api-host")] +pub mod node_api_host; mod param_type_guard; // #6468: the `node:http2` constant tables are only reachable through the // `http2` native-module namespace, so a program that never imports `node:http2` diff --git a/crates/perry-runtime/src/node_api_host/functions.rs b/crates/perry-runtime/src/node_api_host/functions.rs new file mode 100644 index 0000000000..e8bdf36dfa --- /dev/null +++ b/crates/perry-runtime/src/node_api_host/functions.rs @@ -0,0 +1,280 @@ +use super::*; +use crate::value::JSValue; +use std::ffi::{c_char, c_void}; + +pub type NapiCallback = Option NapiValue>; + +fn callback_info( + env: NapiEnv, + info: NapiCallbackInfo, + f: impl FnOnce(&CallbackInfoRecord) -> R, +) -> Option { + if info.is_null() { + return None; + } + with_env(env, |env| { + let address = info as usize; + if !env.active_callback_infos.contains(&address) { + return None; + } + let info = unsafe { &*info.cast::() }; + (info.env_serial == env.serial).then(|| f(info)) + }) + .flatten() +} + +fn current_callback_record(index: usize) -> Option { + let env = current_env(); + with_env(env, |env| { + env.callbacks.get(index).map(|record| NativeCallbackRecord { + callback: record.callback, + data: record.data, + }) + }) + .flatten() +} + +extern "C" fn napi_callback_thunk( + closure: *const crate::closure::ClosureHeader, + arguments: f64, +) -> f64 { + let env = current_env(); + let callback_index = crate::closure::js_closure_get_capture_ptr(closure, 0).max(0) as usize; + let Some(callback) = current_callback_record(callback_index) else { + return f64::from_bits(crate::value::TAG_UNDEFINED); + }; + let callback_data = callback.data; + let native_callback: unsafe extern "C" fn(NapiEnv, NapiCallbackInfo) -> NapiValue = + unsafe { std::mem::transmute(callback.callback) }; + + let mut scope = std::ptr::null_mut(); + if unsafe { napi_open_handle_scope(env, &mut scope) } != NapiStatus::Ok { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + + let mut argument_handles = Vec::new(); + let arguments_ptr = + crate::value::js_nanbox_get_pointer(arguments) as *const crate::array::ArrayHeader; + if !arguments_ptr.is_null() { + let length = crate::array::js_array_length(arguments_ptr); + argument_handles.reserve(length as usize); + for index in 0..length { + let bits = crate::array::js_array_get_f64(arguments_ptr, index).to_bits(); + if let Ok(handle) = add_handle(env, bits) { + argument_handles.push(handle); + } + } + } + let this_bits = crate::object::js_implicit_this_get().to_bits(); + let this_value = add_handle(env, this_bits).unwrap_or(std::ptr::null_mut()); + + let mut info = Box::new(CallbackInfoRecord { + env_serial: with_env(env, |env| env.serial).unwrap_or_default(), + args: argument_handles, + this_value, + data: callback_data, + new_target: std::ptr::null_mut(), + }); + let info_ptr = (&mut *info) as *mut CallbackInfoRecord as NapiCallbackInfo; + with_env_mut(env, |env| env.active_callback_infos.push(info_ptr as usize)); + + let returned = unsafe { native_callback(env, info_ptr) }; + let returned_bits = if returned.is_null() { + crate::value::TAG_UNDEFINED + } else { + value_bits(env, returned).unwrap_or(crate::value::TAG_UNDEFINED) + }; + + with_env_mut(env, |env| { + if let Some(position) = env + .active_callback_infos + .iter() + .rposition(|address| *address == info_ptr as usize) + { + env.active_callback_infos.remove(position); + } + }); + unsafe { napi_close_handle_scope(env, scope) }; + drop(info); + + let exception = with_env_mut(env, |env| env.pending_exception_bits.take()).flatten(); + if let Some(exception) = exception { + crate::exception::js_throw(f64::from_bits(exception)); + } + f64::from_bits(returned_bits) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_function( + env: NapiEnv, + utf8name: *const c_char, + length: usize, + callback: NapiCallback, + data: *mut c_void, + result: *mut NapiValue, +) -> NapiStatus { + if result.is_null() || callback.is_none() { + return set_status( + env, + NapiStatus::InvalidArg, + "result and callback must not be null", + ); + } + let name = if utf8name.is_null() { + Vec::new() + } else { + let length = if length == NAPI_AUTO_LENGTH { + std::ffi::CStr::from_ptr(utf8name).to_bytes().len() + } else { + length + }; + std::slice::from_raw_parts(utf8name.cast::(), length).to_vec() + }; + let callback = callback.unwrap() as usize; + let callback_index = match with_env_mut(env, |env| { + let index = env.callbacks.len(); + env.callbacks.push(NativeCallbackRecord { + callback, + data: data as usize, + }); + index + }) { + Some(index) => index, + None => return NapiStatus::InvalidArg, + }; + + let function_pointer = napi_callback_thunk as *const u8; + crate::closure::js_register_closure_synthetic_arguments(function_pointer, 0); + crate::closure::js_register_closure_arity(function_pointer, 0); + crate::closure::js_register_closure_length(function_pointer, 0); + let closure = crate::closure::js_closure_alloc(function_pointer, 1); + crate::closure::js_closure_set_capture_ptr(closure, 0, callback_index as i64); + let handle = match add_handle(env, JSValue::pointer(closure.cast()).bits()) { + Ok(handle) => handle, + Err(status) => return status, + }; + + if !name.is_empty() { + let name_ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name_value = f64::from_bits(JSValue::string_ptr(name_ptr).bits()); + let closure_bits = match value_bits(env, handle) { + Ok(bits) => bits, + Err(status) => return status, + }; + let closure_ptr = JSValue::from_bits(closure_bits).as_pointer::() as usize; + crate::closure::closure_set_dynamic_prop(closure_ptr, "name", name_value); + } + *result = handle; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_cb_info( + env: NapiEnv, + info: NapiCallbackInfo, + argc: *mut usize, + argv: *mut NapiValue, + this_arg: *mut NapiValue, + data: *mut *mut c_void, +) -> NapiStatus { + if argc.is_null() { + return set_status(env, NapiStatus::InvalidArg, "argc must not be null"); + } + let capacity = *argc; + let Some((args, this_value, callback_data)) = callback_info(env, info, |info| { + (info.args.clone(), info.this_value, info.data) + }) else { + return set_status(env, NapiStatus::InvalidArg, "callback info is not active"); + }; + if !argv.is_null() { + for (index, argument) in args.iter().take(capacity).enumerate() { + *argv.add(index) = *argument; + } + } + *argc = args.len(); + if !this_arg.is_null() { + *this_arg = this_value; + } + if !data.is_null() { + *data = callback_data as *mut c_void; + } + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_new_target( + env: NapiEnv, + info: NapiCallbackInfo, + result: *mut NapiValue, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Some(new_target) = callback_info(env, info, |info| info.new_target) else { + return set_status(env, NapiStatus::InvalidArg, "callback info is not active"); + }; + *result = new_target; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_call_function( + env: NapiEnv, + recv: NapiValue, + function: NapiValue, + argc: usize, + argv: *const NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + if pending_exception(env).is_some() { + return set_status(env, NapiStatus::PendingException, "an exception is pending"); + } + if recv.is_null() || (argc != 0 && argv.is_null()) { + return set_status( + env, + NapiStatus::InvalidArg, + "invalid receiver or argument vector", + ); + } + let Ok(receiver_bits) = value_bits(env, recv) else { + return set_status(env, NapiStatus::InvalidArg, "receiver is not a live handle"); + }; + let Ok(function_bits) = value_bits(env, function) else { + return set_status(env, NapiStatus::InvalidArg, "function is not a live handle"); + }; + let function_value = JSValue::from_bits(function_bits); + if !function_value.is_pointer() + || !crate::closure::is_closure_ptr(function_value.as_pointer::() as usize) + { + return set_status(env, NapiStatus::FunctionExpected, "value must be callable"); + } + let mut arguments = Vec::with_capacity(argc); + for index in 0..argc { + let handle = *argv.add(index); + let Ok(bits) = value_bits(env, handle) else { + return set_status(env, NapiStatus::InvalidArg, "argument is not a live handle"); + }; + arguments.push(f64::from_bits(bits)); + } + let previous_this = crate::object::js_implicit_this_set(f64::from_bits(receiver_bits)); + let call_result = catch_value_call(env, || { + crate::closure::js_native_call_value( + f64::from_bits(function_bits), + arguments.as_ptr(), + arguments.len(), + ) + }); + crate::object::js_implicit_this_set(previous_this); + match call_result { + Ok(value) => { + if !result.is_null() { + let Ok(handle) = add_handle(env, value.to_bits()) else { + return NapiStatus::InvalidArg; + }; + *result = handle; + } + ok(env) + } + Err(status) => status, + } +} diff --git a/crates/perry-runtime/src/node_api_host/mod.rs b/crates/perry-runtime/src/node_api_host/mod.rs new file mode 100644 index 0000000000..022bb0cd5f --- /dev/null +++ b/crates/perry-runtime/src/node_api_host/mod.rs @@ -0,0 +1,399 @@ +#![allow(clippy::missing_safety_doc)] +//! Node-API host core (#8523). +//! +//! Addons see opaque `napi_value` tokens, never Perry heap addresses. Each +//! token carries an environment-local slot index and generation. Slots belong +//! to a strict handle-scope stack and are mutable GC roots, so a copying +//! collection rewrites the actual storage an addon will later read. +//! +//! The exported functions use Node-API's raw-pointer ABI. Their pointer +//! validity requirements are defined by `js_native_api.h`; each entry point +//! validates nullable arguments before dereferencing them. + +mod functions; +mod scopes; +mod values; + +use std::cell::RefCell; +use std::ffi::{c_char, c_void, CString}; +use std::sync::atomic::{AtomicU64, Ordering}; + +pub use functions::*; +pub use scopes::*; +pub use values::*; + +pub type NapiEnv = *mut c_void; +pub type NapiValue = *mut c_void; +pub type NapiHandleScope = *mut c_void; +pub type NapiEscapableHandleScope = *mut c_void; +pub type NapiRef = *mut c_void; +pub type NapiCallbackInfo = *mut c_void; + +pub const NAPI_AUTO_LENGTH: usize = usize::MAX; +pub const NAPI_VERSION: u32 = 8; + +#[repr(i32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NapiStatus { + Ok = 0, + InvalidArg = 1, + ObjectExpected = 2, + StringExpected = 3, + NameExpected = 4, + FunctionExpected = 5, + NumberExpected = 6, + BooleanExpected = 7, + ArrayExpected = 8, + GenericFailure = 9, + PendingException = 10, + Cancelled = 11, + EscapeCalledTwice = 12, + HandleScopeMismatch = 13, + CallbackScopeMismatch = 14, + QueueFull = 15, + Closing = 16, + BigintExpected = 17, + DateExpected = 18, + ArraybufferExpected = 19, + DetachableArraybufferExpected = 20, + WouldDeadlock = 21, + NoExternalBuffersAllowed = 22, + CannotRunJs = 23, +} + +#[repr(C)] +pub struct NapiExtendedErrorInfo { + pub error_message: *const c_char, + pub engine_reserved: *mut c_void, + pub engine_error_code: u32, + pub error_code: NapiStatus, +} + +#[derive(Clone, Copy)] +pub(crate) struct HandleSlot { + pub value_bits: u64, + pub generation: u32, + pub scope_depth: u32, + pub live: bool, +} + +pub(crate) struct HandleToken { + env_serial: u64, + slot: u32, + generation: u32, +} + +pub(crate) struct ScopeToken { + env_serial: u64, + depth: u32, + escapable: bool, + escaped: bool, + closed: bool, +} + +pub(crate) struct ReferenceRecord { + env_serial: u64, + value_bits: u64, + refcount: u32, + deleted: bool, +} + +pub(crate) struct NativeCallbackRecord { + pub callback: usize, + pub data: usize, +} + +pub(crate) struct CallbackInfoRecord { + pub env_serial: u64, + pub args: Vec, + pub this_value: NapiValue, + pub data: usize, + pub new_target: NapiValue, +} + +// The boxed records are intentional: their addresses are the opaque pointers +// returned to addon code and must survive growth of the owning vectors. +#[allow(clippy::vec_box)] +pub(crate) struct Env { + serial: u64, + owner: std::thread::ThreadId, + slots: Vec, + tokens: Vec>, + scopes: Vec<*mut ScopeToken>, + scope_tokens: Vec>, + references: Vec>, + callbacks: Vec, + active_callback_infos: Vec, + pending_exception_bits: Option, + last_status: NapiStatus, + last_error_message: CString, + error_info: NapiExtendedErrorInfo, +} + +impl Env { + fn new(serial: u64) -> Self { + let mut env = Self { + serial, + owner: std::thread::current().id(), + slots: Vec::new(), + tokens: Vec::new(), + scopes: Vec::new(), + scope_tokens: Vec::new(), + references: Vec::new(), + callbacks: Vec::new(), + active_callback_infos: Vec::new(), + pending_exception_bits: None, + last_status: NapiStatus::Ok, + last_error_message: CString::new("napi_ok").unwrap(), + error_info: NapiExtendedErrorInfo { + error_message: std::ptr::null(), + engine_reserved: std::ptr::null_mut(), + engine_error_code: 0, + error_code: NapiStatus::Ok, + }, + }; + env.refresh_error_info(); + env + } + + fn refresh_error_info(&mut self) { + self.error_info.error_message = self.last_error_message.as_ptr(); + self.error_info.error_code = self.last_status; + } + + fn set_status(&mut self, status: NapiStatus, message: &'static str) -> NapiStatus { + self.last_status = status; + self.last_error_message = CString::new(message).expect("static N-API error has no NUL"); + self.refresh_error_info(); + status + } + + fn current_scope_depth(&self) -> u32 { + self.scopes.len() as u32 + } + + fn add_handle_at_depth(&mut self, value_bits: u64, scope_depth: u32) -> NapiValue { + let slot = self.slots.len() as u32; + let generation = 1; + self.slots.push(HandleSlot { + value_bits, + generation, + scope_depth, + live: true, + }); + let mut token = Box::new(HandleToken { + env_serial: self.serial, + slot, + generation, + }); + let ptr = (&mut *token) as *mut HandleToken as NapiValue; + self.tokens.push(token); + ptr + } + + fn add_handle(&mut self, value_bits: u64) -> NapiValue { + self.add_handle_at_depth(value_bits, self.current_scope_depth()) + } + + fn token(&self, value: NapiValue) -> Option<&HandleToken> { + if value.is_null() { + return None; + } + self.tokens + .iter() + .find(|token| std::ptr::eq(token.as_ref(), value.cast::())) + .map(Box::as_ref) + } + + fn value_bits(&self, value: NapiValue) -> Option { + let token = self.token(value)?; + if token.env_serial != self.serial { + return None; + } + let slot = self.slots.get(token.slot as usize)?; + (slot.live && slot.generation == token.generation).then_some(slot.value_bits) + } + + fn invalidate_scope(&mut self, depth: u32) { + for slot in &mut self.slots { + if slot.live && slot.scope_depth >= depth { + slot.live = false; + slot.generation = slot.generation.wrapping_add(1).max(1); + slot.value_bits = crate::value::TAG_UNDEFINED; + } + } + } + + fn reference(&self, reference: NapiRef) -> Option<&ReferenceRecord> { + if reference.is_null() { + return None; + } + self.references + .iter() + .find(|record| std::ptr::eq(record.as_ref(), reference.cast::())) + .map(Box::as_ref) + .filter(|record| record.env_serial == self.serial && !record.deleted) + } + + fn reference_mut(&mut self, reference: NapiRef) -> Option<&mut ReferenceRecord> { + if reference.is_null() { + return None; + } + self.references + .iter_mut() + .find(|record| std::ptr::eq(record.as_ref(), reference.cast::())) + .map(Box::as_mut) + .filter(|record| record.env_serial == self.serial && !record.deleted) + } +} + +static NEXT_ENV_SERIAL: AtomicU64 = AtomicU64::new(1); + +crate::perry_thread_local! { + static NODE_API_ENV: RefCell>> = const { RefCell::new(None) }; +} + +/// Return the current Perry agent's lazily-created Node-API environment. +pub fn current_env() -> NapiEnv { + NODE_API_ENV.with(|cell| { + let mut env = cell.borrow_mut(); + if env.is_none() { + *env = Some(Box::new(Env::new( + NEXT_ENV_SERIAL.fetch_add(1, Ordering::Relaxed), + ))); + } + env.as_deref_mut().unwrap() as *mut Env as NapiEnv + }) +} + +pub(crate) fn with_env(env: NapiEnv, f: impl FnOnce(&Env) -> R) -> Option { + if env.is_null() { + return None; + } + NODE_API_ENV.with(|cell| { + let borrowed = cell.borrow(); + let current = borrowed.as_deref()?; + if !std::ptr::eq(current, env.cast::()) || current.owner != std::thread::current().id() + { + return None; + } + Some(f(current)) + }) +} + +/// `f` must not allocate in Perry's GC heap. Callers copy inputs out, drop the +/// borrow, allocate, then re-enter only to publish the resulting root slot. +pub(crate) fn with_env_mut(env: NapiEnv, f: impl FnOnce(&mut Env) -> R) -> Option { + if env.is_null() { + return None; + } + NODE_API_ENV.with(|cell| { + let mut borrowed = cell.borrow_mut(); + let current = borrowed.as_deref_mut()?; + if !std::ptr::eq(current, env.cast::()) || current.owner != std::thread::current().id() + { + return None; + } + Some(f(current)) + }) +} + +pub(crate) fn value_bits(env: NapiEnv, value: NapiValue) -> Result { + with_env(env, |env| env.value_bits(value)) + .flatten() + .ok_or(NapiStatus::InvalidArg) +} + +pub(crate) fn add_handle(env: NapiEnv, value_bits: u64) -> Result { + with_env_mut(env, |env| env.add_handle(value_bits)).ok_or(NapiStatus::InvalidArg) +} + +pub(crate) fn set_status(env: NapiEnv, status: NapiStatus, message: &'static str) -> NapiStatus { + with_env_mut(env, |env| env.set_status(status, message)).unwrap_or(NapiStatus::InvalidArg) +} + +pub(crate) fn ok(env: NapiEnv) -> NapiStatus { + set_status(env, NapiStatus::Ok, "napi_ok") +} + +pub(crate) fn pending_exception(env: NapiEnv) -> Option { + with_env(env, |env| env.pending_exception_bits).flatten() +} + +pub(crate) fn store_pending_exception(env: NapiEnv, bits: u64) -> NapiStatus { + with_env_mut(env, |env| { + env.pending_exception_bits = Some(bits); + env.set_status(NapiStatus::PendingException, "an exception is pending") + }) + .unwrap_or(NapiStatus::InvalidArg) +} + +pub(crate) fn catch_value_call(env: NapiEnv, f: impl FnOnce() -> f64) -> Result { + match crate::exception::js_call_catching(f) { + Ok(value) => Ok(value), + Err(exception) => { + store_pending_exception(env, exception.to_bits()); + Err(NapiStatus::PendingException) + } + } +} + +/// Mark and rewrite every native-owned Node-API root. +pub fn scan_node_api_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { + NODE_API_ENV.with(|cell| { + let mut borrowed = cell.borrow_mut(); + let Some(env) = borrowed.as_deref_mut() else { + return; + }; + for slot in &mut env.slots { + if slot.live { + visitor.visit_nanbox_u64_slot(&mut slot.value_bits); + } + } + if let Some(exception) = env.pending_exception_bits.as_mut() { + visitor.visit_nanbox_u64_slot(exception); + } + for reference in &mut env.references { + if !reference.deleted && reference.refcount > 0 { + visitor.visit_nanbox_u64_slot(&mut reference.value_bits); + } + } + }); +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_last_error_info( + env: NapiEnv, + result: *mut *const NapiExtendedErrorInfo, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + match with_env_mut(env, |env| { + env.refresh_error_info(); + &env.error_info as *const NapiExtendedErrorInfo + }) { + Some(info) => { + *result = info; + NapiStatus::Ok + } + None => NapiStatus::InvalidArg, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_version(env: NapiEnv, result: *mut u32) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + *result = NAPI_VERSION; + ok(env) +} + +#[cfg(test)] +pub(crate) fn reset_env_for_test() { + NODE_API_ENV.with(|cell| *cell.borrow_mut() = None); +} + +#[cfg(test)] +mod tests; diff --git a/crates/perry-runtime/src/node_api_host/scopes.rs b/crates/perry-runtime/src/node_api_host/scopes.rs new file mode 100644 index 0000000000..3d148b48eb --- /dev/null +++ b/crates/perry-runtime/src/node_api_host/scopes.rs @@ -0,0 +1,336 @@ +use super::*; +use std::ffi::{c_char, c_void}; + +fn open_scope(env: NapiEnv, escapable: bool, result: *mut *mut c_void) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let scope = with_env_mut(env, |env| { + let depth = env.scopes.len() as u32 + 1; + let mut token = Box::new(ScopeToken { + env_serial: env.serial, + depth, + escapable, + escaped: false, + closed: false, + }); + let ptr = (&mut *token) as *mut ScopeToken; + env.scopes.push(ptr); + env.scope_tokens.push(token); + ptr.cast::() + }); + let Some(scope) = scope else { + return NapiStatus::InvalidArg; + }; + unsafe { *result = scope }; + ok(env) +} + +fn close_scope(env: NapiEnv, scope: *mut c_void, escapable: bool) -> NapiStatus { + if scope.is_null() { + return set_status(env, NapiStatus::InvalidArg, "scope must not be null"); + } + with_env_mut(env, |env| { + let Some(&top) = env.scopes.last() else { + return env.set_status( + NapiStatus::HandleScopeMismatch, + "handle scopes must close in LIFO order", + ); + }; + if !std::ptr::eq(top, scope.cast::()) { + return env.set_status( + NapiStatus::HandleScopeMismatch, + "handle scopes must close in LIFO order", + ); + } + let Some(token) = env + .scope_tokens + .iter_mut() + .find(|token| std::ptr::eq(token.as_ref(), top)) + else { + return env.set_status(NapiStatus::InvalidArg, "unknown handle scope"); + }; + if token.closed || token.env_serial != env.serial || token.escapable != escapable { + return env.set_status( + NapiStatus::HandleScopeMismatch, + "handle scope kind mismatch", + ); + } + let depth = token.depth; + token.closed = true; + env.scopes.pop(); + env.invalidate_scope(depth); + env.set_status(NapiStatus::Ok, "napi_ok") + }) + .unwrap_or(NapiStatus::InvalidArg) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_open_handle_scope( + env: NapiEnv, + result: *mut NapiHandleScope, +) -> NapiStatus { + open_scope(env, false, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_close_handle_scope( + env: NapiEnv, + scope: NapiHandleScope, +) -> NapiStatus { + close_scope(env, scope, false) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_open_escapable_handle_scope( + env: NapiEnv, + result: *mut NapiEscapableHandleScope, +) -> NapiStatus { + open_scope(env, true, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_close_escapable_handle_scope( + env: NapiEnv, + scope: NapiEscapableHandleScope, +) -> NapiStatus { + close_scope(env, scope, true) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_escape_handle( + env: NapiEnv, + scope: NapiEscapableHandleScope, + escapee: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + if scope.is_null() || result.is_null() { + return set_status( + env, + NapiStatus::InvalidArg, + "scope and result must not be null", + ); + } + let Ok(bits) = value_bits(env, escapee) else { + return set_status(env, NapiStatus::InvalidArg, "escapee is not a live handle"); + }; + let escaped = with_env_mut(env, |env| { + let Some(&top) = env.scopes.last() else { + return Err(NapiStatus::HandleScopeMismatch); + }; + if !std::ptr::eq(top, scope.cast::()) { + return Err(NapiStatus::HandleScopeMismatch); + } + let Some(token) = env + .scope_tokens + .iter_mut() + .find(|token| std::ptr::eq(token.as_ref(), top)) + else { + return Err(NapiStatus::InvalidArg); + }; + if !token.escapable || token.closed { + return Err(NapiStatus::HandleScopeMismatch); + } + if token.escaped { + return Err(NapiStatus::EscapeCalledTwice); + } + token.escaped = true; + let parent_depth = token.depth.saturating_sub(1); + Ok(env.add_handle_at_depth(bits, parent_depth)) + }); + match escaped { + Some(Ok(handle)) => { + *result = handle; + ok(env) + } + Some(Err(NapiStatus::EscapeCalledTwice)) => set_status( + env, + NapiStatus::EscapeCalledTwice, + "an escapable handle scope may escape only once", + ), + Some(Err(status)) => set_status(env, status, "handle scope mismatch"), + None => NapiStatus::InvalidArg, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_reference( + env: NapiEnv, + value: NapiValue, + initial_refcount: u32, + result: *mut NapiRef, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + if initial_refcount == 0 { + return set_status( + env, + NapiStatus::GenericFailure, + "weak Node-API references are not enabled in this host core", + ); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let reference = with_env_mut(env, |env| { + let mut record = Box::new(ReferenceRecord { + env_serial: env.serial, + value_bits: bits, + refcount: initial_refcount, + deleted: false, + }); + let ptr = (&mut *record) as *mut ReferenceRecord as NapiRef; + env.references.push(record); + ptr + }); + let Some(reference) = reference else { + return NapiStatus::InvalidArg; + }; + *result = reference; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_delete_reference(env: NapiEnv, reference: NapiRef) -> NapiStatus { + with_env_mut(env, |env| { + let Some(reference) = env.reference_mut(reference) else { + return env.set_status(NapiStatus::InvalidArg, "reference is not live"); + }; + reference.deleted = true; + reference.value_bits = crate::value::TAG_UNDEFINED; + env.set_status(NapiStatus::Ok, "napi_ok") + }) + .unwrap_or(NapiStatus::InvalidArg) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_reference_ref( + env: NapiEnv, + reference: NapiRef, + result: *mut u32, +) -> NapiStatus { + with_env_mut(env, |env| { + let Some(reference) = env.reference_mut(reference) else { + return env.set_status(NapiStatus::InvalidArg, "reference is not live"); + }; + reference.refcount = reference.refcount.saturating_add(1); + if !result.is_null() { + *result = reference.refcount; + } + env.set_status(NapiStatus::Ok, "napi_ok") + }) + .unwrap_or(NapiStatus::InvalidArg) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_reference_unref( + env: NapiEnv, + reference: NapiRef, + result: *mut u32, +) -> NapiStatus { + with_env_mut(env, |env| { + let Some(reference) = env.reference_mut(reference) else { + return env.set_status(NapiStatus::InvalidArg, "reference is not live"); + }; + if reference.refcount <= 1 { + return env.set_status( + NapiStatus::GenericFailure, + "weak Node-API references are not enabled in this host core", + ); + } + reference.refcount -= 1; + if !result.is_null() { + *result = reference.refcount; + } + env.set_status(NapiStatus::Ok, "napi_ok") + }) + .unwrap_or(NapiStatus::InvalidArg) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_reference_value( + env: NapiEnv, + reference: NapiRef, + result: *mut NapiValue, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let bits = with_env(env, |env| { + env.reference(reference).map(|record| record.value_bits) + }); + let Some(Some(bits)) = bits else { + return set_status(env, NapiStatus::InvalidArg, "reference is not live"); + }; + let Ok(handle) = add_handle(env, bits) else { + return NapiStatus::InvalidArg; + }; + *result = handle; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_throw(env: NapiEnv, error: NapiValue) -> NapiStatus { + let Ok(bits) = value_bits(env, error) else { + return set_status(env, NapiStatus::InvalidArg, "error is not a live handle"); + }; + if with_env_mut(env, |env| env.pending_exception_bits = Some(bits)).is_none() { + return NapiStatus::InvalidArg; + } + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_is_exception_pending(env: NapiEnv, result: *mut bool) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + *result = pending_exception(env).is_some(); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_and_clear_last_exception( + env: NapiEnv, + result: *mut NapiValue, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let bits = with_env_mut(env, |env| env.pending_exception_bits.take()); + let Some(bits) = bits else { + return NapiStatus::InvalidArg; + }; + let handle = add_handle(env, bits.unwrap_or(crate::value::TAG_UNDEFINED)) + .expect("validated environment disappeared"); + *result = handle; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_fatal_error( + location: *const c_char, + location_len: usize, + message: *const c_char, + message_len: usize, +) -> ! { + fn bytes(ptr: *const c_char, len: usize) -> String { + if ptr.is_null() { + return String::new(); + } + let len = if len == NAPI_AUTO_LENGTH { + unsafe { std::ffi::CStr::from_ptr(ptr).to_bytes().len() } + } else { + len + }; + String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(ptr.cast::(), len) }) + .into_owned() + } + eprintln!( + "Perry Node-API fatal error at {}: {}", + bytes(location, location_len), + bytes(message, message_len) + ); + std::process::abort() +} diff --git a/crates/perry-runtime/src/node_api_host/tests.rs b/crates/perry-runtime/src/node_api_host/tests.rs new file mode 100644 index 0000000000..491902e72b --- /dev/null +++ b/crates/perry-runtime/src/node_api_host/tests.rs @@ -0,0 +1,527 @@ +use super::*; +use std::ffi::{c_void, CString}; + +fn test_env() -> NapiEnv { + crate::gc::ensure_gc_initialized(); + reset_env_for_test(); + current_env() +} + +fn int32(env: NapiEnv, value: i32) -> NapiValue { + let mut result = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_int32(env, value, &mut result) }, + NapiStatus::Ok + ); + result +} + +fn read_int32(env: NapiEnv, value: NapiValue) -> i32 { + let mut result = 0; + assert_eq!( + unsafe { napi_get_value_int32(env, value, &mut result) }, + NapiStatus::Ok + ); + result +} + +#[test] +fn reports_supported_node_api_version() { + let env = test_env(); + let mut version = 0; + assert_eq!( + unsafe { napi_get_version(env, &mut version) }, + NapiStatus::Ok + ); + assert_eq!(version, NAPI_VERSION); +} + +#[test] +fn primitive_values_round_trip_and_report_types() { + let env = test_env(); + let number = int32(env, -42); + assert_eq!(read_int32(env, number), -42); + + let mut value_type = NapiValueType::Undefined; + assert_eq!( + unsafe { napi_typeof(env, number, &mut value_type) }, + NapiStatus::Ok + ); + assert_eq!(value_type, NapiValueType::Number); + + let mut boolean = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_boolean(env, true, &mut boolean) }, + NapiStatus::Ok + ); + let mut unboxed = false; + assert_eq!( + unsafe { napi_get_value_bool(env, boolean, &mut unboxed) }, + NapiStatus::Ok + ); + assert!(unboxed); + assert_eq!( + unsafe { napi_get_value_double(env, boolean, std::ptr::null_mut()) }, + NapiStatus::InvalidArg + ); +} + +#[test] +fn handle_scopes_are_lifo_and_invalidate_local_handles() { + let env = test_env(); + let mut outer = std::ptr::null_mut(); + let mut inner = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_open_handle_scope(env, &mut outer) }, + NapiStatus::Ok + ); + let outer_value = int32(env, 1); + assert_eq!( + unsafe { napi_open_handle_scope(env, &mut inner) }, + NapiStatus::Ok + ); + let inner_value = int32(env, 2); + + assert_eq!( + unsafe { napi_close_handle_scope(env, outer) }, + NapiStatus::HandleScopeMismatch + ); + assert_eq!( + unsafe { napi_close_handle_scope(env, inner) }, + NapiStatus::Ok + ); + let mut ignored = 0; + assert_eq!( + unsafe { napi_get_value_int32(env, inner_value, &mut ignored) }, + NapiStatus::InvalidArg + ); + assert_eq!(read_int32(env, outer_value), 1); + assert_eq!( + unsafe { napi_close_handle_scope(env, outer) }, + NapiStatus::Ok + ); +} + +#[test] +fn escapable_scope_promotes_exactly_one_handle() { + let env = test_env(); + let mut scope = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_open_escapable_handle_scope(env, &mut scope) }, + NapiStatus::Ok + ); + let local = int32(env, 73); + let mut escaped = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_escape_handle(env, scope, local, &mut escaped) }, + NapiStatus::Ok + ); + let mut second = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_escape_handle(env, scope, local, &mut second) }, + NapiStatus::EscapeCalledTwice + ); + assert_eq!( + unsafe { napi_close_escapable_handle_scope(env, scope) }, + NapiStatus::Ok + ); + assert_eq!(read_int32(env, escaped), 73); +} + +#[test] +fn utf8_latin1_and_utf16_strings_round_trip() { + let env = test_env(); + let utf8 = CString::new("Perry 🦜").unwrap(); + let mut string = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_string_utf8(env, utf8.as_ptr(), NAPI_AUTO_LENGTH, &mut string) }, + NapiStatus::Ok + ); + let mut byte_length = 0; + assert_eq!( + unsafe { + napi_get_value_string_utf8(env, string, std::ptr::null_mut(), 0, &mut byte_length) + }, + NapiStatus::Ok + ); + let mut bytes = vec![0i8; byte_length + 1]; + let mut copied = 0; + assert_eq!( + unsafe { + napi_get_value_string_utf8(env, string, bytes.as_mut_ptr(), bytes.len(), &mut copied) + }, + NapiStatus::Ok + ); + assert_eq!(copied, utf8.as_bytes().len()); + assert_eq!( + unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast::(), copied) }, + utf8.as_bytes() + ); + + let utf16 = [0x0041, 0xd800, 0xd83d, 0xde80]; + let mut wtf16 = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_string_utf16(env, utf16.as_ptr(), utf16.len(), &mut wtf16) }, + NapiStatus::Ok + ); + let mut out = [0u16; 8]; + let mut units = 0; + assert_eq!( + unsafe { napi_get_value_string_utf16(env, wtf16, out.as_mut_ptr(), out.len(), &mut units) }, + NapiStatus::Ok + ); + assert_eq!(&out[..units], &utf16); + + let latin1 = [0x41u8, 0xe9]; + let mut latin_string = std::ptr::null_mut(); + assert_eq!( + unsafe { + napi_create_string_latin1(env, latin1.as_ptr().cast(), latin1.len(), &mut latin_string) + }, + NapiStatus::Ok + ); + let mut latin_out = [0i8; 3]; + let mut latin_len = 0; + assert_eq!( + unsafe { + napi_get_value_string_latin1( + env, + latin_string, + latin_out.as_mut_ptr(), + latin_out.len(), + &mut latin_len, + ) + }, + NapiStatus::Ok + ); + assert_eq!( + unsafe { std::slice::from_raw_parts(latin_out.as_ptr().cast::(), latin_len) }, + latin1 + ); +} + +#[test] +fn objects_arrays_and_named_properties_interoperate() { + let env = test_env(); + let mut object = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_object(env, &mut object) }, + NapiStatus::Ok + ); + let value = int32(env, 99); + assert_eq!( + unsafe { napi_set_named_property(env, object, c"answer".as_ptr(), value) }, + NapiStatus::Ok + ); + let mut present = false; + assert_eq!( + unsafe { napi_has_named_property(env, object, c"answer".as_ptr(), &mut present) }, + NapiStatus::Ok + ); + assert!(present); + let mut read = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_named_property(env, object, c"answer".as_ptr(), &mut read) }, + NapiStatus::Ok + ); + assert_eq!(read_int32(env, read), 99); + + let mut array = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_array_with_length(env, 2, &mut array) }, + NapiStatus::Ok + ); + assert_eq!( + unsafe { napi_set_element(env, array, 1, value) }, + NapiStatus::Ok + ); + let mut length = 0; + assert_eq!( + unsafe { napi_get_array_length(env, array, &mut length) }, + NapiStatus::Ok + ); + assert_eq!(length, 2); + assert_eq!( + unsafe { napi_get_element(env, array, 1, &mut read) }, + NapiStatus::Ok + ); + assert_eq!(read_int32(env, read), 99); +} + +#[test] +fn pending_exceptions_and_strong_references_are_roots() { + let env = test_env(); + let mut scope = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_open_handle_scope(env, &mut scope) }, + NapiStatus::Ok + ); + let value = int32(env, 17); + let mut reference = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_reference(env, value, 1, &mut reference) }, + NapiStatus::Ok + ); + assert_eq!(unsafe { napi_throw(env, value) }, NapiStatus::Ok); + assert_eq!( + unsafe { napi_close_handle_scope(env, scope) }, + NapiStatus::Ok + ); + + let mut pending = false; + assert_eq!( + unsafe { napi_is_exception_pending(env, &mut pending) }, + NapiStatus::Ok + ); + assert!(pending); + let mut exception = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_and_clear_last_exception(env, &mut exception) }, + NapiStatus::Ok + ); + assert_eq!(read_int32(env, exception), 17); + + let mut referenced = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_reference_value(env, reference, &mut referenced) }, + NapiStatus::Ok + ); + assert_eq!(read_int32(env, referenced), 17); + assert_eq!( + unsafe { napi_delete_reference(env, reference) }, + NapiStatus::Ok + ); +} + +#[test] +fn bigint_date_symbol_and_error_helpers_use_node_api_semantics() { + let env = test_env(); + + let mut bigint = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_bigint_int64(env, -7, &mut bigint) }, + NapiStatus::Ok + ); + let mut signed = 0; + let mut lossless = false; + assert_eq!( + unsafe { napi_get_value_bigint_int64(env, bigint, &mut signed, &mut lossless) }, + NapiStatus::Ok + ); + assert_eq!(signed, -7); + assert!(lossless); + let mut unsigned = 0; + assert_eq!( + unsafe { napi_get_value_bigint_uint64(env, bigint, &mut unsigned, &mut lossless) }, + NapiStatus::Ok + ); + assert_eq!(unsigned, (-7i64) as u64); + assert!(!lossless); + + let mut date = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_date(env, 1_234.5, &mut date) }, + NapiStatus::Ok + ); + let mut is_date = false; + assert_eq!( + unsafe { napi_is_date(env, date, &mut is_date) }, + NapiStatus::Ok + ); + assert!(is_date); + let mut timestamp = 0.0; + assert_eq!( + unsafe { napi_get_date_value(env, date, &mut timestamp) }, + NapiStatus::Ok + ); + assert_eq!(timestamp, 1_234.5); + + let description = CString::new("identity").unwrap(); + let mut description_value = std::ptr::null_mut(); + assert_eq!( + unsafe { + napi_create_string_utf8( + env, + description.as_ptr(), + NAPI_AUTO_LENGTH, + &mut description_value, + ) + }, + NapiStatus::Ok + ); + let mut symbol = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_symbol(env, description_value, &mut symbol) }, + NapiStatus::Ok + ); + let mut value_type = NapiValueType::Undefined; + assert_eq!( + unsafe { napi_typeof(env, symbol, &mut value_type) }, + NapiStatus::Ok + ); + assert_eq!(value_type, NapiValueType::Symbol); + + assert_eq!( + unsafe { napi_throw_type_error(env, c"ERR_TEST".as_ptr(), c"boom".as_ptr()) }, + NapiStatus::Ok + ); + let mut pending = false; + assert_eq!( + unsafe { napi_is_exception_pending(env, &mut pending) }, + NapiStatus::Ok + ); + assert!(pending); + let mut error = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_and_clear_last_exception(env, &mut error) }, + NapiStatus::Ok + ); + let mut is_error = false; + assert_eq!( + unsafe { napi_is_error(env, error, &mut is_error) }, + NapiStatus::Ok + ); + assert!(is_error); +} + +unsafe extern "C" fn add_callback(env: NapiEnv, info: NapiCallbackInfo) -> NapiValue { + let mut argc = 2; + let mut argv = [std::ptr::null_mut(); 2]; + let mut data = std::ptr::null_mut(); + assert_eq!( + napi_get_cb_info( + env, + info, + &mut argc, + argv.as_mut_ptr(), + std::ptr::null_mut(), + &mut data, + ), + NapiStatus::Ok + ); + assert_eq!(argc, 2); + assert_eq!(data as usize, 0x8523); + let sum = read_int32(env, argv[0]) + read_int32(env, argv[1]); + int32(env, sum) +} + +unsafe extern "C" fn throwing_callback(env: NapiEnv, _info: NapiCallbackInfo) -> NapiValue { + assert_eq!( + napi_throw_type_error(env, std::ptr::null(), c"callback failed".as_ptr()), + NapiStatus::Ok + ); + std::ptr::null_mut() +} + +#[test] +fn native_callbacks_receive_arguments_data_and_return_values() { + let env = test_env(); + let mut function = std::ptr::null_mut(); + assert_eq!( + unsafe { + napi_create_function( + env, + c"add".as_ptr(), + NAPI_AUTO_LENGTH, + Some(add_callback), + 0x8523usize as *mut c_void, + &mut function, + ) + }, + NapiStatus::Ok + ); + let mut value_type = NapiValueType::Undefined; + assert_eq!( + unsafe { napi_typeof(env, function, &mut value_type) }, + NapiStatus::Ok + ); + assert_eq!(value_type, NapiValueType::Function); + + let mut receiver = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_undefined(env, &mut receiver) }, + NapiStatus::Ok + ); + let arguments = [int32(env, 20), int32(env, 22)]; + let mut result = std::ptr::null_mut(); + assert_eq!( + unsafe { + napi_call_function( + env, + receiver, + function, + arguments.len(), + arguments.as_ptr(), + &mut result, + ) + }, + NapiStatus::Ok + ); + assert_eq!(read_int32(env, result), 42); +} + +#[test] +fn native_callback_exceptions_are_caught_before_returning_to_addon_code() { + let env = test_env(); + let mut function = std::ptr::null_mut(); + assert_eq!( + unsafe { + napi_create_function( + env, + c"fail".as_ptr(), + NAPI_AUTO_LENGTH, + Some(throwing_callback), + std::ptr::null_mut(), + &mut function, + ) + }, + NapiStatus::Ok + ); + let mut receiver = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_undefined(env, &mut receiver) }, + NapiStatus::Ok + ); + assert_eq!( + unsafe { + napi_call_function( + env, + receiver, + function, + 0, + std::ptr::null(), + std::ptr::null_mut(), + ) + }, + NapiStatus::PendingException + ); + let mut exception = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_get_and_clear_last_exception(env, &mut exception) }, + NapiStatus::Ok + ); + let mut is_error = false; + assert_eq!( + unsafe { napi_is_error(env, exception, &mut is_error) }, + NapiStatus::Ok + ); + assert!(is_error); +} + +#[test] +fn node_api_handles_are_rewritten_by_a_collection() { + let env = test_env(); + let text = CString::new("a rooted Node-API string that outlives GC").unwrap(); + let mut value = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_create_string_utf8(env, text.as_ptr(), NAPI_AUTO_LENGTH, &mut value) }, + NapiStatus::Ok + ); + crate::gc::js_gc_collect(); + let mut length = 0; + assert_eq!( + unsafe { napi_get_value_string_utf8(env, value, std::ptr::null_mut(), 0, &mut length) }, + NapiStatus::Ok + ); + assert_eq!(length, text.as_bytes().len()); +} diff --git a/crates/perry-runtime/src/node_api_host/values.rs b/crates/perry-runtime/src/node_api_host/values.rs new file mode 100644 index 0000000000..0c50e87b16 --- /dev/null +++ b/crates/perry-runtime/src/node_api_host/values.rs @@ -0,0 +1,1296 @@ +use super::*; +use crate::value::JSValue; +use std::ffi::{c_char, c_void}; + +#[repr(i32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NapiValueType { + Undefined = 0, + Null = 1, + Boolean = 2, + Number = 3, + String = 4, + Symbol = 5, + Object = 6, + Function = 7, + External = 8, + Bigint = 9, +} + +fn write_handle(env: NapiEnv, bits: u64, result: *mut NapiValue) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(handle) = add_handle(env, bits) else { + return NapiStatus::InvalidArg; + }; + unsafe { *result = handle }; + ok(env) +} + +fn value_as_number(bits: u64) -> Option { + let value = JSValue::from_bits(bits); + if value.is_int32() { + Some(value.as_int32() as f64) + } else if value.is_number() { + Some(value.as_number()) + } else { + None + } +} + +fn value_as_bool(bits: u64) -> Option { + let value = JSValue::from_bits(bits); + value.is_bool().then(|| value.as_bool()) +} + +fn to_int32(number: f64) -> i32 { + if !number.is_finite() || number == 0.0 { + return 0; + } + let modulo = number.trunc().rem_euclid(4_294_967_296.0); + if modulo >= 2_147_483_648.0 { + (modulo - 4_294_967_296.0) as i32 + } else { + modulo as i32 + } +} + +fn pointer_bits(ptr: *const u8) -> u64 { + JSValue::pointer(ptr).bits() +} + +fn string_bits(ptr: *mut crate::string::StringHeader) -> u64 { + JSValue::string_ptr(ptr).bits() +} + +fn bigint_bits(ptr: *mut crate::bigint::BigIntHeader) -> u64 { + JSValue::bigint_ptr(ptr).bits() +} + +fn create_string(env: NapiEnv, bytes: &[u8], wtf8: bool, result: *mut NapiValue) -> NapiStatus { + let ptr = if wtf8 { + crate::string::js_string_from_wtf8_bytes(bytes.as_ptr(), bytes.len() as u32) + } else { + crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) + }; + write_handle(env, string_bits(ptr), result) +} + +fn input_len(ptr: *const c_char, length: usize) -> Result { + if ptr.is_null() { + return Err(NapiStatus::InvalidArg); + } + Ok(if length == NAPI_AUTO_LENGTH { + unsafe { std::ffi::CStr::from_ptr(ptr).to_bytes().len() } + } else { + length + }) +} + +fn push_wtf8(code: u32, out: &mut Vec) { + if code <= 0x7f { + out.push(code as u8); + } else if code <= 0x7ff { + out.push((0xc0 | (code >> 6)) as u8); + out.push((0x80 | (code & 0x3f)) as u8); + } else if code <= 0xffff { + out.push((0xe0 | (code >> 12)) as u8); + out.push((0x80 | ((code >> 6) & 0x3f)) as u8); + out.push((0x80 | (code & 0x3f)) as u8); + } else { + out.push((0xf0 | (code >> 18)) as u8); + out.push((0x80 | ((code >> 12) & 0x3f)) as u8); + out.push((0x80 | ((code >> 6) & 0x3f)) as u8); + out.push((0x80 | (code & 0x3f)) as u8); + } +} + +fn utf16_to_wtf8(units: &[u16]) -> Vec { + let mut out = Vec::with_capacity(units.len()); + let mut i = 0; + while i < units.len() { + let first = units[i] as u32; + if (0xd800..=0xdbff).contains(&first) && i + 1 < units.len() { + let second = units[i + 1] as u32; + if (0xdc00..=0xdfff).contains(&second) { + push_wtf8( + 0x1_0000 + ((first - 0xd800) << 10) + (second - 0xdc00), + &mut out, + ); + i += 2; + continue; + } + } + push_wtf8(first, &mut out); + i += 1; + } + out +} + +fn string_bytes(bits: u64) -> Result, NapiStatus> { + let value = JSValue::from_bits(bits); + if !value.is_any_string() { + return Err(NapiStatus::StringExpected); + } + let mut scratch = [0; crate::value::SHORT_STRING_MAX_LEN]; + let Some((ptr, len)) = + crate::string::str_bytes_from_jsvalue(f64::from_bits(bits), &mut scratch) + else { + return Err(NapiStatus::StringExpected); + }; + if len == 0 { + return Ok(Vec::new()); + } + Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec()) +} + +fn wtf8_code_points(bytes: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < bytes.len() { + let (advance, _, code) = crate::string::wtf8_step(bytes, i); + out.push(code); + i = i.saturating_add(advance.max(1)); + } + out +} + +fn wtf8_to_utf8(bytes: &[u8]) -> Vec { + let mut out = Vec::with_capacity(bytes.len()); + for code in wtf8_code_points(bytes) { + let ch = char::from_u32(code).unwrap_or(char::REPLACEMENT_CHARACTER); + let mut encoded = [0; 4]; + out.extend_from_slice(ch.encode_utf8(&mut encoded).as_bytes()); + } + out +} + +fn wtf8_to_utf16(bytes: &[u8]) -> Vec { + let mut out = Vec::new(); + for code in wtf8_code_points(bytes) { + if code <= 0xffff { + out.push(code as u16); + } else { + let code = code - 0x1_0000; + out.push((0xd800 + (code >> 10)) as u16); + out.push((0xdc00 + (code & 0x3ff)) as u16); + } + } + out +} + +fn get_string_source(env: NapiEnv, value: NapiValue) -> Result, NapiStatus> { + string_bytes(value_bits(env, value)?) +} + +fn set_string_error(env: NapiEnv, status: NapiStatus) -> NapiStatus { + match status { + NapiStatus::StringExpected => set_status(env, status, "value must be a JavaScript string"), + _ => set_status(env, status, "invalid Node-API string argument"), + } +} + +fn named_key(env: NapiEnv, name: *const c_char) -> Result { + let len = input_len(name, NAPI_AUTO_LENGTH)?; + let bytes = unsafe { std::slice::from_raw_parts(name.cast::(), len) }; + let ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + add_handle(env, string_bits(ptr)) +} + +fn property_call( + env: NapiEnv, + object: NapiValue, + key: NapiValue, + f: impl FnOnce(f64, f64) -> f64, +) -> Result { + if pending_exception(env).is_some() { + return Err(NapiStatus::PendingException); + } + let object_bits = value_bits(env, object)?; + let key_bits = value_bits(env, key)?; + catch_value_call(env, || { + f(f64::from_bits(object_bits), f64::from_bits(key_bits)) + }) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_undefined(env: NapiEnv, result: *mut NapiValue) -> NapiStatus { + write_handle(env, crate::value::TAG_UNDEFINED, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_null(env: NapiEnv, result: *mut NapiValue) -> NapiStatus { + write_handle(env, crate::value::TAG_NULL, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_global(env: NapiEnv, result: *mut NapiValue) -> NapiStatus { + if pending_exception(env).is_some() { + return set_status(env, NapiStatus::PendingException, "an exception is pending"); + } + let global = crate::object::js_get_global_this(); + write_handle(env, global.to_bits(), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_boolean( + env: NapiEnv, + value: bool, + result: *mut NapiValue, +) -> NapiStatus { + write_handle(env, JSValue::bool(value).bits(), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_object(env: NapiEnv, result: *mut NapiValue) -> NapiStatus { + let object = crate::object::js_object_alloc(0, 0); + write_handle(env, pointer_bits(object.cast()), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_array(env: NapiEnv, result: *mut NapiValue) -> NapiStatus { + let array = crate::array::js_array_alloc(0); + write_handle(env, pointer_bits(array.cast()), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_array_with_length( + env: NapiEnv, + length: usize, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(length) = u32::try_from(length) else { + return set_status(env, NapiStatus::InvalidArg, "array length exceeds u32"); + }; + let array = crate::array::js_array_alloc_with_length(length); + write_handle(env, pointer_bits(array.cast()), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_double( + env: NapiEnv, + value: f64, + result: *mut NapiValue, +) -> NapiStatus { + write_handle(env, JSValue::number(value).bits(), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_int32( + env: NapiEnv, + value: i32, + result: *mut NapiValue, +) -> NapiStatus { + write_handle(env, JSValue::int32(value).bits(), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_uint32( + env: NapiEnv, + value: u32, + result: *mut NapiValue, +) -> NapiStatus { + let bits = if value <= i32::MAX as u32 { + JSValue::int32(value as i32).bits() + } else { + JSValue::number(value as f64).bits() + }; + write_handle(env, bits, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_int64( + env: NapiEnv, + value: i64, + result: *mut NapiValue, +) -> NapiStatus { + write_handle(env, JSValue::number(value as f64).bits(), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_double( + env: NapiEnv, + value: NapiValue, + result: *mut f64, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let Some(number) = value_as_number(bits) else { + return set_status(env, NapiStatus::NumberExpected, "value must be a number"); + }; + *result = number; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_int32( + env: NapiEnv, + value: NapiValue, + result: *mut i32, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let Some(number) = value_as_number(bits) else { + return set_status(env, NapiStatus::NumberExpected, "value must be a number"); + }; + *result = to_int32(number); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_uint32( + env: NapiEnv, + value: NapiValue, + result: *mut u32, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let Some(number) = value_as_number(bits) else { + return set_status(env, NapiStatus::NumberExpected, "value must be a number"); + }; + *result = to_int32(number) as u32; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_int64( + env: NapiEnv, + value: NapiValue, + result: *mut i64, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let Some(number) = value_as_number(bits) else { + return set_status(env, NapiStatus::NumberExpected, "value must be a number"); + }; + *result = number as i64; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_bool( + env: NapiEnv, + value: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let Some(boolean) = value_as_bool(bits) else { + return set_status(env, NapiStatus::BooleanExpected, "value must be a boolean"); + }; + *result = boolean; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_typeof( + env: NapiEnv, + value: NapiValue, + result: *mut NapiValueType, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + let js = JSValue::from_bits(bits); + let value_type = if js.is_undefined() { + NapiValueType::Undefined + } else if js.is_null() { + NapiValueType::Null + } else if js.is_bool() { + NapiValueType::Boolean + } else if js.is_number() || js.is_int32() { + NapiValueType::Number + } else if js.is_any_string() { + NapiValueType::String + } else if js.is_bigint() { + NapiValueType::Bigint + } else if crate::symbol::js_is_symbol(f64::from_bits(bits)) != 0 { + NapiValueType::Symbol + } else if js.is_pointer() && crate::closure::is_closure_ptr(js.as_pointer::() as usize) { + NapiValueType::Function + } else { + NapiValueType::Object + }; + *result = value_type; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_string_utf8( + env: NapiEnv, + value: *const c_char, + length: usize, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(length) = input_len(value, length) else { + return set_status(env, NapiStatus::InvalidArg, "string data must not be null"); + }; + let bytes = std::slice::from_raw_parts(value.cast::(), length); + create_string(env, bytes, false, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_string_latin1( + env: NapiEnv, + value: *const c_char, + length: usize, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(length) = input_len(value, length) else { + return set_status(env, NapiStatus::InvalidArg, "string data must not be null"); + }; + let input = std::slice::from_raw_parts(value.cast::(), length); + let mut utf8 = Vec::with_capacity(length); + for &byte in input { + push_wtf8(byte as u32, &mut utf8); + } + create_string(env, &utf8, false, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_string_utf16( + env: NapiEnv, + value: *const u16, + length: usize, + result: *mut NapiValue, +) -> NapiStatus { + if value.is_null() { + return set_status(env, NapiStatus::InvalidArg, "string data must not be null"); + } + let length = if length == NAPI_AUTO_LENGTH { + let mut len = 0; + while *value.add(len) != 0 { + len += 1; + } + len + } else { + length + }; + let wtf8 = utf16_to_wtf8(std::slice::from_raw_parts(value, length)); + create_string(env, &wtf8, true, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_string_utf8( + env: NapiEnv, + value: NapiValue, + buffer: *mut c_char, + buffer_size: usize, + result: *mut usize, +) -> NapiStatus { + let bytes = match get_string_source(env, value) { + Ok(bytes) => wtf8_to_utf8(&bytes), + Err(status) => return set_string_error(env, status), + }; + let copied = if buffer.is_null() || buffer_size == 0 { + 0 + } else { + let copied = bytes.len().min(buffer_size - 1); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer.cast::(), copied); + *buffer.add(copied) = 0; + copied + }; + if !result.is_null() { + *result = if buffer.is_null() { + bytes.len() + } else { + copied + }; + } + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_string_latin1( + env: NapiEnv, + value: NapiValue, + buffer: *mut c_char, + buffer_size: usize, + result: *mut usize, +) -> NapiStatus { + let bytes = match get_string_source(env, value) { + Ok(bytes) => wtf8_code_points(&bytes) + .into_iter() + .map(|code| if code <= 0xff { code as u8 } else { b'?' }) + .collect::>(), + Err(status) => return set_string_error(env, status), + }; + let copied = if buffer.is_null() || buffer_size == 0 { + 0 + } else { + let copied = bytes.len().min(buffer_size - 1); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer.cast::(), copied); + *buffer.add(copied) = 0; + copied + }; + if !result.is_null() { + *result = if buffer.is_null() { + bytes.len() + } else { + copied + }; + } + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_string_utf16( + env: NapiEnv, + value: NapiValue, + buffer: *mut u16, + buffer_size: usize, + result: *mut usize, +) -> NapiStatus { + let units = match get_string_source(env, value) { + Ok(bytes) => wtf8_to_utf16(&bytes), + Err(status) => return set_string_error(env, status), + }; + let copied = if buffer.is_null() || buffer_size == 0 { + 0 + } else { + let copied = units.len().min(buffer_size - 1); + std::ptr::copy_nonoverlapping(units.as_ptr(), buffer, copied); + *buffer.add(copied) = 0; + copied + }; + if !result.is_null() { + *result = if buffer.is_null() { + units.len() + } else { + copied + }; + } + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_coerce_to_bool( + env: NapiEnv, + value: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + write_handle( + env, + JSValue::bool(crate::value::js_is_truthy(f64::from_bits(bits)) != 0).bits(), + result, + ) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_coerce_to_number( + env: NapiEnv, + value: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + match catch_value_call(env, || { + crate::builtins::js_number_coerce(f64::from_bits(bits)) + }) { + Ok(value) => write_handle(env, value.to_bits(), result), + Err(status) => status, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_coerce_to_string( + env: NapiEnv, + value: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + match catch_value_call(env, || { + let ptr = crate::value::js_jsvalue_to_string(f64::from_bits(bits)); + f64::from_bits(string_bits(ptr)) + }) { + Ok(value) => write_handle(env, value.to_bits(), result), + Err(status) => status, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_coerce_to_object( + env: NapiEnv, + value: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + match catch_value_call(env, || { + crate::object::js_object_coerce(f64::from_bits(bits)) + }) { + Ok(value) => write_handle(env, value.to_bits(), result), + Err(status) => status, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_set_property( + env: NapiEnv, + object: NapiValue, + key: NapiValue, + value: NapiValue, +) -> NapiStatus { + let Ok(value_bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + match property_call(env, object, key, |object, key| { + crate::object::js_object_set_property_key(object, key, f64::from_bits(value_bits)) + }) { + Ok(_) => ok(env), + Err(status) => set_status(env, status, "property assignment failed"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_property( + env: NapiEnv, + object: NapiValue, + key: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + match property_call(env, object, key, |object, key| unsafe { + crate::object::js_object_get_property_key(object, key) + }) { + Ok(value) => write_handle(env, value.to_bits(), result), + Err(status) => set_status(env, status, "property lookup failed"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_has_property( + env: NapiEnv, + object: NapiValue, + key: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + match property_call(env, object, key, |object, key| { + crate::object::js_object_has_property(object, key) + }) { + Ok(value) => { + *result = JSValue::from_bits(value.to_bits()).to_bool(); + ok(env) + } + Err(status) => set_status(env, status, "property lookup failed"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_has_own_property( + env: NapiEnv, + object: NapiValue, + key: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + match property_call(env, object, key, |object, key| { + crate::object::js_object_has_own(object, key) + }) { + Ok(value) => { + *result = JSValue::from_bits(value.to_bits()).to_bool(); + ok(env) + } + Err(status) => set_status(env, status, "own-property lookup failed"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_delete_property( + env: NapiEnv, + object: NapiValue, + key: NapiValue, + result: *mut bool, +) -> NapiStatus { + match property_call(env, object, key, |object, key| { + f64::from(crate::object::js_object_delete_dynamic_value(object, key)) + }) { + Ok(value) => { + if !result.is_null() { + *result = value != 0.0; + } + ok(env) + } + Err(status) => set_status(env, status, "property deletion failed"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_set_named_property( + env: NapiEnv, + object: NapiValue, + name: *const c_char, + value: NapiValue, +) -> NapiStatus { + let key = match named_key(env, name) { + Ok(key) => key, + Err(status) => return set_status(env, status, "property name must not be null"), + }; + napi_set_property(env, object, key, value) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_named_property( + env: NapiEnv, + object: NapiValue, + name: *const c_char, + result: *mut NapiValue, +) -> NapiStatus { + let key = match named_key(env, name) { + Ok(key) => key, + Err(status) => return set_status(env, status, "property name must not be null"), + }; + napi_get_property(env, object, key, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_has_named_property( + env: NapiEnv, + object: NapiValue, + name: *const c_char, + result: *mut bool, +) -> NapiStatus { + let key = match named_key(env, name) { + Ok(key) => key, + Err(status) => return set_status(env, status, "property name must not be null"), + }; + napi_has_property(env, object, key, result) +} + +fn element_key(env: NapiEnv, index: u32) -> Result { + add_handle(env, JSValue::number(index as f64).bits()) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_set_element( + env: NapiEnv, + object: NapiValue, + index: u32, + value: NapiValue, +) -> NapiStatus { + let Ok(key) = element_key(env, index) else { + return NapiStatus::InvalidArg; + }; + napi_set_property(env, object, key, value) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_element( + env: NapiEnv, + object: NapiValue, + index: u32, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(key) = element_key(env, index) else { + return NapiStatus::InvalidArg; + }; + napi_get_property(env, object, key, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_has_element( + env: NapiEnv, + object: NapiValue, + index: u32, + result: *mut bool, +) -> NapiStatus { + let Ok(key) = element_key(env, index) else { + return NapiStatus::InvalidArg; + }; + napi_has_property(env, object, key, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_delete_element( + env: NapiEnv, + object: NapiValue, + index: u32, + result: *mut bool, +) -> NapiStatus { + let Ok(key) = element_key(env, index) else { + return NapiStatus::InvalidArg; + }; + napi_delete_property(env, object, key, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_is_array( + env: NapiEnv, + value: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + *result = JSValue::from_bits(crate::array::js_array_is_array(f64::from_bits(bits)).to_bits()) + .to_bool(); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_array_length( + env: NapiEnv, + value: NapiValue, + result: *mut u32, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + if !JSValue::from_bits(crate::array::js_array_is_array(f64::from_bits(bits)).to_bits()) + .to_bool() + { + return set_status(env, NapiStatus::ArrayExpected, "value must be an array"); + } + let mut length_value = std::ptr::null_mut(); + let status = napi_get_named_property(env, value, c"length".as_ptr(), &mut length_value); + if status != NapiStatus::Ok { + return status; + } + napi_get_value_uint32(env, length_value, result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_strict_equals( + env: NapiEnv, + lhs: NapiValue, + rhs: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let (Ok(lhs), Ok(rhs)) = (value_bits(env, lhs), value_bits(env, rhs)) else { + return set_status(env, NapiStatus::InvalidArg, "values must be live handles"); + }; + *result = crate::value::js_jsvalue_equals(f64::from_bits(lhs), f64::from_bits(rhs)) != 0; + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_prototype( + env: NapiEnv, + object: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(bits) = value_bits(env, object) else { + return set_status(env, NapiStatus::InvalidArg, "object is not a live handle"); + }; + match catch_value_call(env, || { + crate::object::js_object_get_prototype_of(f64::from_bits(bits)) + }) { + Ok(value) => write_handle(env, value.to_bits(), result), + Err(status) => status, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_property_names( + env: NapiEnv, + object: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let Ok(bits) = value_bits(env, object) else { + return set_status(env, NapiStatus::InvalidArg, "object is not a live handle"); + }; + match catch_value_call(env, || { + let array = crate::object::js_object_keys_value(f64::from_bits(bits)); + f64::from_bits(pointer_bits(array.cast())) + }) { + Ok(value) => write_handle(env, value.to_bits(), result), + Err(status) => status, + } +} + +fn create_error_kind( + env: NapiEnv, + code: NapiValue, + message: NapiValue, + result: *mut NapiValue, + kind: extern "C" fn(*mut crate::string::StringHeader) -> *mut crate::error::ErrorHeader, +) -> NapiStatus { + let Ok(message_bits) = value_bits(env, message) else { + return set_status(env, NapiStatus::InvalidArg, "message is not a live handle"); + }; + if !JSValue::from_bits(message_bits).is_any_string() { + return set_status(env, NapiStatus::StringExpected, "message must be a string"); + } + let message_ptr = crate::value::js_get_string_pointer_unified(f64::from_bits(message_bits)) + as *mut crate::string::StringHeader; + let scope = crate::gc::RuntimeHandleScope::new(); + let message_root = scope.root_string_ptr(message_ptr); + let error = kind( + message_root + .get_raw_const_ptr::() + .cast_mut(), + ); + let status = write_handle(env, pointer_bits(error.cast()), result); + if status != NapiStatus::Ok || code.is_null() { + return status; + } + let error_handle = unsafe { *result }; + unsafe { napi_set_named_property(env, error_handle, c"code".as_ptr(), code) } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_error( + env: NapiEnv, + code: NapiValue, + message: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + create_error_kind( + env, + code, + message, + result, + crate::error::js_error_new_with_message, + ) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_type_error( + env: NapiEnv, + code: NapiValue, + message: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + create_error_kind(env, code, message, result, crate::error::js_typeerror_new) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_range_error( + env: NapiEnv, + code: NapiValue, + message: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + create_error_kind(env, code, message, result, crate::error::js_rangeerror_new) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_is_error( + env: NapiEnv, + value: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + *result = JSValue::from_bits(crate::error::js_error_is_error(f64::from_bits(bits)).to_bits()) + .to_bool(); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_bigint_int64( + env: NapiEnv, + value: i64, + result: *mut NapiValue, +) -> NapiStatus { + write_handle( + env, + bigint_bits(crate::bigint::js_bigint_from_i64(value)), + result, + ) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_bigint_uint64( + env: NapiEnv, + value: u64, + result: *mut NapiValue, +) -> NapiStatus { + write_handle( + env, + bigint_bits(crate::bigint::js_bigint_from_u64(value)), + result, + ) +} + +fn bigint_value( + env: NapiEnv, + value: NapiValue, +) -> Result<*const crate::bigint::BigIntHeader, NapiStatus> { + let bits = value_bits(env, value)?; + let value = JSValue::from_bits(bits); + if !value.is_bigint() { + return Err(NapiStatus::BigintExpected); + } + Ok(value.as_bigint_ptr()) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_bigint_int64( + env: NapiEnv, + value: NapiValue, + result: *mut i64, + lossless: *mut bool, +) -> NapiStatus { + if result.is_null() || lossless.is_null() { + return set_status( + env, + NapiStatus::InvalidArg, + "result and lossless must not be null", + ); + } + let pointer = match bigint_value(env, value) { + Ok(pointer) => pointer, + Err(NapiStatus::BigintExpected) => { + return set_status(env, NapiStatus::BigintExpected, "value must be a BigInt"); + } + Err(status) => return set_status(env, status, "value is not a live handle"), + }; + let limbs = (*pointer).limbs; + let low = limbs[0] as i64; + let fill = if low < 0 { u64::MAX } else { 0 }; + *result = low; + *lossless = limbs[1..].iter().all(|limb| *limb == fill); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_value_bigint_uint64( + env: NapiEnv, + value: NapiValue, + result: *mut u64, + lossless: *mut bool, +) -> NapiStatus { + if result.is_null() || lossless.is_null() { + return set_status( + env, + NapiStatus::InvalidArg, + "result and lossless must not be null", + ); + } + let pointer = match bigint_value(env, value) { + Ok(pointer) => pointer, + Err(NapiStatus::BigintExpected) => { + return set_status(env, NapiStatus::BigintExpected, "value must be a BigInt"); + } + Err(status) => return set_status(env, status, "value is not a live handle"), + }; + let limbs = (*pointer).limbs; + *result = limbs[0]; + *lossless = limbs[1..].iter().all(|limb| *limb == 0); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_symbol( + env: NapiEnv, + description: NapiValue, + result: *mut NapiValue, +) -> NapiStatus { + let description = if description.is_null() { + std::ptr::null_mut() + } else { + let Ok(bits) = value_bits(env, description) else { + return set_status( + env, + NapiStatus::InvalidArg, + "description is not a live handle", + ); + }; + if !JSValue::from_bits(bits).is_any_string() { + return set_status( + env, + NapiStatus::StringExpected, + "description must be a string", + ); + } + crate::value::js_get_string_pointer_unified(f64::from_bits(bits)) + as *mut crate::string::StringHeader + }; + let symbol = crate::symbol::alloc_symbol(description, false); + write_handle(env, pointer_bits(symbol.cast()), result) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_date( + env: NapiEnv, + time: f64, + result: *mut NapiValue, +) -> NapiStatus { + write_handle( + env, + crate::date::js_date_new_from_timestamp(time).to_bits(), + result, + ) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_is_date( + env: NapiEnv, + value: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + *result = crate::date::is_date_value(f64::from_bits(bits)); + ok(env) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_get_date_value( + env: NapiEnv, + value: NapiValue, + result: *mut f64, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let Ok(bits) = value_bits(env, value) else { + return set_status(env, NapiStatus::InvalidArg, "value is not a live handle"); + }; + if !crate::date::is_date_value(f64::from_bits(bits)) { + return set_status(env, NapiStatus::DateExpected, "value must be a Date"); + } + *result = crate::date::date_cell_timestamp(f64::from_bits(bits)); + ok(env) +} + +fn throw_c_error( + env: NapiEnv, + code: *const c_char, + message: *const c_char, + create: unsafe extern "C" fn(NapiEnv, NapiValue, NapiValue, *mut NapiValue) -> NapiStatus, +) -> NapiStatus { + if message.is_null() { + return set_status(env, NapiStatus::InvalidArg, "message must not be null"); + } + let mut message_value = std::ptr::null_mut(); + let status = + unsafe { napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &mut message_value) }; + if status != NapiStatus::Ok { + return status; + } + let mut code_value = std::ptr::null_mut(); + if !code.is_null() { + let status = + unsafe { napi_create_string_utf8(env, code, NAPI_AUTO_LENGTH, &mut code_value) }; + if status != NapiStatus::Ok { + return status; + } + } + let mut error = std::ptr::null_mut(); + let status = unsafe { create(env, code_value, message_value, &mut error) }; + if status != NapiStatus::Ok { + return status; + } + unsafe { napi_throw(env, error) } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_throw_error( + env: NapiEnv, + code: *const c_char, + message: *const c_char, +) -> NapiStatus { + throw_c_error(env, code, message, napi_create_error) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_throw_type_error( + env: NapiEnv, + code: *const c_char, + message: *const c_char, +) -> NapiStatus { + throw_c_error(env, code, message, napi_create_type_error) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_throw_range_error( + env: NapiEnv, + code: *const c_char, + message: *const c_char, +) -> NapiStatus { + throw_c_error(env, code, message, napi_create_range_error) +} + +#[no_mangle] +pub unsafe extern "C" fn napi_instanceof( + env: NapiEnv, + object: NapiValue, + constructor: NapiValue, + result: *mut bool, +) -> NapiStatus { + if result.is_null() { + return set_status(env, NapiStatus::InvalidArg, "result must not be null"); + } + let (Ok(object), Ok(constructor)) = (value_bits(env, object), value_bits(env, constructor)) + else { + return set_status(env, NapiStatus::InvalidArg, "values must be live handles"); + }; + match catch_value_call(env, || { + crate::object::js_instanceof_dynamic(f64::from_bits(object), f64::from_bits(constructor)) + }) { + Ok(value) => { + *result = JSValue::from_bits(value.to_bits()).to_bool(); + ok(env) + } + Err(status) => status, + } +} + +#[no_mangle] +pub unsafe extern "C" fn napi_create_external( + env: NapiEnv, + _data: *mut c_void, + _finalize_cb: Option, + _finalize_hint: *mut c_void, + _result: *mut NapiValue, +) -> NapiStatus { + set_status( + env, + NapiStatus::GenericFailure, + "external values and finalizers are not enabled in this host core", + ) +} diff --git a/docs/src/internals/node-api-host.md b/docs/src/internals/node-api-host.md index a7063d8ad6..15f8ef36bb 100644 --- a/docs/src/internals/node-api-host.md +++ b/docs/src/internals/node-api-host.md @@ -2,8 +2,23 @@ Status: design contract for [#8523](https://github.com/PerryTS/perry/issues/8523). The implementation is deliberately staged behind the completed `bun:ffi` -callback work in #6562. This document fixes the representation, lifetime, ABI, -loader, and shipping decisions before the first `napi_*` symbol is exported. +callback work in #6562. This document fixed the representation, lifetime, ABI, +loader, and shipping decisions before implementation began. + +## Implementation status + +The optional `perry-runtime/node-api-host` feature now contains the Stage 1 +host core: environment and opaque handle validation, strict handle scopes, +strong references, mutable GC root scanning, pending exceptions, primitive and +string conversion, objects and arrays, BigInt/date/symbol values, and native +callback invocation. It is intentionally not enabled by the compiler yet, so +programs without native addons retain the zero-byte default path. + +The feature is an internal integration surface until the remaining Stage 1 +weak-reference/finalizer work and the Stage 2 loader/export table land. In +particular, a successful build with this feature does not by itself make +`process.dlopen()` accept `.node` files. Unsupported external values and weak +references fail safely instead of exposing untraced Perry heap addresses. The host lets a Perry executable load a prebuilt Node-API (`.node`) addon without embedding Node, V8, JavaScriptCore, or another JavaScript engine. It is From 8273c3a956d19791a44bd0a5c849ec85615fbe48 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Wed, 26 Aug 2026 09:37:46 +0200 Subject: [PATCH 2/4] docs: add Node-API host changelog fragment --- changelog.d/8850-node-api-host-core.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/8850-node-api-host-core.md diff --git a/changelog.d/8850-node-api-host-core.md b/changelog.d/8850-node-api-host-core.md new file mode 100644 index 0000000000..97b6908565 --- /dev/null +++ b/changelog.d/8850-node-api-host-core.md @@ -0,0 +1,3 @@ +Added the opt-in, GC-safe Node-API host core with opaque handle scopes, +value/property APIs, native callbacks, references, and pending exceptions as +the runtime foundation for prebuilt `.node` addon support. From debcc8581b0a3d8f40fbb0a3cde1229cb324e6c3 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Wed, 26 Aug 2026 09:58:05 +0200 Subject: [PATCH 3/4] runtime: harden Node-API host contracts --- .../src/node_api_host/functions.rs | 29 ++++++- crates/perry-runtime/src/node_api_host/mod.rs | 56 +++++++++----- .../perry-runtime/src/node_api_host/scopes.rs | 39 ++++------ .../perry-runtime/src/node_api_host/tests.rs | 77 ++++++++++++++++++- .../perry-runtime/src/node_api_host/values.rs | 24 +++++- 5 files changed, 175 insertions(+), 50 deletions(-) diff --git a/crates/perry-runtime/src/node_api_host/functions.rs b/crates/perry-runtime/src/node_api_host/functions.rs index e8bdf36dfa..28d8021095 100644 --- a/crates/perry-runtime/src/node_api_host/functions.rs +++ b/crates/perry-runtime/src/node_api_host/functions.rs @@ -128,6 +128,13 @@ pub unsafe extern "C" fn napi_create_function( } else { length }; + if length > i32::MAX as usize { + return set_status( + env, + NapiStatus::InvalidArg, + "function name length exceeds i32", + ); + } std::slice::from_raw_parts(utf8name.cast::(), length).to_vec() }; let callback = callback.unwrap() as usize; @@ -177,10 +184,14 @@ pub unsafe extern "C" fn napi_get_cb_info( this_arg: *mut NapiValue, data: *mut *mut c_void, ) -> NapiStatus { - if argc.is_null() { - return set_status(env, NapiStatus::InvalidArg, "argc must not be null"); + if argc.is_null() && !argv.is_null() { + return set_status( + env, + NapiStatus::InvalidArg, + "argc is required when argv is provided", + ); } - let capacity = *argc; + let capacity = if argc.is_null() { 0 } else { *argc }; let Some((args, this_value, callback_data)) = callback_info(env, info, |info| { (info.args.clone(), info.this_value, info.data) }) else { @@ -190,8 +201,18 @@ pub unsafe extern "C" fn napi_get_cb_info( for (index, argument) in args.iter().take(capacity).enumerate() { *argv.add(index) = *argument; } + if capacity > args.len() { + let Ok(undefined) = add_handle(env, crate::value::TAG_UNDEFINED) else { + return NapiStatus::InvalidArg; + }; + for index in args.len()..capacity { + *argv.add(index) = undefined; + } + } + } + if !argc.is_null() { + *argc = args.len(); } - *argc = args.len(); if !this_arg.is_null() { *this_arg = this_value; } diff --git a/crates/perry-runtime/src/node_api_host/mod.rs b/crates/perry-runtime/src/node_api_host/mod.rs index 022bb0cd5f..ca6ef3095a 100644 --- a/crates/perry-runtime/src/node_api_host/mod.rs +++ b/crates/perry-runtime/src/node_api_host/mod.rs @@ -118,10 +118,17 @@ pub(crate) struct Env { serial: u64, owner: std::thread::ThreadId, slots: Vec, + free_slots: Vec, + // Tokens are intentional tombstones: their addon-visible addresses are + // never reused, so an out-of-scope handle cannot alias a later value. tokens: Vec>, - scopes: Vec<*mut ScopeToken>, + token_lookup: crate::fast_hash::PtrHashMap, + scopes: Vec, + // Scope and reference records follow the same stable-address rule as + // value tokens. Their compact backing slots/roots are released instead. scope_tokens: Vec>, references: Vec>, + reference_lookup: crate::fast_hash::PtrHashMap, callbacks: Vec, active_callback_infos: Vec, pending_exception_bits: Option, @@ -136,10 +143,13 @@ impl Env { serial, owner: std::thread::current().id(), slots: Vec::new(), + free_slots: Vec::new(), tokens: Vec::new(), + token_lookup: crate::fast_hash::new_ptr_hash_map(), scopes: Vec::new(), scope_tokens: Vec::new(), references: Vec::new(), + reference_lookup: crate::fast_hash::new_ptr_hash_map(), callbacks: Vec::new(), active_callback_infos: Vec::new(), pending_exception_bits: None, @@ -173,20 +183,31 @@ impl Env { } fn add_handle_at_depth(&mut self, value_bits: u64, scope_depth: u32) -> NapiValue { - let slot = self.slots.len() as u32; - let generation = 1; - self.slots.push(HandleSlot { - value_bits, - generation, - scope_depth, - live: true, - }); + let (slot, generation) = if let Some(slot) = self.free_slots.pop() { + let record = &mut self.slots[slot as usize]; + debug_assert!(!record.live); + record.value_bits = value_bits; + record.scope_depth = scope_depth; + record.live = true; + (slot, record.generation) + } else { + let slot = self.slots.len() as u32; + let generation = 1; + self.slots.push(HandleSlot { + value_bits, + generation, + scope_depth, + live: true, + }); + (slot, generation) + }; let mut token = Box::new(HandleToken { env_serial: self.serial, slot, generation, }); let ptr = (&mut *token) as *mut HandleToken as NapiValue; + self.token_lookup.insert(ptr as usize, self.tokens.len()); self.tokens.push(token); ptr } @@ -199,10 +220,8 @@ impl Env { if value.is_null() { return None; } - self.tokens - .iter() - .find(|token| std::ptr::eq(token.as_ref(), value.cast::())) - .map(Box::as_ref) + let index = *self.token_lookup.get(&(value as usize))?; + self.tokens.get(index).map(Box::as_ref) } fn value_bits(&self, value: NapiValue) -> Option { @@ -215,11 +234,12 @@ impl Env { } fn invalidate_scope(&mut self, depth: u32) { - for slot in &mut self.slots { + for (index, slot) in self.slots.iter_mut().enumerate() { if slot.live && slot.scope_depth >= depth { slot.live = false; slot.generation = slot.generation.wrapping_add(1).max(1); slot.value_bits = crate::value::TAG_UNDEFINED; + self.free_slots.push(index as u32); } } } @@ -228,9 +248,9 @@ impl Env { if reference.is_null() { return None; } + let index = *self.reference_lookup.get(&(reference as usize))?; self.references - .iter() - .find(|record| std::ptr::eq(record.as_ref(), reference.cast::())) + .get(index) .map(Box::as_ref) .filter(|record| record.env_serial == self.serial && !record.deleted) } @@ -239,9 +259,9 @@ impl Env { if reference.is_null() { return None; } + let index = *self.reference_lookup.get(&(reference as usize))?; self.references - .iter_mut() - .find(|record| std::ptr::eq(record.as_ref(), reference.cast::())) + .get_mut(index) .map(Box::as_mut) .filter(|record| record.env_serial == self.serial && !record.deleted) } diff --git a/crates/perry-runtime/src/node_api_host/scopes.rs b/crates/perry-runtime/src/node_api_host/scopes.rs index 3d148b48eb..6dba81b169 100644 --- a/crates/perry-runtime/src/node_api_host/scopes.rs +++ b/crates/perry-runtime/src/node_api_host/scopes.rs @@ -15,7 +15,7 @@ fn open_scope(env: NapiEnv, escapable: bool, result: *mut *mut c_void) -> NapiSt closed: false, }); let ptr = (&mut *token) as *mut ScopeToken; - env.scopes.push(ptr); + env.scopes.push(env.scope_tokens.len()); env.scope_tokens.push(token); ptr.cast::() }); @@ -37,19 +37,15 @@ fn close_scope(env: NapiEnv, scope: *mut c_void, escapable: bool) -> NapiStatus "handle scopes must close in LIFO order", ); }; - if !std::ptr::eq(top, scope.cast::()) { + let Some(token) = env.scope_tokens.get_mut(top).map(Box::as_mut) else { + return env.set_status(NapiStatus::InvalidArg, "unknown handle scope"); + }; + if !std::ptr::eq(token, scope.cast::()) { return env.set_status( NapiStatus::HandleScopeMismatch, "handle scopes must close in LIFO order", ); } - let Some(token) = env - .scope_tokens - .iter_mut() - .find(|token| std::ptr::eq(token.as_ref(), top)) - else { - return env.set_status(NapiStatus::InvalidArg, "unknown handle scope"); - }; if token.closed || token.env_serial != env.serial || token.escapable != escapable { return env.set_status( NapiStatus::HandleScopeMismatch, @@ -118,16 +114,12 @@ pub unsafe extern "C" fn napi_escape_handle( let Some(&top) = env.scopes.last() else { return Err(NapiStatus::HandleScopeMismatch); }; - if !std::ptr::eq(top, scope.cast::()) { - return Err(NapiStatus::HandleScopeMismatch); - } - let Some(token) = env - .scope_tokens - .iter_mut() - .find(|token| std::ptr::eq(token.as_ref(), top)) - else { + let Some(token) = env.scope_tokens.get_mut(top).map(Box::as_mut) else { return Err(NapiStatus::InvalidArg); }; + if !std::ptr::eq(token, scope.cast::()) { + return Err(NapiStatus::HandleScopeMismatch); + } if !token.escapable || token.closed { return Err(NapiStatus::HandleScopeMismatch); } @@ -181,6 +173,8 @@ pub unsafe extern "C" fn napi_create_reference( deleted: false, }); let ptr = (&mut *record) as *mut ReferenceRecord as NapiRef; + env.reference_lookup + .insert(ptr as usize, env.references.len()); env.references.push(record); ptr }); @@ -298,13 +292,14 @@ pub unsafe extern "C" fn napi_get_and_clear_last_exception( if result.is_null() { return set_status(env, NapiStatus::InvalidArg, "result must not be null"); } - let bits = with_env_mut(env, |env| env.pending_exception_bits.take()); - let Some(bits) = bits else { + let pending = with_env_mut(env, |env| env.pending_exception_bits.take()); + let Some(pending) = pending else { return NapiStatus::InvalidArg; }; - let handle = add_handle(env, bits.unwrap_or(crate::value::TAG_UNDEFINED)) - .expect("validated environment disappeared"); - *result = handle; + *result = match pending { + Some(bits) => add_handle(env, bits).expect("validated environment disappeared"), + None => std::ptr::null_mut(), + }; ok(env) } diff --git a/crates/perry-runtime/src/node_api_host/tests.rs b/crates/perry-runtime/src/node_api_host/tests.rs index 491902e72b..c287007da1 100644 --- a/crates/perry-runtime/src/node_api_host/tests.rs +++ b/crates/perry-runtime/src/node_api_host/tests.rs @@ -1,5 +1,5 @@ use super::*; -use std::ffi::{c_void, CString}; +use std::ffi::{c_char, c_void, CString}; fn test_env() -> NapiEnv { crate::gc::ensure_gc_initialized(); @@ -100,6 +100,24 @@ fn handle_scopes_are_lifo_and_invalidate_local_handles() { unsafe { napi_close_handle_scope(env, outer) }, NapiStatus::Ok ); + + let slot_count = with_env(env, |env| env.slots.len()).unwrap(); + let mut recycled_scope = std::ptr::null_mut(); + assert_eq!( + unsafe { napi_open_handle_scope(env, &mut recycled_scope) }, + NapiStatus::Ok + ); + let recycled_value = int32(env, 3); + assert_eq!(with_env(env, |env| env.slots.len()).unwrap(), slot_count); + assert_eq!( + unsafe { napi_get_value_int32(env, inner_value, &mut ignored) }, + NapiStatus::InvalidArg + ); + assert_eq!(read_int32(env, recycled_value), 3); + assert_eq!( + unsafe { napi_close_handle_scope(env, recycled_scope) }, + NapiStatus::Ok + ); } #[test] @@ -144,7 +162,7 @@ fn utf8_latin1_and_utf16_strings_round_trip() { }, NapiStatus::Ok ); - let mut bytes = vec![0i8; byte_length + 1]; + let mut bytes = vec![0 as c_char; byte_length + 1]; let mut copied = 0; assert_eq!( unsafe { @@ -180,7 +198,7 @@ fn utf8_latin1_and_utf16_strings_round_trip() { }, NapiStatus::Ok ); - let mut latin_out = [0i8; 3]; + let mut latin_out = [0 as c_char; 3]; let mut latin_len = 0; assert_eq!( unsafe { @@ -198,6 +216,20 @@ fn utf8_latin1_and_utf16_strings_round_trip() { unsafe { std::slice::from_raw_parts(latin_out.as_ptr().cast::(), latin_len) }, latin1 ); + + let mut oversized = std::ptr::null_mut(); + assert_eq!( + unsafe { + napi_create_string_utf8(env, c"".as_ptr(), i32::MAX as usize + 1, &mut oversized) + }, + NapiStatus::InvalidArg + ); + assert_eq!( + unsafe { + napi_create_string_utf16(env, [0u16].as_ptr(), i32::MAX as usize + 1, &mut oversized) + }, + NapiStatus::InvalidArg + ); } #[test] @@ -281,6 +313,13 @@ fn pending_exceptions_and_strong_references_are_roots() { ); assert_eq!(read_int32(env, exception), 17); + let mut no_exception = 1usize as NapiValue; + assert_eq!( + unsafe { napi_get_and_clear_last_exception(env, &mut no_exception) }, + NapiStatus::Ok + ); + assert!(no_exception.is_null()); + let mut referenced = std::ptr::null_mut(); assert_eq!( unsafe { napi_get_reference_value(env, reference, &mut referenced) }, @@ -385,6 +424,38 @@ fn bigint_date_symbol_and_error_helpers_use_node_api_semantics() { } unsafe extern "C" fn add_callback(env: NapiEnv, info: NapiCallbackInfo) -> NapiValue { + assert_eq!( + napi_get_cb_info( + env, + info, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ), + NapiStatus::Ok + ); + + let mut padded_argc = 4; + let mut padded_argv = [std::ptr::null_mut(); 4]; + assert_eq!( + napi_get_cb_info( + env, + info, + &mut padded_argc, + padded_argv.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ), + NapiStatus::Ok + ); + assert_eq!(padded_argc, 2); + for value in &padded_argv[2..] { + let mut value_type = NapiValueType::Object; + assert_eq!(napi_typeof(env, *value, &mut value_type), NapiStatus::Ok); + assert_eq!(value_type, NapiValueType::Undefined); + } + let mut argc = 2; let mut argv = [std::ptr::null_mut(); 2]; let mut data = std::ptr::null_mut(); diff --git a/crates/perry-runtime/src/node_api_host/values.rs b/crates/perry-runtime/src/node_api_host/values.rs index 0c50e87b16..937a007526 100644 --- a/crates/perry-runtime/src/node_api_host/values.rs +++ b/crates/perry-runtime/src/node_api_host/values.rs @@ -81,11 +81,15 @@ fn input_len(ptr: *const c_char, length: usize) -> Result { if ptr.is_null() { return Err(NapiStatus::InvalidArg); } - Ok(if length == NAPI_AUTO_LENGTH { + let length = if length == NAPI_AUTO_LENGTH { unsafe { std::ffi::CStr::from_ptr(ptr).to_bytes().len() } } else { length - }) + }; + if length > i32::MAX as usize { + return Err(NapiStatus::InvalidArg); + } + Ok(length) } fn push_wtf8(code: u32, out: &mut Vec) { @@ -490,6 +494,9 @@ pub unsafe extern "C" fn napi_create_string_utf16( } else { length }; + if length > i32::MAX as usize { + return set_status(env, NapiStatus::InvalidArg, "string length exceeds i32"); + } let wtf8 = utf16_to_wtf8(std::slice::from_raw_parts(value, length)); create_string(env, &wtf8, true, result) } @@ -1145,7 +1152,18 @@ pub unsafe extern "C" fn napi_create_symbol( crate::value::js_get_string_pointer_unified(f64::from_bits(bits)) as *mut crate::string::StringHeader }; - let symbol = crate::symbol::alloc_symbol(description, false); + let symbol = if description.is_null() { + crate::symbol::alloc_symbol(std::ptr::null_mut(), false) + } else { + let scope = crate::gc::RuntimeHandleScope::new(); + let description_root = scope.root_string_ptr(description); + crate::symbol::alloc_symbol( + description_root + .get_raw_const_ptr::() + .cast_mut(), + false, + ) + }; write_handle(env, pointer_bits(symbol.cast()), result) } From e92300bf9392d9e2f14d36374b9904c0b2f049fc Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Wed, 26 Aug 2026 10:04:41 +0200 Subject: [PATCH 4/4] runtime: bound Node-API UTF-16 encoding --- crates/perry-runtime/src/node_api_host/tests.rs | 7 ++++++- crates/perry-runtime/src/node_api_host/values.rs | 11 +++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/node_api_host/tests.rs b/crates/perry-runtime/src/node_api_host/tests.rs index c287007da1..d530826a16 100644 --- a/crates/perry-runtime/src/node_api_host/tests.rs +++ b/crates/perry-runtime/src/node_api_host/tests.rs @@ -226,7 +226,12 @@ fn utf8_latin1_and_utf16_strings_round_trip() { ); assert_eq!( unsafe { - napi_create_string_utf16(env, [0u16].as_ptr(), i32::MAX as usize + 1, &mut oversized) + napi_create_string_utf16( + env, + [0u16].as_ptr(), + u32::MAX as usize / 3 + 1, + &mut oversized, + ) }, NapiStatus::InvalidArg ); diff --git a/crates/perry-runtime/src/node_api_host/values.rs b/crates/perry-runtime/src/node_api_host/values.rs index 937a007526..8215d1c622 100644 --- a/crates/perry-runtime/src/node_api_host/values.rs +++ b/crates/perry-runtime/src/node_api_host/values.rs @@ -494,8 +494,15 @@ pub unsafe extern "C" fn napi_create_string_utf16( } else { length }; - if length > i32::MAX as usize { - return set_status(env, NapiStatus::InvalidArg, "string length exceeds i32"); + // A lone UTF-16 surrogate expands to three WTF-8 bytes. Reject before + // constructing the input slice so the encoded byte length always fits the + // u32 length accepted by Perry's string allocator. + if length > u32::MAX as usize / 3 { + return set_status( + env, + NapiStatus::InvalidArg, + "encoded string length may exceed u32", + ); } let wtf8 = utf16_to_wtf8(std::slice::from_raw_parts(value, length)); create_string(env, &wtf8, true, result)