From 26a7c810fe4deff0d1465185a5cc6cd4299e8650 Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Thu, 4 Jun 2026 14:03:23 +0000 Subject: [PATCH] fix(node): route domain errors from native event emitters --- crates/perry-ext-events/src/lib.rs | 275 ++++++++++++++---- crates/perry-ffi/src/handle.rs | 13 + crates/perry-ffi/src/lib.rs | 4 +- crates/perry-hir/src/lower/expr_new.rs | 3 +- crates/perry-runtime/src/error.rs | 40 ++- crates/perry-runtime/src/gc/barrier.rs | 18 +- crates/perry-runtime/src/gc/copying.rs | 6 +- crates/perry-runtime/src/gc/layout.rs | 4 +- crates/perry-runtime/src/gc/roots.rs | 19 +- crates/perry-runtime/src/gc/tests/barrier.rs | 13 + .../src/gc/tests/layout_trace.rs | 21 ++ .../src/gc/tests/runtime_roots.rs | 28 ++ crates/perry-runtime/src/gc/trace.rs | 6 +- crates/perry-runtime/src/gc/types.rs | 1 + crates/perry-runtime/src/gc/verify.rs | 13 +- .../perry-runtime/src/object/class_handles.rs | 57 ++++ .../src/object/class_registry.rs | 11 +- crates/perry-stdlib/src/common/dispatch.rs | 21 +- crates/perry-stdlib/src/domain.rs | 73 ++++- crates/perry-stdlib/src/events.rs | 9 +- crates/perry-stdlib/src/events/domain.rs | 13 + 21 files changed, 535 insertions(+), 113 deletions(-) diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index f4ead6bd1c..d1ba0c952d 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -20,16 +20,19 @@ //! `events.getMaxListeners` / `events.setMaxListeners` helpers. use perry_ffi::{ - gc_register_mutable_root_scanner_named, get_handle_mut, iter_handles_of_mut, js_array_alloc, - js_array_get, js_array_push, js_array_set, nanbox_string_bits, read_string, register_handle, - ArrayHeader, GcRootVisitor, Handle, JsClosure, JsPromise, JsString, JsValue, ObjectHeader, - Promise, RawClosureHeader, StringHeader, + gc_register_mutable_root_scanner_named, get_handle, get_handle_mut, iter_handles_of_mut, + js_array_alloc, js_array_get, js_array_push, js_array_set, nanbox_string_bits, read_string, + register_handle_with_id, ArrayHeader, GcRootVisitor, Handle, JsPromise, JsString, JsValue, + ObjectHeader, Promise, RawClosureHeader, StringHeader, }; use std::collections::HashMap; +use std::sync::atomic::{AtomicI64, Ordering}; const MIN_HEAP_POINTER: u64 = 0x1000; -const EVENT_TARGET_MIN_HEAP_POINTER: u64 = 0x10000; +const EVENT_TARGET_MIN_HEAP_POINTER: u64 = 0x100000; const MAX_HEAP_POINTER: u64 = 0x0000_FFFF_FFFF_FFFF; +const EXT_EVENTS_HANDLE_ID_START: Handle = 0xE0000; +const EXT_EVENTS_HANDLE_ID_END: Handle = 0x100000; // Direct hook into perry-runtime's sync Promise resolve. // @@ -73,6 +76,18 @@ extern "C" { fn js_abort_signal_is_aborted(signal: *mut u8) -> i32; fn js_abort_error_value() -> f64; fn js_throw(value: f64) -> !; + fn js_get_string_pointer_unified(value: f64) -> i64; + fn js_jsvalue_to_string(value: f64) -> *mut StringHeader; + fn js_native_call_value(callback: f64, args_ptr: *const f64, args_len: usize) -> f64; + fn js_domain_emit_error_from_event_emitter( + handle: Handle, + error: f64, + emitter: f64, + domain_thrown: i32, + ) -> i32; + fn js_register_external_event_emitter_domain_probe(f: unsafe extern "C" fn(i64) -> bool); + fn js_register_external_event_emitter_domain_get(f: unsafe extern "C" fn(i64) -> i64); + fn js_register_external_event_emitter_domain_set(f: unsafe extern "C" fn(i64, i64) -> i32); // #3072: shared listener validator. Takes the raw NaN-box bits of the // listener arg (codegen routes these methods through NA_JSV) and the arg // name; returns the closure pointer when callable, else throws @@ -121,6 +136,7 @@ pub struct EventEmitterHandle { event_order: Vec, pending_once_promises: HashMap>, max_listeners: i32, + domain_handle: Option, } // SAFETY: `*mut Promise` is not Send/Sync by default, but the registry's @@ -144,6 +160,7 @@ impl EventEmitterHandle { // Node's default — `getMaxListeners()` on a fresh emitter // returns 10. max_listeners: 10, + domain_handle: None, } } @@ -179,6 +196,21 @@ impl EventEmitterHandle { } static EVENTS_GC_REGISTERED: std::sync::Once = std::sync::Once::new(); +static EVENTS_DOMAIN_HOOKS_REGISTERED: std::sync::Once = std::sync::Once::new(); +static NEXT_EVENT_EMITTER_HANDLE: AtomicI64 = AtomicI64::new(EXT_EVENTS_HANDLE_ID_START); + +fn next_event_emitter_handle_id() -> Handle { + let handle = NEXT_EVENT_EMITTER_HANDLE.fetch_add(1, Ordering::SeqCst); + if handle >= EXT_EVENTS_HANDLE_ID_END { + panic!("perry-ext-events handle id range exhausted"); + } + handle +} + +fn register_event_emitter_handle() -> Handle { + let handle = next_event_emitter_handle_id(); + register_handle_with_id(EventEmitterHandle::new(), handle) +} fn ensure_gc_scanner_registered() { EVENTS_GC_REGISTERED.call_once(|| { @@ -186,6 +218,33 @@ fn ensure_gc_scanner_registered() { }); } +fn ensure_domain_hooks_registered() { + EVENTS_DOMAIN_HOOKS_REGISTERED.call_once(|| unsafe { + js_register_external_event_emitter_domain_probe(ext_event_emitter_domain_probe); + js_register_external_event_emitter_domain_get(ext_event_emitter_domain_get); + js_register_external_event_emitter_domain_set(ext_event_emitter_domain_set); + }); +} + +unsafe extern "C" fn ext_event_emitter_domain_probe(handle: i64) -> bool { + get_handle::(handle).is_some() +} + +unsafe extern "C" fn ext_event_emitter_domain_get(handle: i64) -> i64 { + get_handle::(handle) + .and_then(|emitter| emitter.domain_handle) + .unwrap_or(0) +} + +unsafe extern "C" fn ext_event_emitter_domain_set(handle: i64, domain: i64) -> i32 { + if let Some(emitter) = get_handle_mut::(handle) { + emitter.domain_handle = if domain == 0 { None } else { Some(domain) }; + 1 + } else { + 0 + } +} + /// GC root scanner: visit every registered EventEmitterHandle, /// and expose every listener closure pointer + pending Promise slot. fn scan_events_roots(visitor: &mut GcRootVisitor<'_>) { @@ -258,6 +317,25 @@ unsafe fn read_str(ptr: *const StringHeader) -> Option { read_string(handle).map(String::from) } +fn value_from_bits(bits: i64) -> f64 { + f64::from_bits(bits as u64) +} + +unsafe fn event_name_from_bits(event_bits: i64) -> Option { + let value = value_from_bits(event_bits); + let jsval = JsValue::from_bits(value.to_bits()); + let ptr = if jsval.is_string() { + js_get_string_pointer_unified(value) as *mut StringHeader + } else { + js_jsvalue_to_string(value) + }; + read_str(ptr) +} + +fn event_bits_from_string_ptr(ptr: *const StringHeader) -> i64 { + nanbox_string_bits(ptr as *mut StringHeader) as i64 +} + fn undefined_value() -> f64 { f64::from_bits(TAG_UNDEFINED_F64_BITS) } @@ -371,7 +449,8 @@ fn remove_listener_by_callback(emitter: &mut EventEmitterHandle, callback: i64) #[no_mangle] pub extern "C" fn js_event_emitter_new() -> Handle { ensure_gc_scanner_registered(); - register_handle(EventEmitterHandle::new()) + ensure_domain_hooks_registered(); + register_event_emitter_handle() } /// `new EventEmitter(options?)` — the constructor shape codegen actually @@ -391,7 +470,8 @@ pub extern "C" fn js_event_emitter_new() -> Handle { #[no_mangle] pub unsafe extern "C" fn js_event_emitter_new_with_options(_options: f64) -> Handle { ensure_gc_scanner_registered(); - register_handle(EventEmitterHandle::new()) + ensure_domain_hooks_registered(); + register_event_emitter_handle() } /// `emitter.on(eventName, listener)` — register a listener. @@ -405,13 +485,14 @@ pub unsafe extern "C" fn js_event_emitter_new_with_options(_options: f64) -> Han #[no_mangle] pub unsafe extern "C" fn js_event_emitter_on( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, listener_bits: i64, ) -> Handle { ensure_gc_scanner_registered(); + ensure_domain_hooks_registered(); // #3072: reject non-function listeners with TypeError [ERR_INVALID_ARG_TYPE]. let callback_ptr = validate_event_listener(listener_bits); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return handle; }; if let Some(emitter) = get_handle_mut::(handle) { @@ -428,12 +509,13 @@ pub unsafe extern "C" fn js_event_emitter_on( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_once( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, listener_bits: i64, ) -> Handle { ensure_gc_scanner_registered(); + ensure_domain_hooks_registered(); let callback_ptr = validate_event_listener(listener_bits); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return handle; }; if let Some(emitter) = get_handle_mut::(handle) { @@ -450,12 +532,13 @@ pub unsafe extern "C" fn js_event_emitter_once( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_prepend_listener( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, listener_bits: i64, ) -> Handle { ensure_gc_scanner_registered(); + ensure_domain_hooks_registered(); let callback_ptr = validate_event_listener(listener_bits); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return handle; }; if let Some(emitter) = get_handle_mut::(handle) { @@ -472,12 +555,13 @@ pub unsafe extern "C" fn js_event_emitter_prepend_listener( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_prepend_once_listener( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, listener_bits: i64, ) -> Handle { ensure_gc_scanner_registered(); + ensure_domain_hooks_registered(); let callback_ptr = validate_event_listener(listener_bits); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return handle; }; if let Some(emitter) = get_handle_mut::(handle) { @@ -497,6 +581,22 @@ unsafe fn first_arg_or_undefined(args_ptr: *const ArrayHeader) -> f64 { f64::from_bits(js_array_get(args_ptr, 0).bits()) } +unsafe fn collect_emit_args(args_ptr: *const ArrayHeader) -> Vec { + if args_ptr.is_null() { + return Vec::new(); + } + let len = (*args_ptr).length; + let mut args = Vec::with_capacity(len as usize); + for index in 0..len { + args.push(f64::from_bits(js_array_get(args_ptr, index).bits())); + } + args +} + +unsafe fn call_emitter_listener(callback: i64, args: &[f64]) -> f64 { + js_native_call_value(nanbox_pointer_bits(callback), args.as_ptr(), args.len()) +} + /// Drain pending `events.once` promises for `event_name` on the given /// emitter, resolving each with the full args array passed to `emit`. /// @@ -581,12 +681,12 @@ unsafe fn reject_pending_once_promises_for_error( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_emit( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, args_ptr: *mut ArrayHeader, ) -> f64 { const TAG_FALSE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0003); const TAG_TRUE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0004); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return TAG_FALSE_F64; }; let mut had_listeners = false; @@ -606,6 +706,7 @@ pub unsafe extern "C" fn js_event_emitter_emit( } let first_arg = first_arg_or_undefined(args_ptr); + let emitted_args = collect_emit_args(args_ptr); if event_name == "error" { let has_error_once = emitter .pending_once_promises @@ -614,6 +715,15 @@ pub unsafe extern "C" fn js_event_emitter_emit( let rejected_once = reject_pending_once_promises_for_error(emitter, first_arg); had_listeners = had_listeners || has_error_once || rejected_once; if snapshot.is_empty() && !has_error_once && !rejected_once { + if let Some(domain) = emitter.domain_handle { + let _ = js_domain_emit_error_from_event_emitter( + domain, + first_arg, + nanbox_pointer_bits(handle), + 0, + ); + return TAG_FALSE_F64; + } js_throw(first_arg); } } @@ -622,8 +732,7 @@ pub unsafe extern "C" fn js_event_emitter_emit( for l in snapshot { if l.callback != 0 { - let closure = JsClosure::from_raw(l.callback as *const RawClosureHeader); - let _ = closure.call1(first_arg); + let _ = call_emitter_listener(l.callback, &emitted_args); } } } @@ -644,13 +753,10 @@ pub unsafe extern "C" fn js_event_emitter_emit( /// /// `event_name_ptr` must be null or a Perry-runtime `StringHeader`. #[no_mangle] -pub unsafe extern "C" fn js_event_emitter_emit0( - handle: Handle, - event_name_ptr: *const StringHeader, -) -> f64 { +pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) -> f64 { const TAG_FALSE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0003); const TAG_TRUE_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0004); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return TAG_FALSE_F64; }; let mut had_listeners = false; @@ -679,6 +785,15 @@ pub unsafe extern "C" fn js_event_emitter_emit0( let rejected_once = reject_pending_once_promises_for_error(emitter, error_value); had_listeners = had_listeners || has_error_once || rejected_once; if snapshot.is_empty() && !has_error_once && !rejected_once { + if let Some(domain) = emitter.domain_handle { + let _ = js_domain_emit_error_from_event_emitter( + domain, + error_value, + nanbox_pointer_bits(handle), + 0, + ); + return TAG_FALSE_F64; + } js_throw(error_value); } } @@ -686,8 +801,7 @@ pub unsafe extern "C" fn js_event_emitter_emit0( for l in snapshot { if l.callback != 0 { - let closure = JsClosure::from_raw(l.callback as *const RawClosureHeader); - let _ = closure.call0(); + let _ = call_emitter_listener(l.callback, &[]); } } } @@ -707,12 +821,12 @@ pub unsafe extern "C" fn js_event_emitter_emit0( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_remove_listener( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, listener_bits: i64, ) -> Handle { // #3072: `removeListener`/`off` require a callable listener too. let callback_ptr = validate_event_listener(listener_bits); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return handle; }; if let Some(emitter) = get_handle_mut::(handle) { @@ -738,13 +852,15 @@ pub unsafe extern "C" fn js_event_emitter_remove_listener( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_remove_all_listeners( handle: Handle, - event_name_ptr: *const StringHeader, + args_ptr: *const ArrayHeader, ) -> Handle { if let Some(emitter) = get_handle_mut::(handle) { - if event_name_ptr.is_null() { + if args_ptr.is_null() || (*args_ptr).length == 0 { emitter.events.clear(); emitter.event_order.clear(); - } else if let Some(event_name) = read_str(event_name_ptr) { + } else if let Some(event_name) = + event_name_from_bits(js_array_get(args_ptr, 0).bits() as i64) + { emitter.events.remove(&event_name); if let Some(pos) = emitter.event_order.iter().position(|s| s == &event_name) { emitter.event_order.remove(pos); @@ -762,13 +878,32 @@ pub unsafe extern "C" fn js_event_emitter_remove_all_listeners( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_listener_count( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, + listener_bits: i64, ) -> f64 { - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return 0.0; }; + let listener_value = JsValue::from_bits(listener_bits as u64); + let listener_filter = if listener_value.is_undefined() || listener_value.is_null() { + None + } else if listener_value.is_pointer() { + let ptr = listener_value.as_pointer::() as i64; + (ptr != 0).then_some(ptr) + } else { + Some(0) + }; if let Some(emitter) = get_handle_mut::(handle) { if let Some(listeners) = emitter.events.get(&event_name) { + if let Some(callback_ptr) = listener_filter { + if callback_ptr == 0 { + return 0.0; + } + return listeners + .iter() + .filter(|listener| listener.callback == callback_ptr) + .count() as f64; + } return listeners.len() as f64; } } @@ -827,10 +962,10 @@ pub unsafe extern "C" fn js_event_emitter_event_names(handle: Handle) -> *mut Ar #[no_mangle] pub unsafe extern "C" fn js_event_emitter_listeners( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, ) -> *mut ArrayHeader { let arr = js_array_alloc(0); - let Some(event_name) = read_str(event_name_ptr) else { + let Some(event_name) = event_name_from_bits(event_bits) else { return arr; }; if let Some(emitter) = get_handle_mut::(handle) { @@ -857,9 +992,9 @@ pub unsafe extern "C" fn js_event_emitter_listeners( #[no_mangle] pub unsafe extern "C" fn js_event_emitter_raw_listeners( handle: Handle, - event_name_ptr: *const StringHeader, + event_bits: i64, ) -> *mut ArrayHeader { - js_event_emitter_listeners(handle, event_name_ptr) + js_event_emitter_listeners(handle, event_bits) } // ============================================================================ @@ -892,11 +1027,13 @@ extern "C" fn events_once_abort_listener(closure: *const RawClosureHeader) -> f6 /// `event_name_ptr` must be null or a Perry-runtime `StringHeader`. #[no_mangle] pub unsafe extern "C" fn js_events_once( - handle: Handle, + target_value: f64, event_name_ptr: *const StringHeader, options: f64, ) -> *mut Promise { ensure_gc_scanner_registered(); + ensure_domain_hooks_registered(); + let handle = handle_from_js_value_bits(target_value.to_bits()); let prom = JsPromise::new(); let raw = prom.as_raw(); let Some(event_name) = read_str(event_name_ptr) else { @@ -1001,11 +1138,13 @@ extern "C" fn events_on_abort_listener(closure: *const RawClosureHeader) -> f64 /// `event_name_ptr` must be null or a Perry-runtime `StringHeader`. #[no_mangle] pub unsafe extern "C" fn js_events_on( - handle: Handle, + target_value: f64, event_name_ptr: *const StringHeader, options: f64, ) -> *mut ArrayHeader { ensure_gc_scanner_registered(); + ensure_domain_hooks_registered(); + let handle = handle_from_js_value_bits(target_value.to_bits()); let queue = js_array_alloc(0); let Some(event_name) = read_str(event_name_ptr) else { return queue; @@ -1084,11 +1223,12 @@ pub unsafe extern "C" fn js_events_add_abort_listener(signal_ptr: i64, callback_ /// `event_name_ptr` must be null or a Perry-runtime `StringHeader`. #[no_mangle] pub unsafe extern "C" fn js_events_get_event_listeners( - handle: Handle, + target_value: f64, event_name_ptr: *const StringHeader, ) -> *mut ArrayHeader { + let handle = handle_from_js_value_bits(target_value.to_bits()); if get_handle_mut::(handle).is_some() { - return js_event_emitter_listeners(handle, event_name_ptr); + return js_event_emitter_listeners(handle, event_bits_from_string_ptr(event_name_ptr)); } if let Some(target) = event_target_ptr(handle) { return js_event_target_get_event_listeners(target, event_name_ptr); @@ -1106,15 +1246,21 @@ pub unsafe extern "C" fn js_events_get_event_listeners( /// `event_name_ptr` must be null or a Perry-runtime `StringHeader`. #[no_mangle] pub unsafe extern "C" fn js_events_listener_count( - handle: Handle, + target_value: f64, event_name_ptr: *const StringHeader, ) -> f64 { - js_event_emitter_listener_count(handle, event_name_ptr) + let handle = handle_from_js_value_bits(target_value.to_bits()); + js_event_emitter_listener_count( + handle, + event_bits_from_string_ptr(event_name_ptr), + TAG_UNDEFINED_F64_BITS as i64, + ) } /// `events.getMaxListeners(emitter)` — alias. #[no_mangle] -pub unsafe extern "C" fn js_events_get_max_listeners(handle: Handle) -> f64 { +pub unsafe extern "C" fn js_events_get_max_listeners(target_value: f64) -> f64 { + let handle = handle_from_js_value_bits(target_value.to_bits()); if let Some(emitter) = get_handle_mut::(handle) { return emitter.max_listeners as f64; } @@ -1207,6 +1353,14 @@ mod tests { nanbox_pointer_bits(closure as i64).to_bits() as i64 } + fn event_bits(event_name: perry_ffi::JsString) -> i64 { + nanbox_string_bits(event_name.as_raw()) as i64 + } + + fn undefined_bits() -> i64 { + TAG_UNDEFINED_F64_BITS as i64 + } + fn assert_rewritten(before: usize, after: usize) { assert_ne!(after, before); assert!(perry_runtime::arena::pointer_in_nursery(after)); @@ -1216,7 +1370,8 @@ mod tests { fn new_emitter_starts_empty() { let h = js_event_emitter_new(); let event_name = alloc_string("foo"); - let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; + let count = + unsafe { js_event_emitter_listener_count(h, event_bits(event_name), undefined_bits()) }; assert_eq!(count, 0.0); drop_handle(h); } @@ -1226,9 +1381,10 @@ mod tests { let h = js_event_emitter_new(); let event_name = alloc_string("change"); // Real closures — we never emit so the bodies aren't invoked. - let _ = unsafe { js_event_emitter_on(h, event_name.as_raw() as *const _, fake_listener()) }; - let _ = unsafe { js_event_emitter_on(h, event_name.as_raw() as *const _, fake_listener()) }; - let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; + let _ = unsafe { js_event_emitter_on(h, event_bits(event_name), fake_listener()) }; + let _ = unsafe { js_event_emitter_on(h, event_bits(event_name), fake_listener()) }; + let count = + unsafe { js_event_emitter_listener_count(h, event_bits(event_name), undefined_bits()) }; assert_eq!(count, 2.0); drop_handle(h); } @@ -1240,11 +1396,12 @@ mod tests { let a = fake_listener(); let b = fake_listener(); unsafe { - js_event_emitter_on(h, event_name.as_raw() as *const _, a); - js_event_emitter_on(h, event_name.as_raw() as *const _, b); - js_event_emitter_remove_listener(h, event_name.as_raw() as *const _, a); + js_event_emitter_on(h, event_bits(event_name), a); + js_event_emitter_on(h, event_bits(event_name), b); + js_event_emitter_remove_listener(h, event_bits(event_name), a); } - let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; + let count = + unsafe { js_event_emitter_listener_count(h, event_bits(event_name), undefined_bits()) }; assert_eq!(count, 1.0); drop_handle(h); } @@ -1254,11 +1411,12 @@ mod tests { let h = js_event_emitter_new(); let event_name = alloc_string("x"); unsafe { - js_event_emitter_on(h, event_name.as_raw() as *const _, fake_listener()); - js_event_emitter_on(h, event_name.as_raw() as *const _, fake_listener()); + js_event_emitter_on(h, event_bits(event_name), fake_listener()); + js_event_emitter_on(h, event_bits(event_name), fake_listener()); js_event_emitter_remove_all_listeners(h, std::ptr::null()); } - let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; + let count = + unsafe { js_event_emitter_listener_count(h, event_bits(event_name), undefined_bits()) }; assert_eq!(count, 0.0); drop_handle(h); } @@ -1268,10 +1426,10 @@ mod tests { let h = js_event_emitter_new(); let event_name = alloc_string("ord"); unsafe { - js_event_emitter_on(h, event_name.as_raw() as *const _, fake_listener()); - js_event_emitter_prepend_listener(h, event_name.as_raw() as *const _, fake_listener()); + js_event_emitter_on(h, event_bits(event_name), fake_listener()); + js_event_emitter_prepend_listener(h, event_bits(event_name), fake_listener()); } - let arr = unsafe { js_event_emitter_listeners(h, event_name.as_raw() as *const _) }; + let arr = unsafe { js_event_emitter_listeners(h, event_bits(event_name)) }; assert!(!arr.is_null()); drop_handle(h); } @@ -1317,6 +1475,7 @@ mod tests { event_order: vec!["ready".to_string()], pending_once_promises, max_listeners: 10, + domain_handle: None, }); let _ = perry_runtime::gc::gc_collect_minor(); diff --git a/crates/perry-ffi/src/handle.rs b/crates/perry-ffi/src/handle.rs index 5edb091bc4..9e301c72fe 100644 --- a/crates/perry-ffi/src/handle.rs +++ b/crates/perry-ffi/src/handle.rs @@ -198,6 +198,19 @@ pub fn register_handle(value: T) -> Handle { handle } +/// Register `value` under a caller-selected handle id. +/// +/// This is for in-tree wrappers that must reserve a disjoint handle band for +/// generic runtime dispatch. External wrappers should prefer +/// [`register_handle`]. +pub fn register_handle_with_id(value: T, handle: Handle) -> Handle { + if handle <= INVALID_HANDLE { + panic!("perry-ffi handle id must be positive"); + } + HANDLES.insert(handle, Box::new(value)); + handle +} + fn next_handle_id() -> Handle { let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst); if handle >= FFI_HANDLE_ID_END { diff --git a/crates/perry-ffi/src/lib.rs b/crates/perry-ffi/src/lib.rs index 2cc3bcbd69..6ee5195160 100644 --- a/crates/perry-ffi/src/lib.rs +++ b/crates/perry-ffi/src/lib.rs @@ -65,8 +65,8 @@ pub use handle::gc_register_root_scanner; pub use handle::{ drop_handle, gc_register_mutable_root_scanner, gc_register_mutable_root_scanner_named, get_handle, get_handle_mut, handle_exists, iter_handle_ids_of, iter_handles_of, - iter_handles_of_mut, register_handle, take_handle, with_handle, with_handle_mut, - GcMutableRootScanner, GcRootVisitor, Handle, INVALID_HANDLE, + iter_handles_of_mut, register_handle, register_handle_with_id, take_handle, with_handle, + with_handle_mut, GcMutableRootScanner, GcRootVisitor, Handle, INVALID_HANDLE, }; mod jsvalue; diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index b6333e686e..75df28ce10 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -159,6 +159,7 @@ fn is_url_encoding_constructor_name(name: &str) -> bool { fn module_constructor_name(module_name: &str, method_name: Option<&str>) -> Option<&'static str> { match (module_name, method_name) { + ("events", Some("EventEmitter")) => Some("EventEmitter"), ("events", Some("EventEmitterAsyncResource")) => Some("EventEmitterAsyncResource"), ("url", Some("URL")) => Some("URL"), ("url", Some("URLSearchParams")) => Some("URLSearchParams"), @@ -623,7 +624,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R let class_name = prop_ident.sym.as_ref(); if matches!( (module_name, class_name), - ("events", "EventEmitterAsyncResource") + ("events", "EventEmitter" | "EventEmitterAsyncResource") | ("async_hooks", "AsyncLocalStorage" | "AsyncResource") | ("sqlite", "DatabaseSync" | "Session" | "StatementSync") ) { diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index b189896aa8..e7424f0767 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -2,7 +2,7 @@ //! //! Provides the built-in Error class and its subclasses. -use crate::string::{js_string_from_bytes, StringHeader}; +use crate::string::{js_string_from_bytes, js_string_from_bytes_with_capacity, StringHeader}; /// Object type tag for runtime type discrimination pub const OBJECT_TYPE_REGULAR: u32 = 1; @@ -84,12 +84,38 @@ pub struct ErrorHeader { unsafe fn make_stack(name: &str, message: &str) -> *mut StringHeader { // Build a simple ": \n at " string. // Real stack traces are not implemented; the test only checks `.includes(message)`. - let s = if message.is_empty() { - format!("{}\n at ", name) - } else { - format!("{}: {}\n at ", name, message) - }; - js_string_from_bytes(s.as_ptr(), s.len() as u32) + const SEP: &[u8] = b": "; + const SUFFIX: &[u8] = b"\n at "; + + let name_bytes = name.as_bytes(); + let message_bytes = message.as_bytes(); + let sep_len = if message.is_empty() { 0 } else { SEP.len() }; + let total_len = name_bytes.len() + sep_len + message_bytes.len() + SUFFIX.len(); + if total_len > u32::MAX as usize { + return js_string_from_bytes(std::ptr::null(), 0); + } + + let ptr = js_string_from_bytes_with_capacity(std::ptr::null(), 0, total_len as u32); + let mut out = (ptr as *mut u8).add(std::mem::size_of::()); + std::ptr::copy_nonoverlapping(name_bytes.as_ptr(), out, name_bytes.len()); + out = out.add(name_bytes.len()); + if !message.is_empty() { + std::ptr::copy_nonoverlapping(SEP.as_ptr(), out, SEP.len()); + out = out.add(SEP.len()); + std::ptr::copy_nonoverlapping(message_bytes.as_ptr(), out, message_bytes.len()); + out = out.add(message_bytes.len()); + } + std::ptr::copy_nonoverlapping(SUFFIX.as_ptr(), out, SUFFIX.len()); + + (*ptr).byte_len = total_len as u32; + (*ptr).utf16_len = (name.encode_utf16().count() + + if message.is_empty() { + 0 + } else { + SEP.len() + message.encode_utf16().count() + } + + SUFFIX.len()) as u32; + ptr } unsafe fn alloc_error( diff --git a/crates/perry-runtime/src/gc/barrier.rs b/crates/perry-runtime/src/gc/barrier.rs index 9f5ba7b7a6..91386cea37 100644 --- a/crates/perry-runtime/src/gc/barrier.rs +++ b/crates/perry-runtime/src/gc/barrier.rs @@ -761,12 +761,12 @@ pub(super) fn heap_word_candidate_addr(bits: u64) -> Option { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { let ptr = (bits & POINTER_MASK) as usize; - return (ptr != 0).then_some(ptr); + return (ptr >= MIN_HEAP_POINTER as usize).then_some(ptr); } if tag >= 0x7FF8_0000_0000_0000 { return None; } - if (0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&bits) { + if (MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&bits) { Some(bits as usize) } else { None @@ -801,7 +801,7 @@ pub(super) fn current_heap_header_for_user_ptr( user_ptr: usize, valid_ptrs: Option<&ValidPointerSet>, ) -> Option<*mut GcHeader> { - if user_ptr < GC_HEADER_SIZE + 0x1000 { + if user_ptr < MIN_HEAP_POINTER as usize { return None; } if valid_ptrs.is_some_and(|ptrs| ptrs.contains(&user_ptr)) { @@ -1039,7 +1039,7 @@ pub(super) fn barrier_parent_needs_remembering(parent_addr: usize, external_slot #[inline] pub(super) fn malloc_gc_parent_addr(parent_addr: usize) -> bool { - if parent_addr < GC_HEADER_SIZE + 0x1000 { + if parent_addr < MIN_HEAP_POINTER as usize { return false; } unsafe { @@ -1063,12 +1063,20 @@ pub(super) fn malloc_gc_parent_addr(parent_addr: usize) -> bool { pub(super) fn decode_heap_addr(bits: u64) -> usize { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { - (bits & POINTER_MASK) as usize + let addr = (bits & POINTER_MASK) as usize; + if addr >= MIN_HEAP_POINTER as usize { + addr + } else { + 0 + } } else if tag < 0x7FF8_0000_0000_0000 { // Possible raw pointer. Accept only if the arena side metadata // recognizes it as a heap address; ordinary f64 payload bits // miss the metadata table and remain non-pointers. let addr = bits as usize; + if addr < MIN_HEAP_POINTER as usize { + return 0; + } if matches!( crate::arena::classify_heap_generation(addr), crate::arena::HeapGeneration::Unknown diff --git a/crates/perry-runtime/src/gc/copying.rs b/crates/perry-runtime/src/gc/copying.rs index 65d4d885f0..8bfea29275 100644 --- a/crates/perry-runtime/src/gc/copying.rs +++ b/crates/perry-runtime/src/gc/copying.rs @@ -127,7 +127,7 @@ impl CopyingPointerSet { #[inline] pub(super) fn raw_pointer_candidate(bits: u64) -> bool { - (0x1000..=POINTER_MASK).contains(&bits) && bits & 0x7 == 0 + (MIN_HEAP_POINTER..=POINTER_MASK).contains(&bits) && bits & 0x7 == 0 } #[inline] @@ -135,7 +135,7 @@ impl CopyingPointerSet { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { let addr = (bits & POINTER_MASK) as usize; - return (addr != 0).then_some((addr, true, tag)); + return (addr >= MIN_HEAP_POINTER as usize).then_some((addr, true, tag)); } if tag >= 0x7FF8_0000_0000_0000 { return None; @@ -155,7 +155,7 @@ impl CopyingPointerSet { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { let addr = (bits & POINTER_MASK) as usize; - if addr == 0 { + if addr < MIN_HEAP_POINTER as usize { return Ok(None); } return self diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 977193cab0..b1d6044281 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -271,12 +271,12 @@ pub(super) fn strip_nanbox_user_ptr(bits: u64) -> usize { pub(super) fn layout_pointer_bearing_bits(bits: u64) -> bool { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { - return bits & POINTER_MASK != 0; + return bits & POINTER_MASK >= MIN_HEAP_POINTER; } if tag >= 0x7FF8_0000_0000_0000 { return false; } - (0x1000..=POINTER_MASK).contains(&bits) && (bits & 0x7) == 0 + (MIN_HEAP_POINTER..=POINTER_MASK).contains(&bits) && (bits & 0x7) == 0 } #[inline] diff --git a/crates/perry-runtime/src/gc/roots.rs b/crates/perry-runtime/src/gc/roots.rs index f704f15f0b..539440c3d7 100644 --- a/crates/perry-runtime/src/gc/roots.rs +++ b/crates/perry-runtime/src/gc/roots.rs @@ -518,7 +518,7 @@ pub(super) fn try_mark_value_or_raw(word: u64, valid_ptrs: &ValidPointerSet) -> // and plain integers. Valid heap pointers are in the lower 48-bit address space and // won't have NaN-boxing tags in upper bits (already rejected above). let raw_ptr_u64 = word; - if !(0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&raw_ptr_u64) { + if !(MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&raw_ptr_u64) { return false; // Too small (null/invalid) or has upper bits set (NaN tag or non-address) } let raw_ptr = raw_ptr_u64 as usize; @@ -904,9 +904,11 @@ impl<'a> RuntimeRootVisitor<'a> { RuntimeRootVisitMode::Copy { mark } => { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { - (*mark)(f64::from_bits(bits)); + if bits & POINTER_MASK >= MIN_HEAP_POINTER { + (*mark)(f64::from_bits(bits)); + } } else if tag < 0x7FF8_0000_0000_0000 - && (0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&bits) + && (MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&bits) { (*mark)(f64::from_bits(POINTER_TAG | (bits & POINTER_MASK))); } @@ -917,7 +919,7 @@ impl<'a> RuntimeRootVisitor<'a> { #[inline] pub(super) fn visit_tagged_raw_addr(&mut self, addr: usize, copy_tag: u64) -> Option { - if addr == 0 { + if addr < MIN_HEAP_POINTER as usize { return None; } match &mut self.mode { @@ -1458,7 +1460,7 @@ pub(super) fn mark_mutable_root_slots_step( pub(super) fn shadow_slot_pointer_root(bits: u64) -> bool { let tag = bits & TAG_MASK; let addr = bits & POINTER_MASK; - addr != 0 && (tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG) + addr >= MIN_HEAP_POINTER && (tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG) } #[inline] @@ -1477,7 +1479,7 @@ pub(super) fn mutable_slot_points_to_valid_root(bits: u64, valid_ptrs: &ValidPoi return valid_ptrs.contains(&addr); } let raw_ptr = bits as usize; - raw_ptr != 0 && valid_ptrs.contains(&raw_ptr) + raw_ptr >= MIN_HEAP_POINTER as usize && valid_ptrs.contains(&raw_ptr) } #[inline] @@ -1567,7 +1569,10 @@ pub(super) fn nanboxed_root_header( return None; } let ptr_val = (value_bits & POINTER_MASK) as usize; - if ptr_val == 0 || !valid_ptrs.maybe_contains(ptr_val) || !valid_ptrs.contains(&ptr_val) { + if ptr_val < MIN_HEAP_POINTER as usize + || !valid_ptrs.maybe_contains(ptr_val) + || !valid_ptrs.contains(&ptr_val) + { return None; } Some(unsafe { header_from_user_ptr(ptr_val as *const u8) }) diff --git a/crates/perry-runtime/src/gc/tests/barrier.rs b/crates/perry-runtime/src/gc/tests/barrier.rs index 548d0898b3..efc1421975 100644 --- a/crates/perry-runtime/src/gc/tests/barrier.rs +++ b/crates/perry-runtime/src/gc/tests/barrier.rs @@ -299,6 +299,13 @@ fn test_write_barrier_non_pointer_child_skipped() { 0, "number child must not enter remembered set" ); + // Small native handles use pointer-like NaN-box tags but are not heap objects. + js_write_barrier(POINTER_TAG | (old as u64), POINTER_TAG | 0xE0000); + assert_eq!( + remembered_set_size(), + 0, + "small native-handle child must not enter remembered set" + ); } #[test] @@ -312,6 +319,12 @@ fn test_write_barrier_non_pointer_parent_skipped() { 0, "non-pointer parent must not dirty remembered pages" ); + js_write_barrier_slot(POINTER_TAG | 0xE0000, 0, POINTER_TAG | young as u64); + assert_eq!( + remembered_set_size(), + 0, + "small native-handle parent must not dirty remembered pages" + ); } #[test] diff --git a/crates/perry-runtime/src/gc/tests/layout_trace.rs b/crates/perry-runtime/src/gc/tests/layout_trace.rs index 5b4d106ec3..72a8fabf26 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace.rs @@ -1502,3 +1502,24 @@ fn test_trace_closure_uses_pointer_layout_mask() { clear_marks(); clear_mark_seeds(); } + +#[test] +fn test_layout_mask_ignores_small_pointer_tagged_native_handles() { + clear_marks(); + clear_mark_seeds(); + + let closure = crate::closure::js_closure_alloc(layout_mask_test_closure as *const u8, 2); + let native_handle_bits = POINTER_TAG | 0xE0000; + crate::closure::js_closure_set_capture_f64(closure, 0, f64::from_bits(native_handle_bits)); + crate::closure::js_closure_set_capture_ptr(closure, 1, 0xE0000); + + assert_eq!(test_layout_pointer_slot_count(closure as usize, 2), Some(0)); + let slots = unsafe { test_heap_child_slots_for_user(closure as *mut u8) }; + assert!(matches!( + slots.as_slice(), + [HeapChildSlot::PointerFreeRange(range)] if range.slot_count() == 2 + )); + + clear_marks(); + clear_mark_seeds(); +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots.rs b/crates/perry-runtime/src/gc/tests/runtime_roots.rs index ed17bb48a9..4127d1802b 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots.rs @@ -936,6 +936,34 @@ fn test_transient_runtime_handle_scope_drop_removes_roots() { } } +#[test] +fn test_transient_runtime_handle_scope_ignores_small_native_handle_root() { + clear_marks(); + clear_mark_seeds(); + + let live = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let valid_ptrs = build_valid_pointer_set(); + let native_handle_bits = POINTER_TAG | 0xE0000; + { + let scope = RuntimeHandleScope::new(); + let native = scope.root_nanbox_f64(f64::from_bits(native_handle_bits)); + let heap = scope.root_nanbox_u64(ptr_bits(live as usize)); + assert_eq!(native.get_nanbox_u64(), native_handle_bits); + assert_eq!(heap.get_nanbox_u64(), ptr_bits(live as usize)); + + let mut marker = RuntimeRootVisitor::for_mark(&valid_ptrs); + scan_runtime_handle_roots_mut(&mut marker); + } + + unsafe { + assert_ne!((*header_from_user_ptr(live)).gc_flags & GC_FLAG_MARKED, 0); + } + assert_eq!(RuntimeHandleScope::active_len_for_tests(), 0); + + clear_marks(); + clear_mark_seeds(); +} + #[test] fn test_set_gc_field_rewrite_reindexes_elements() { clear_marks(); diff --git a/crates/perry-runtime/src/gc/trace.rs b/crates/perry-runtime/src/gc/trace.rs index fe7528a688..24e2d5ecfd 100644 --- a/crates/perry-runtime/src/gc/trace.rs +++ b/crates/perry-runtime/src/gc/trace.rs @@ -388,7 +388,7 @@ pub(super) fn try_mark_value(value_bits: u64, valid_ptrs: &ValidPointerSet) -> b return false; } let ptr_val = (value_bits & POINTER_MASK) as usize; - if ptr_val == 0 { + if ptr_val < MIN_HEAP_POINTER as usize { return false; } @@ -458,7 +458,7 @@ pub(super) unsafe fn mark_field_into_worklist( let tag = val_bits & TAG_MASK; let ptr_val: usize = if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { let p = (val_bits & POINTER_MASK) as usize; - if p == 0 { + if p < MIN_HEAP_POINTER as usize { return false; } p @@ -468,7 +468,7 @@ pub(super) unsafe fn mark_field_into_worklist( // user-address range. f64 numbers have the exponent bits set, // which puts them well above 0x0000_FFFF_FFFF_FFFF — they're // rejected here. - if !(0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&val_bits) { + if !(MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&val_bits) { return false; } val_bits as usize diff --git a/crates/perry-runtime/src/gc/types.rs b/crates/perry-runtime/src/gc/types.rs index 631dcf7cfc..ed941f5d81 100644 --- a/crates/perry-runtime/src/gc/types.rs +++ b/crates/perry-runtime/src/gc/types.rs @@ -784,3 +784,4 @@ pub(super) const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; pub(super) const BIGINT_TAG: u64 = 0x7FFA_0000_0000_0000; pub(super) const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; pub(super) const TAG_MASK: u64 = 0xFFFF_0000_0000_0000; +pub(super) const MIN_HEAP_POINTER: u64 = 0x100000; diff --git a/crates/perry-runtime/src/gc/verify.rs b/crates/perry-runtime/src/gc/verify.rs index 2b4c37d930..a445b75075 100644 --- a/crates/perry-runtime/src/gc/verify.rs +++ b/crates/perry-runtime/src/gc/verify.rs @@ -4,7 +4,11 @@ pub(super) fn try_rewrite_value(bits: u64, valid_ptrs: &ValidPointerSet) -> Opti let tag = bits & TAG_MASK; let (ptr_addr, is_nanbox) = match tag { t if t == POINTER_TAG || t == STRING_TAG || t == BIGINT_TAG => { - ((bits & POINTER_MASK) as usize, true) + let addr = (bits & POINTER_MASK) as usize; + if addr < MIN_HEAP_POINTER as usize { + return None; + } + (addr, true) } _ => { // Reject NaN-tagged non-pointer values (numbers, @@ -13,7 +17,7 @@ pub(super) fn try_rewrite_value(bits: u64, valid_ptrs: &ValidPointerSet) -> Opti return None; } // Raw pointer fallback: lower 48 bits valid range. - if !(0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&bits) { + if !(MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&bits) { return None; } (bits as usize, false) @@ -33,12 +37,15 @@ pub(super) fn try_rewrite_nanboxed_value(bits: u64, valid_ptrs: &ValidPointerSet return None; } let ptr_addr = (bits & POINTER_MASK) as usize; + if ptr_addr < MIN_HEAP_POINTER as usize { + return None; + } let new_user = try_rewrite_raw_addr(ptr_addr, valid_ptrs)?; Some(tag | (new_user as u64 & POINTER_MASK)) } pub(super) fn try_rewrite_raw_addr(ptr_addr: usize, valid_ptrs: &ValidPointerSet) -> Option { - if ptr_addr == 0 { + if ptr_addr < MIN_HEAP_POINTER as usize { return None; } let mut current = ptr_addr; diff --git a/crates/perry-runtime/src/object/class_handles.rs b/crates/perry-runtime/src/object/class_handles.rs index 291328c07b..2875110559 100644 --- a/crates/perry-runtime/src/object/class_handles.rs +++ b/crates/perry-runtime/src/object/class_handles.rs @@ -70,6 +70,9 @@ pub type FetchHandleKindProbeFn = unsafe extern "C" fn(id: usize) -> u8; /// as heap objects. pub type EventEmitterHandleProbeFn = unsafe extern "C" fn(handle: i64) -> bool; pub type EventEmitterAsyncResourceHandleProbeFn = unsafe extern "C" fn(handle: i64) -> bool; +pub type ExternalEventEmitterDomainProbeFn = unsafe extern "C" fn(handle: i64) -> bool; +pub type ExternalEventEmitterDomainGetFn = unsafe extern "C" fn(handle: i64) -> i64; +pub type ExternalEventEmitterDomainSetFn = unsafe extern "C" fn(handle: i64, domain: i64) -> i32; /// Probe for stdlib `net.Socket` handles. Socket instances are represented as /// pointer-tagged small integer handles, not heap objects with class ids. @@ -96,6 +99,9 @@ static FETCH_HANDLE_KIND_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut static EVENT_EMITTER_HANDLE_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static EVENT_EMITTER_ASYNC_RESOURCE_HANDLE_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); +static EXTERNAL_EVENT_EMITTER_DOMAIN_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); +static EXTERNAL_EVENT_EMITTER_DOMAIN_GET_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); +static EXTERNAL_EVENT_EMITTER_DOMAIN_SET_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static NET_SOCKET_HANDLE_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static EVENT_EMITTER_ON_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); @@ -239,6 +245,57 @@ pub unsafe extern "C" fn js_register_event_emitter_async_resource_handle_probe( EVENT_EMITTER_ASYNC_RESOURCE_HANDLE_PROBE_PTR.store(f as *mut (), Ordering::Release); } +#[inline] +pub fn external_event_emitter_domain_probe() -> Option { + let p = EXTERNAL_EVENT_EMITTER_DOMAIN_PROBE_PTR.load(Ordering::Acquire); + if p.is_null() { + None + } else { + Some(unsafe { std::mem::transmute::<*mut (), ExternalEventEmitterDomainProbeFn>(p) }) + } +} + +#[inline] +pub fn external_event_emitter_domain_get() -> Option { + let p = EXTERNAL_EVENT_EMITTER_DOMAIN_GET_PTR.load(Ordering::Acquire); + if p.is_null() { + None + } else { + Some(unsafe { std::mem::transmute::<*mut (), ExternalEventEmitterDomainGetFn>(p) }) + } +} + +#[inline] +pub fn external_event_emitter_domain_set() -> Option { + let p = EXTERNAL_EVENT_EMITTER_DOMAIN_SET_PTR.load(Ordering::Acquire); + if p.is_null() { + None + } else { + Some(unsafe { std::mem::transmute::<*mut (), ExternalEventEmitterDomainSetFn>(p) }) + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_external_event_emitter_domain_probe( + f: ExternalEventEmitterDomainProbeFn, +) { + EXTERNAL_EVENT_EMITTER_DOMAIN_PROBE_PTR.store(f as *mut (), Ordering::Release); +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_external_event_emitter_domain_get( + f: ExternalEventEmitterDomainGetFn, +) { + EXTERNAL_EVENT_EMITTER_DOMAIN_GET_PTR.store(f as *mut (), Ordering::Release); +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_external_event_emitter_domain_set( + f: ExternalEventEmitterDomainSetFn, +) { + EXTERNAL_EVENT_EMITTER_DOMAIN_SET_PTR.store(f as *mut (), Ordering::Release); +} + #[inline] pub fn net_socket_handle_probe() -> Option { let p = NET_SOCKET_HANDLE_PROBE_PTR.load(Ordering::Acquire); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 9b20d59705..91d50e16f0 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -12,10 +12,13 @@ pub use super::class_handles::{ event_emitter_async_resource_handle_probe, event_emitter_handle_probe, event_emitter_on, - fetch_handle_kind_probe, handle_method_dispatch, handle_own_property_names_dispatch, - handle_property_dispatch, handle_property_set_dispatch, handle_prototype_dispatch, - js_register_event_emitter_async_resource_handle_probe, js_register_event_emitter_handle_probe, - js_register_event_emitter_on, js_register_fetch_handle_kind_probe, + external_event_emitter_domain_get, external_event_emitter_domain_probe, + external_event_emitter_domain_set, fetch_handle_kind_probe, handle_method_dispatch, + handle_own_property_names_dispatch, handle_property_dispatch, handle_property_set_dispatch, + handle_prototype_dispatch, js_register_event_emitter_async_resource_handle_probe, + js_register_event_emitter_handle_probe, js_register_event_emitter_on, + js_register_external_event_emitter_domain_get, js_register_external_event_emitter_domain_probe, + js_register_external_event_emitter_domain_set, js_register_fetch_handle_kind_probe, js_register_handle_method_dispatch, js_register_handle_own_property_names_dispatch, js_register_handle_property_dispatch, js_register_handle_property_set_dispatch, js_register_handle_prototype_dispatch, js_register_net_socket_handle_probe, diff --git a/crates/perry-stdlib/src/common/dispatch.rs b/crates/perry-stdlib/src/common/dispatch.rs index fec7989f40..91dab833db 100644 --- a/crates/perry-stdlib/src/common/dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch.rs @@ -18,9 +18,16 @@ fn nanbox_handle_value(handle: i64) -> f64 { f64::from_bits(POINTER_TAG_BITS | (handle as u64 & POINTER_MASK_BITS)) } +fn root_nanbox_f64_slice<'scope>( + scope: &'scope perry_runtime::gc::RuntimeHandleScope, + args: &[f64], +) -> Vec> { + args.iter().map(|arg| scope.root_nanbox_f64(*arg)).collect() +} + unsafe fn pack_args_array(args: &[f64]) -> *mut perry_runtime::ArrayHeader { let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let arg_handles = scope.root_nanbox_f64_slice(args); + let arg_handles = root_nanbox_f64_slice(&scope, args); let arr = perry_runtime::js_array_alloc(0); let arr_handle = scope.root_raw_mut_ptr(arr); for arg in &arg_handles { @@ -195,15 +202,16 @@ pub unsafe extern "C" fn js_handle_method_dispatch( }; let method_name = method_name_owned.as_str(); let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let original_args: Vec = if args_len > 0 && !args_ptr.is_null() { + let mut args: Vec = if args_len > 0 && !args_ptr.is_null() { std::slice::from_raw_parts(args_ptr, args_len).to_vec() } else { Vec::new() }; - let arg_handles = scope.root_nanbox_f64_slice(&original_args); - let args = perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + let arg_handles = root_nanbox_f64_slice(&scope, &args); + for (arg, handle) in args.iter_mut().zip(arg_handles.iter()) { + *arg = handle.get_nanbox_f64(); + } let _ = method_name; - let _ = args; let _ = handle; if let Some(v) = crate::domain::dispatch_domain_method(handle, method_name, &args) { @@ -2302,7 +2310,7 @@ pub unsafe extern "C" fn js_handle_property_dispatch( unsafe fn dispatch_sqlite_stmt(handle: i64, method: &str, args: &[f64]) -> f64 { use perry_runtime::js_nanbox_pointer; let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let arg_handles = scope.root_nanbox_f64_slice(args); + let arg_handles = root_nanbox_f64_slice(&scope, args); // Pack args into a fresh JS array. Each `f64` is already a // NaN-boxed value as the codegen produces. js_array_push takes a // perry_ffi::JsValue (NaN-boxed), but the runtime helpers in @@ -2706,7 +2714,6 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { js_register_handle_property_set_dispatch(js_handle_property_set_dispatch); js_register_handle_own_property_names_dispatch(js_handle_own_property_names_dispatch); js_register_handle_prototype_dispatch(js_handle_prototype_dispatch); - crate::string_decoder::string_decoder_prototype_value(); #[cfg(feature = "http-client")] js_register_global_fetch_with_options(crate::fetch::js_fetch_with_options); #[cfg(feature = "http-client")] diff --git a/crates/perry-stdlib/src/domain.rs b/crates/perry-stdlib/src/domain.rs index 6addcfe63e..fc733303d1 100644 --- a/crates/perry-stdlib/src/domain.rs +++ b/crates/perry-stdlib/src/domain.rs @@ -12,40 +12,83 @@ use std::sync::Once; // `events` is feature-gated behind `bundled-events`; when the well-known // bindings table routes `import 'events'` to perry-ext-events the in-tree -// module is configured out (and the default auto-optimize build compiles -// without it). The domain<->EventEmitter integration degrades to inert -// no-ops in that build — these shims keep `mod domain` (which is NOT -// feature-gated) compiling either way. +// module may not own EventEmitter handles. The runtime hooks below let the +// external EventEmitter implementation participate in domain membership +// without making this stdlib module link directly against that crate. #[cfg(feature = "bundled-events")] #[inline] -fn ee_is_event_emitter_handle(handle: Handle) -> bool { +fn stdlib_ee_is_event_emitter_handle(handle: Handle) -> bool { crate::events::is_event_emitter_handle(handle) } #[cfg(not(feature = "bundled-events"))] #[inline] -fn ee_is_event_emitter_handle(_handle: Handle) -> bool { +fn stdlib_ee_is_event_emitter_handle(_handle: Handle) -> bool { false } #[cfg(feature = "bundled-events")] #[inline] -fn ee_get_domain(handle: Handle) -> Handle { +fn stdlib_ee_get_domain(handle: Handle) -> Handle { crate::events::js_event_emitter_get_domain(handle) } #[cfg(not(feature = "bundled-events"))] #[inline] -fn ee_get_domain(_handle: Handle) -> Handle { +fn stdlib_ee_get_domain(_handle: Handle) -> Handle { 0 } #[cfg(feature = "bundled-events")] #[inline] -fn ee_set_domain(handle: Handle, domain: Handle) { +fn stdlib_ee_set_domain(handle: Handle, domain: Handle) { let _ = crate::events::js_event_emitter_set_domain(handle, domain); } #[cfg(not(feature = "bundled-events"))] #[inline] -fn ee_set_domain(_handle: Handle, _domain: Handle) {} +fn stdlib_ee_set_domain(_handle: Handle, _domain: Handle) {} + +#[inline] +fn external_ee_is_event_emitter_handle(handle: Handle) -> bool { + perry_runtime::object::external_event_emitter_domain_probe() + .is_some_and(|probe| unsafe { probe(handle) }) +} + +#[inline] +fn external_ee_get_domain(handle: Handle) -> Handle { + perry_runtime::object::external_event_emitter_domain_get() + .map(|get| unsafe { get(handle) }) + .unwrap_or(0) +} + +#[inline] +fn external_ee_set_domain(handle: Handle, domain: Handle) { + if let Some(set) = perry_runtime::object::external_event_emitter_domain_set() { + let _ = unsafe { set(handle, domain) }; + } +} + +#[inline] +fn ee_is_event_emitter_handle(handle: Handle) -> bool { + stdlib_ee_is_event_emitter_handle(handle) || external_ee_is_event_emitter_handle(handle) +} + +#[inline] +fn ee_get_domain(handle: Handle) -> Handle { + let stdlib_domain = stdlib_ee_get_domain(handle); + if stdlib_domain != 0 { + stdlib_domain + } else { + external_ee_get_domain(handle) + } +} + +#[inline] +fn ee_set_domain(handle: Handle, domain: Handle) { + if stdlib_ee_is_event_emitter_handle(handle) { + stdlib_ee_set_domain(handle, domain); + } else if external_ee_is_event_emitter_handle(handle) { + external_ee_set_domain(handle, domain); + } +} const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; @@ -260,6 +303,16 @@ pub unsafe fn js_domain_emit_error( emit_domain_event(handle, "error", &[error]) } +#[no_mangle] +pub unsafe extern "C" fn js_domain_emit_error_from_event_emitter( + handle: Handle, + error: f64, + emitter: f64, + domain_thrown: i32, +) -> i32 { + js_domain_emit_error(handle, error, emitter, domain_thrown != 0) as i32 +} + unsafe fn call_with_domain(handle: Handle, callback: f64, args: &[f64]) -> f64 { enter_domain(handle); let trap_buf = perry_runtime::exception::js_try_push(); diff --git a/crates/perry-stdlib/src/events.rs b/crates/perry-stdlib/src/events.rs index 227025e2cd..6702deb3f2 100644 --- a/crates/perry-stdlib/src/events.rs +++ b/crates/perry-stdlib/src/events.rs @@ -922,6 +922,13 @@ unsafe fn collect_emit_args(args_ptr: *const ArrayHeader) -> Vec { args } +fn root_nanbox_f64_slice<'scope>( + scope: &'scope perry_runtime::gc::RuntimeHandleScope, + args: &[f64], +) -> Vec> { + args.iter().map(|arg| scope.root_nanbox_f64(*arg)).collect() +} + unsafe fn call_emitter_listener( handle: Handle, async_resource_handle: i64, @@ -932,7 +939,7 @@ unsafe fn call_emitter_listener( let callback_value = js_nanbox_pointer(callback); if async_resource_handle != 0 { let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let arg_handles = scope.root_nanbox_f64_slice(args); + let arg_handles = root_nanbox_f64_slice(&scope, args); let arr = js_array_alloc(0); let arr_handle = scope.root_raw_mut_ptr(arr); for arg in &arg_handles { diff --git a/crates/perry-stdlib/src/events/domain.rs b/crates/perry-stdlib/src/events/domain.rs index d77dec1786..2b8d81e57f 100644 --- a/crates/perry-stdlib/src/events/domain.rs +++ b/crates/perry-stdlib/src/events/domain.rs @@ -33,6 +33,19 @@ pub extern "C" fn js_event_emitter_get_domain(handle: Handle) -> Handle { #[no_mangle] pub extern "C" fn js_event_emitter_domain_value(handle: Handle) -> f64 { let domain = js_event_emitter_get_domain(handle); + let domain = if domain == 0 { + let is_external = perry_runtime::object::external_event_emitter_domain_probe() + .is_some_and(|probe| unsafe { probe(handle) }); + if is_external { + perry_runtime::object::external_event_emitter_domain_get() + .map(|get| unsafe { get(handle) }) + .unwrap_or(0) + } else { + 0 + } + } else { + domain + }; if domain == 0 { f64::from_bits(TAG_NULL_F64_BITS) } else {