From 2704a6c1bccbfb09524e72ef502923627e0b0669 Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Thu, 4 Jun 2026 13:00:39 +0000 Subject: [PATCH] fix(domain): route emitter errors through domains --- crates/perry-ext-events/src/lib.rs | 403 +++++++++++++++--- .../perry-runtime/src/object/class_handles.rs | 34 ++ .../src/object/class_registry.rs | 16 +- crates/perry-stdlib/src/domain.rs | 58 ++- crates/perry-stdlib/src/events.rs | 93 ++-- .../perry/src/commands/compile/strip_dedup.rs | 55 ++- 6 files changed, 521 insertions(+), 138 deletions(-) diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index f4ead6bd1c..0a8f1d54b6 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -20,12 +20,13 @@ //! `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, + js_array_alloc, js_array_get, js_array_push, js_array_set, nanbox_string_bits, read_string, + ArrayHeader, Handle, JsClosure, JsPromise, JsString, JsValue, ObjectHeader, Promise, + RawClosureHeader, StringHeader, }; use std::collections::HashMap; +use std::ffi::c_void; +use std::sync::{Mutex, MutexGuard, Once, OnceLock}; const MIN_HEAP_POINTER: u64 = 0x1000; const EVENT_TARGET_MIN_HEAP_POINTER: u64 = 0x10000; @@ -47,6 +48,12 @@ const MAX_HEAP_POINTER: u64 = 0x0000_FFFF_FFFF_FFFF; // matches the `then`/`await` ordering Node expects, and sidesteps the // pump-registration coupling entirely. extern "C" { + fn perry_ffi_gc_register_mutable_root_scanner_named( + source_ptr: *const u8, + source_len: usize, + scanner_id: usize, + scanner: PerryFfiNamedMutableRootScanner, + ); fn js_promise_resolve(promise: *mut Promise, value: f64); fn js_promise_reject(promise: *mut Promise, reason: f64); // #1557: closure allocation hooks needed by events.on's queue-listener. @@ -73,6 +80,11 @@ 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_domain_emit_error(handle: Handle, error: f64, emitter: f64, domain_thrown: bool) -> bool; + fn js_register_event_emitter_handle_probe(f: unsafe extern "C" fn(i64) -> bool); + fn js_register_event_emitter_get_domain(f: unsafe extern "C" fn(i64) -> i64); + fn js_register_event_emitter_set_domain(f: unsafe extern "C" fn(i64, i64) -> i32); + fn js_register_event_emitter_on(f: unsafe extern "C" fn(i64, i64, i64) -> i64); // #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 @@ -90,8 +102,47 @@ unsafe fn validate_event_listener(listener_bits: i64) -> i64 { } const TAG_UNDEFINED_F64_BITS: u64 = 0x7FFC_0000_0000_0001; +const TAG_NULL_F64_BITS: u64 = 0x7FFC_0000_0000_0002; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; +const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; +const EVENT_EMITTER_HANDLE_ID_START: Handle = 0x38000; +const EVENT_EMITTER_HANDLE_ID_END: Handle = 0x40000; +const FFI_ROOT_SLOT_I64: u32 = 1; +const FFI_ROOT_SLOT_RAW_MUT_PTR: u32 = 3; +const FFI_ROOT_SLOT_NANBOX_F64: u32 = 4; + +type PerryFfiMutableRootVisitor = + extern "C" fn(kind: u32, slot: *mut c_void, ctx: *mut c_void) -> bool; +type PerryFfiNamedMutableRootScanner = + extern "C" fn(scanner_id: usize, visit: PerryFfiMutableRootVisitor, ctx: *mut c_void); + +struct EventsRootVisitor { + visit: PerryFfiMutableRootVisitor, + ctx: *mut c_void, +} + +impl EventsRootVisitor { + fn visit_i64_slot(&mut self, slot: &mut i64) -> bool { + (self.visit)(FFI_ROOT_SLOT_I64, slot as *mut i64 as *mut c_void, self.ctx) + } + + fn visit_raw_mut_ptr_slot(&mut self, slot: &mut *mut T) -> bool { + (self.visit)( + FFI_ROOT_SLOT_RAW_MUT_PTR, + slot as *mut *mut T as *mut c_void, + self.ctx, + ) + } + + fn visit_nanbox_f64_slot(&mut self, slot: &mut f64) -> bool { + (self.visit)( + FFI_ROOT_SLOT_NANBOX_F64, + slot as *mut f64 as *mut c_void, + self.ctx, + ) + } +} #[inline] fn nanbox_pointer_bits(ptr: i64) -> f64 { @@ -121,6 +172,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 +196,7 @@ impl EventEmitterHandle { // Node's default — `getMaxListeners()` on a fresh emitter // returns 10. max_listeners: 10, + domain_handle: None, } } @@ -178,18 +231,139 @@ impl EventEmitterHandle { } } -static EVENTS_GC_REGISTERED: std::sync::Once = std::sync::Once::new(); +type EventEmitterRegistry = Vec>>; + +static EVENT_EMITTERS: OnceLock> = OnceLock::new(); +static EVENTS_RUNTIME_HOOKS_REGISTERED: Once = Once::new(); + +thread_local! { + static EVENTS_GC_REGISTERED: std::cell::Cell = std::cell::Cell::new(false); +} + +fn event_emitters() -> &'static Mutex { + EVENT_EMITTERS.get_or_init(|| Mutex::new(Vec::new())) +} + +fn lock_event_emitters() -> MutexGuard<'static, EventEmitterRegistry> { + event_emitters() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +fn handle_index(handle: Handle) -> Option { + if !(EVENT_EMITTER_HANDLE_ID_START..EVENT_EMITTER_HANDLE_ID_END).contains(&handle) { + return None; + } + Some((handle - EVENT_EMITTER_HANDLE_ID_START) as usize) +} + +fn register_event_emitter_handle(value: EventEmitterHandle) -> Handle { + let mut registry = lock_event_emitters(); + if let Some((idx, slot)) = registry + .iter_mut() + .enumerate() + .find(|(_, slot)| slot.is_none()) + { + *slot = Some(Box::new(value)); + return EVENT_EMITTER_HANDLE_ID_START + idx as Handle; + } + let handle = EVENT_EMITTER_HANDLE_ID_START + registry.len() as Handle; + if handle >= EVENT_EMITTER_HANDLE_ID_END { + panic!("perry-ext-events handle id range exhausted"); + } + registry.push(Some(Box::new(value))); + handle +} + +fn event_emitter_ptr(handle: Handle) -> Option<*mut EventEmitterHandle> { + let idx = handle_index(handle)?; + let mut registry = lock_event_emitters(); + let slot = registry.get_mut(idx)?.as_mut()?; + Some(&mut **slot as *mut EventEmitterHandle) +} + +fn get_event_emitter_mut(handle: Handle) -> Option<&'static mut EventEmitterHandle> { + let ptr = event_emitter_ptr(handle)?; + Some(unsafe { &mut *ptr }) +} + +fn is_local_event_emitter_handle(handle: Handle) -> bool { + let Some(idx) = handle_index(handle) else { + return false; + }; + let registry = lock_event_emitters(); + registry.get(idx).is_some_and(|slot| slot.is_some()) +} + +#[cfg(test)] +fn drop_event_emitter_handle(handle: Handle) -> bool { + let Some(idx) = handle_index(handle) else { + return false; + }; + let mut registry = lock_event_emitters(); + let Some(slot) = registry.get_mut(idx) else { + return false; + }; + slot.take().is_some() +} + +unsafe extern "C" fn event_emitter_handle_probe(handle: i64) -> bool { + is_local_event_emitter_handle(handle) +} + +unsafe extern "C" fn event_emitter_on_hook( + handle: i64, + event_bits: i64, + listener_bits: i64, +) -> i64 { + js_event_emitter_on( + handle, + string_header_ptr_from_arg(event_bits as *const StringHeader), + listener_bits, + ) +} + +fn ensure_runtime_hooks_registered() { + EVENTS_RUNTIME_HOOKS_REGISTERED.call_once(|| unsafe { + js_register_event_emitter_handle_probe(event_emitter_handle_probe); + js_register_event_emitter_get_domain(js_event_emitter_get_domain); + js_register_event_emitter_set_domain(js_event_emitter_set_domain); + js_register_event_emitter_on(event_emitter_on_hook); + }); +} fn ensure_gc_scanner_registered() { - EVENTS_GC_REGISTERED.call_once(|| { - gc_register_mutable_root_scanner_named("perry-ext-events", scan_events_roots); + EVENTS_GC_REGISTERED.with(|registered| { + if registered.get() { + return; + } + unsafe { + const SOURCE: &str = "perry-ext-events"; + perry_ffi_gc_register_mutable_root_scanner_named( + SOURCE.as_ptr(), + SOURCE.len(), + 0, + scan_events_roots_trampoline, + ); + } + registered.set(true); }); } +extern "C" fn scan_events_roots_trampoline( + _scanner_id: usize, + visit: PerryFfiMutableRootVisitor, + ctx: *mut c_void, +) { + let mut visitor = EventsRootVisitor { visit, ctx }; + scan_events_roots(&mut visitor); +} + /// GC root scanner: visit every registered EventEmitterHandle, /// and expose every listener closure pointer + pending Promise slot. -fn scan_events_roots(visitor: &mut GcRootVisitor<'_>) { - iter_handles_of_mut::(|emitter| { +fn scan_events_roots(visitor: &mut EventsRootVisitor) { + let mut registry = lock_event_emitters(); + for emitter in registry.iter_mut().filter_map(|slot| slot.as_deref_mut()) { for listeners in emitter.events.values_mut() { for l in listeners.iter_mut() { if is_heap_pointer_candidate(l.callback) { @@ -206,7 +380,7 @@ fn scan_events_roots(visitor: &mut GcRootVisitor<'_>) { } } } - }); + } } fn is_heap_pointer_candidate(callback: i64) -> bool { @@ -234,6 +408,7 @@ unsafe fn stream_listeners_for_heap_object( handle: Handle, event_name_ptr: *const StringHeader, ) -> Option<*mut ArrayHeader> { + let event_name_ptr = string_header_ptr_from_arg(event_name_ptr); let addr = handle as u64; if event_name_ptr.is_null() || !(EVENT_TARGET_MIN_HEAP_POINTER..=MAX_HEAP_POINTER).contains(&addr) @@ -248,12 +423,41 @@ unsafe fn stream_listeners_for_heap_object( fn handle_from_js_value_bits(bits: u64) -> Handle { if (bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { (bits & POINTER_MASK) as Handle + } else { + let value = f64::from_bits(bits); + if value.is_finite() && value > 0.0 && value.fract() == 0.0 { + value as Handle + } else { + bits as Handle + } + } +} + +fn handle_from_value(value: f64) -> Handle { + let bits = value.to_bits(); + if (bits & 0xFFFF_0000_0000_0000) == POINTER_TAG { + (bits & POINTER_MASK) as Handle + } else if value.is_finite() && value > 0.0 && value.fract() == 0.0 { + value as Handle } else { bits as Handle } } +fn string_header_ptr_from_arg(ptr: *const StringHeader) -> *const StringHeader { + let raw = ptr as u64; + if (raw & 0xFFFF_0000_0000_0000) == STRING_TAG { + (raw & POINTER_MASK) as *const StringHeader + } else { + ptr + } +} + unsafe fn read_str(ptr: *const StringHeader) -> Option { + let ptr = string_header_ptr_from_arg(ptr); + if ptr.is_null() || (ptr as usize) < 0x10000 { + return None; + } let handle = JsString::from_raw(ptr as *mut StringHeader); read_string(handle).map(String::from) } @@ -370,8 +574,9 @@ fn remove_listener_by_callback(emitter: &mut EventEmitterHandle, callback: i64) /// `new EventEmitter()` — returns a handle to the emitter. #[no_mangle] pub extern "C" fn js_event_emitter_new() -> Handle { + ensure_runtime_hooks_registered(); ensure_gc_scanner_registered(); - register_handle(EventEmitterHandle::new()) + register_event_emitter_handle(EventEmitterHandle::new()) } /// `new EventEmitter(options?)` — the constructor shape codegen actually @@ -390,8 +595,9 @@ pub extern "C" fn js_event_emitter_new() -> Handle { /// `_options` is a NaN-boxed JS value; it is not dereferenced. #[no_mangle] pub unsafe extern "C" fn js_event_emitter_new_with_options(_options: f64) -> Handle { + ensure_runtime_hooks_registered(); ensure_gc_scanner_registered(); - register_handle(EventEmitterHandle::new()) + register_event_emitter_handle(EventEmitterHandle::new()) } /// `emitter.on(eventName, listener)` — register a listener. @@ -414,7 +620,7 @@ pub unsafe extern "C" fn js_event_emitter_on( let Some(event_name) = read_str(event_name_ptr) else { return handle; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.add_listener(&event_name, callback_ptr, false, false); } handle @@ -436,7 +642,7 @@ pub unsafe extern "C" fn js_event_emitter_once( let Some(event_name) = read_str(event_name_ptr) else { return handle; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.add_listener(&event_name, callback_ptr, true, false); } handle @@ -458,7 +664,7 @@ pub unsafe extern "C" fn js_event_emitter_prepend_listener( let Some(event_name) = read_str(event_name_ptr) else { return handle; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.add_listener(&event_name, callback_ptr, false, true); } handle @@ -480,7 +686,7 @@ pub unsafe extern "C" fn js_event_emitter_prepend_once_listener( let Some(event_name) = read_str(event_name_ptr) else { return handle; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.add_listener(&event_name, callback_ptr, true, true); } handle @@ -590,7 +796,9 @@ pub unsafe extern "C" fn js_event_emitter_emit( return TAG_FALSE_F64; }; let mut had_listeners = false; - if let Some(emitter) = get_handle_mut::(handle) { + let mut domain_error: Option<(Handle, f64)> = None; + let mut throw_error: Option = None; + if let Some(emitter) = get_event_emitter_mut(handle) { let snapshot: Vec = match emitter.events.get(&event_name) { Some(v) if !v.is_empty() => v.clone(), _ => Vec::new(), @@ -614,19 +822,32 @@ 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 { - js_throw(first_arg); + if let Some(domain) = emitter.domain_handle { + domain_error = Some((domain, first_arg)); + } else { + throw_error = Some(first_arg); + } } } - drain_pending_once_promises(emitter, &event_name, args_ptr); + if domain_error.is_none() && throw_error.is_none() { + drain_pending_once_promises(emitter, &event_name, args_ptr); - for l in snapshot { - if l.callback != 0 { - let closure = JsClosure::from_raw(l.callback as *const RawClosureHeader); - let _ = closure.call1(first_arg); + for l in snapshot { + if l.callback != 0 { + let closure = JsClosure::from_raw(l.callback as *const RawClosureHeader); + let _ = closure.call1(first_arg); + } } } } + if let Some((domain, error)) = domain_error { + let _ = js_domain_emit_error(domain, error, nanbox_pointer_bits(handle), false); + return TAG_FALSE_F64; + } + if let Some(error) = throw_error { + js_throw(error); + } if had_listeners { TAG_TRUE_F64 } else { @@ -654,7 +875,9 @@ pub unsafe extern "C" fn js_event_emitter_emit0( return TAG_FALSE_F64; }; let mut had_listeners = false; - if let Some(emitter) = get_handle_mut::(handle) { + let mut domain_error: Option<(Handle, f64)> = None; + let mut throw_error: Option = None; + if let Some(emitter) = get_event_emitter_mut(handle) { let snapshot: Vec = match emitter.events.get(&event_name) { Some(v) if !v.is_empty() => v.clone(), _ => Vec::new(), @@ -679,18 +902,31 @@ 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 { - js_throw(error_value); + if let Some(domain) = emitter.domain_handle { + domain_error = Some((domain, error_value)); + } else { + throw_error = Some(error_value); + } } } - drain_pending_once_promises(emitter, &event_name, empty_args); + if domain_error.is_none() && throw_error.is_none() { + drain_pending_once_promises(emitter, &event_name, empty_args); - for l in snapshot { - if l.callback != 0 { - let closure = JsClosure::from_raw(l.callback as *const RawClosureHeader); - let _ = closure.call0(); + for l in snapshot { + if l.callback != 0 { + let closure = JsClosure::from_raw(l.callback as *const RawClosureHeader); + let _ = closure.call0(); + } } } } + if let Some((domain, error)) = domain_error { + let _ = js_domain_emit_error(domain, error, nanbox_pointer_bits(handle), false); + return TAG_FALSE_F64; + } + if let Some(error) = throw_error { + js_throw(error); + } if had_listeners { TAG_TRUE_F64 } else { @@ -715,7 +951,7 @@ pub unsafe extern "C" fn js_event_emitter_remove_listener( let Some(event_name) = read_str(event_name_ptr) else { return handle; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { let mut removed = false; if let Some(listeners) = emitter.events.get_mut(&event_name) { if let Some(pos) = listeners.iter().position(|l| l.callback == callback_ptr) { @@ -740,7 +976,7 @@ pub unsafe extern "C" fn js_event_emitter_remove_all_listeners( handle: Handle, event_name_ptr: *const StringHeader, ) -> Handle { - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { if event_name_ptr.is_null() { emitter.events.clear(); emitter.event_order.clear(); @@ -767,7 +1003,7 @@ pub unsafe extern "C" fn js_event_emitter_listener_count( let Some(event_name) = read_str(event_name_ptr) else { return 0.0; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { if let Some(listeners) = emitter.events.get(&event_name) { return listeners.len() as f64; } @@ -778,7 +1014,7 @@ pub unsafe extern "C" fn js_event_emitter_listener_count( /// `emitter.setMaxListeners(n)`. #[no_mangle] pub unsafe extern "C" fn js_event_emitter_set_max_listeners(handle: Handle, n: f64) -> Handle { - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.max_listeners = n as i32; } handle @@ -787,18 +1023,50 @@ pub unsafe extern "C" fn js_event_emitter_set_max_listeners(handle: Handle, n: f /// `emitter.getMaxListeners()`. #[no_mangle] pub unsafe extern "C" fn js_event_emitter_get_max_listeners(handle: Handle) -> f64 { - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { return emitter.max_listeners as f64; } 10.0 } +#[no_mangle] +pub extern "C" fn js_event_emitter_is_handle(handle: Handle) -> bool { + is_local_event_emitter_handle(handle) +} + +#[no_mangle] +pub extern "C" fn js_event_emitter_get_domain(handle: Handle) -> Handle { + get_event_emitter_mut(handle) + .and_then(|emitter| emitter.domain_handle) + .unwrap_or(0) +} + +#[no_mangle] +pub extern "C" fn js_event_emitter_set_domain(handle: Handle, domain: Handle) -> i32 { + if let Some(emitter) = get_event_emitter_mut(handle) { + emitter.domain_handle = if domain == 0 { None } else { Some(domain) }; + 1 + } else { + 0 + } +} + +#[no_mangle] +pub extern "C" fn js_event_emitter_domain_value(handle: Handle) -> f64 { + let domain = js_event_emitter_get_domain(handle); + if domain == 0 { + f64::from_bits(TAG_NULL_F64_BITS) + } else { + nanbox_pointer_bits(domain) + } +} + /// `emitter.eventNames()` — returns an array of strings in insertion /// order (matches Node). #[no_mangle] pub unsafe extern "C" fn js_event_emitter_event_names(handle: Handle) -> *mut ArrayHeader { let arr = js_array_alloc(0); - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { let mut result = arr; for name in emitter.event_order.iter() { let alive = emitter @@ -833,7 +1101,7 @@ pub unsafe extern "C" fn js_event_emitter_listeners( let Some(event_name) = read_str(event_name_ptr) else { return arr; }; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { if let Some(listeners) = emitter.events.get(&event_name) { let mut result = arr; for l in listeners.iter() { @@ -872,7 +1140,7 @@ extern "C" fn events_once_abort_listener(closure: *const RawClosureHeader) -> f6 unsafe { let handle = js_closure_get_capture_ptr(closure, 0) as Handle; let promise = js_closure_get_capture_ptr(closure, 1) as *mut Promise; - let pending = get_handle_mut::(handle) + let pending = get_event_emitter_mut(handle) .and_then(|emitter| remove_pending_once_promise(emitter, promise)); if let Some(pending) = pending { cleanup_pending_abort_listener(&pending); @@ -892,13 +1160,14 @@ 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(); let prom = JsPromise::new(); let raw = prom.as_raw(); + let handle = handle_from_value(target_value); let Some(event_name) = read_str(event_name_ptr) else { return raw; }; @@ -907,7 +1176,7 @@ pub unsafe extern "C" fn js_events_once( js_promise_reject(raw, js_abort_error_value()); return raw; } - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { let mut pending = PendingOnce { promise: raw, signal: undefined_value(), @@ -973,7 +1242,7 @@ extern "C" fn events_on_abort_listener(closure: *const RawClosureHeader) -> f64 let signal_ptr = js_closure_get_capture_ptr(closure, 2) as *mut u8; let abort_promise = js_closure_get_capture_ptr(closure, 3) as *mut Promise; - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { remove_listener_by_callback(emitter, data_listener); } if !signal_ptr.is_null() { @@ -1001,12 +1270,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(); let queue = js_array_alloc(0); + let handle = handle_from_value(target_value); let Some(event_name) = read_str(event_name_ptr) else { return queue; }; @@ -1027,7 +1297,7 @@ pub unsafe extern "C" fn js_events_on( let _ = js_array_push_f64(queue, nanbox_pointer_bits(abort_promise as i64)); } - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.add_listener(&event_name, listener as i64, false, false); if let Some(signal) = signal { if let Some(signal_ptr) = object_ptr_from_value(signal) { @@ -1057,8 +1327,12 @@ pub unsafe extern "C" fn js_events_on( /// `signal_ptr` must be null or a Perry-runtime `ObjectHeader` (the /// AbortSignal instance); `callback_ptr` must be null or a closure pointer. #[no_mangle] -pub unsafe extern "C" fn js_events_add_abort_listener(signal_ptr: i64, callback_ptr: i64) -> i64 { - if signal_ptr == 0 || callback_ptr == 0 { +pub unsafe extern "C" fn js_events_add_abort_listener(signal: f64, listener: f64) -> i64 { + let Some(signal_ptr) = object_ptr_from_value(signal) else { + return 0; + }; + let callback_ptr = handle_from_value(listener); + if callback_ptr == 0 { return 0; } let event_name = b"abort"; @@ -1084,10 +1358,11 @@ 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 { - if get_handle_mut::(handle).is_some() { + let handle = handle_from_value(target_value); + if get_event_emitter_mut(handle).is_some() { return js_event_emitter_listeners(handle, event_name_ptr); } if let Some(target) = event_target_ptr(handle) { @@ -1106,16 +1381,18 @@ 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 { + let handle = handle_from_value(target_value); js_event_emitter_listener_count(handle, event_name_ptr) } /// `events.getMaxListeners(emitter)` — alias. #[no_mangle] -pub unsafe extern "C" fn js_events_get_max_listeners(handle: Handle) -> f64 { - if let Some(emitter) = get_handle_mut::(handle) { +pub unsafe extern "C" fn js_events_get_max_listeners(target_value: f64) -> f64 { + let handle = handle_from_value(target_value); + if let Some(emitter) = get_event_emitter_mut(handle) { return emitter.max_listeners as f64; } if let Some(target) = event_target_ptr(handle) { @@ -1135,7 +1412,7 @@ pub unsafe extern "C" fn js_events_set_max_listeners( let len = (*handles_ptr).length; for i in 0..len { let handle = handle_from_js_value_bits(js_array_get(handles_ptr, i).bits()); - if let Some(emitter) = get_handle_mut::(handle) { + if let Some(emitter) = get_event_emitter_mut(handle) { emitter.max_listeners = n as i32; } else if let Some(target) = event_target_ptr(handle) { let _ = js_event_target_set_max_listeners(target, n); @@ -1148,7 +1425,7 @@ pub unsafe extern "C" fn js_events_set_max_listeners( #[cfg(test)] mod tests { use super::*; - use perry_ffi::{alloc_string, drop_handle, get_handle, register_handle}; + use perry_ffi::alloc_string; use std::collections::HashMap; use std::sync::{Mutex, MutexGuard}; @@ -1218,7 +1495,7 @@ mod tests { let event_name = alloc_string("foo"); let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; assert_eq!(count, 0.0); - drop_handle(h); + drop_event_emitter_handle(h); } #[test] @@ -1230,7 +1507,7 @@ mod tests { 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 _) }; assert_eq!(count, 2.0); - drop_handle(h); + drop_event_emitter_handle(h); } #[test] @@ -1246,7 +1523,7 @@ mod tests { } let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; assert_eq!(count, 1.0); - drop_handle(h); + drop_event_emitter_handle(h); } #[test] @@ -1260,7 +1537,7 @@ mod tests { } let count = unsafe { js_event_emitter_listener_count(h, event_name.as_raw() as *const _) }; assert_eq!(count, 0.0); - drop_handle(h); + drop_event_emitter_handle(h); } #[test] @@ -1273,7 +1550,7 @@ mod tests { } let arr = unsafe { js_event_emitter_listeners(h, event_name.as_raw() as *const _) }; assert!(!arr.is_null()); - drop_handle(h); + drop_event_emitter_handle(h); } #[test] @@ -1285,13 +1562,13 @@ mod tests { js_event_emitter_set_max_listeners(h, 42.0); } assert_eq!(unsafe { js_event_emitter_get_max_listeners(h) }, 42.0); - drop_handle(h); + drop_event_emitter_handle(h); } #[test] fn gc_mutable_scanner_rewrites_listener_and_pending_promise_roots() { let _guard = GcTestGuard::new(); - perry_ffi::gc_register_mutable_root_scanner_named("perry-ext-events", scan_events_roots); + ensure_gc_scanner_registered(); let listener = young_gc_root(); let promise = young_promise_root(); @@ -1312,18 +1589,18 @@ mod tests { abort_listener: 0, }], ); - let handle = register_handle(EventEmitterHandle { + let handle = register_event_emitter_handle(EventEmitterHandle { events, event_order: vec!["ready".to_string()], pending_once_promises, max_listeners: 10, + domain_handle: None, }); let _ = perry_runtime::gc::gc_collect_minor(); { - let emitter = get_handle::(handle) - .expect("emitter handle should remain live"); + let emitter = get_event_emitter_mut(handle).expect("emitter handle should remain live"); assert_rewritten( listener as usize, emitter.events["ready"][0].callback as usize, @@ -1333,6 +1610,6 @@ mod tests { emitter.pending_once_promises["ready"][0].promise as usize, ); } - drop_handle(handle); + drop_event_emitter_handle(handle); } } diff --git a/crates/perry-runtime/src/object/class_handles.rs b/crates/perry-runtime/src/object/class_handles.rs index 291328c07b..672695f460 100644 --- a/crates/perry-runtime/src/object/class_handles.rs +++ b/crates/perry-runtime/src/object/class_handles.rs @@ -70,6 +70,8 @@ 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 EventEmitterGetDomainFn = unsafe extern "C" fn(handle: i64) -> i64; +pub type EventEmitterSetDomainFn = 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 +98,8 @@ 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 EVENT_EMITTER_GET_DOMAIN_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); +static EVENT_EMITTER_SET_DOMAIN_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 +243,36 @@ 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 event_emitter_get_domain() -> Option { + let p = EVENT_EMITTER_GET_DOMAIN_PTR.load(Ordering::Acquire); + if p.is_null() { + None + } else { + Some(unsafe { std::mem::transmute::<*mut (), EventEmitterGetDomainFn>(p) }) + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_event_emitter_get_domain(f: EventEmitterGetDomainFn) { + EVENT_EMITTER_GET_DOMAIN_PTR.store(f as *mut (), Ordering::Release); +} + +#[inline] +pub fn event_emitter_set_domain() -> Option { + let p = EVENT_EMITTER_SET_DOMAIN_PTR.load(Ordering::Acquire); + if p.is_null() { + None + } else { + Some(unsafe { std::mem::transmute::<*mut (), EventEmitterSetDomainFn>(p) }) + } +} + +#[no_mangle] +pub unsafe extern "C" fn js_register_event_emitter_set_domain(f: EventEmitterSetDomainFn) { + EVENT_EMITTER_SET_DOMAIN_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..d8c64e5419 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -11,20 +11,22 @@ //! logic changes. pub use super::class_handles::{ - event_emitter_async_resource_handle_probe, event_emitter_handle_probe, event_emitter_on, + event_emitter_async_resource_handle_probe, event_emitter_get_domain, + event_emitter_handle_probe, event_emitter_on, event_emitter_set_domain, 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, + js_register_event_emitter_async_resource_handle_probe, js_register_event_emitter_get_domain, + js_register_event_emitter_handle_probe, js_register_event_emitter_on, + js_register_event_emitter_set_domain, 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, js_register_stream_handle_kind_probe, js_register_stream_handle_probe, net_socket_handle_probe, stream_handle_kind_probe, stream_handle_probe, EventEmitterAsyncResourceHandleProbeFn, - EventEmitterHandleProbeFn, EventEmitterOnFn, FetchHandleKindProbeFn, HandleMethodDispatchFn, - HandleOwnPropertyNamesDispatchFn, HandlePropertyDispatchFn, HandlePropertySetDispatchFn, - HandlePrototypeDispatchFn, NetSocketHandleProbeFn, StreamHandleKindProbeFn, - StreamHandleProbeFn, + EventEmitterGetDomainFn, EventEmitterHandleProbeFn, EventEmitterOnFn, EventEmitterSetDomainFn, + FetchHandleKindProbeFn, HandleMethodDispatchFn, HandleOwnPropertyNamesDispatchFn, + HandlePropertyDispatchFn, HandlePropertySetDispatchFn, HandlePrototypeDispatchFn, + NetSocketHandleProbeFn, StreamHandleKindProbeFn, StreamHandleProbeFn, }; use super::*; diff --git a/crates/perry-stdlib/src/domain.rs b/crates/perry-stdlib/src/domain.rs index 6addcfe63e..d603cd7228 100644 --- a/crates/perry-stdlib/src/domain.rs +++ b/crates/perry-stdlib/src/domain.rs @@ -12,40 +12,69 @@ 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 still be compiled into stdlib. Keep the local fast path for +// bundled EventEmitter handles, but fall through to runtime hooks so external +// EventEmitter implementations can participate in node:domain routing. +#[inline] +fn ee_is_event_emitter_handle_hook(handle: Handle) -> bool { + perry_runtime::object::event_emitter_handle_probe() + .is_some_and(|probe| unsafe { probe(handle) }) +} + +#[inline] +fn ee_get_domain_hook(handle: Handle) -> Handle { + perry_runtime::object::event_emitter_get_domain() + .map(|get_domain| unsafe { get_domain(handle) }) + .unwrap_or(0) +} + +#[inline] +fn ee_set_domain_hook(handle: Handle, domain: Handle) { + if let Some(set_domain) = perry_runtime::object::event_emitter_set_domain() { + let _ = unsafe { set_domain(handle, domain) }; + } +} + #[cfg(feature = "bundled-events")] #[inline] fn ee_is_event_emitter_handle(handle: Handle) -> bool { - crate::events::is_event_emitter_handle(handle) + crate::events::is_event_emitter_handle(handle) || ee_is_event_emitter_handle_hook(handle) } #[cfg(not(feature = "bundled-events"))] #[inline] -fn ee_is_event_emitter_handle(_handle: Handle) -> bool { - false +fn ee_is_event_emitter_handle(handle: Handle) -> bool { + ee_is_event_emitter_handle_hook(handle) } #[cfg(feature = "bundled-events")] #[inline] fn ee_get_domain(handle: Handle) -> Handle { - crate::events::js_event_emitter_get_domain(handle) + if crate::events::is_event_emitter_handle(handle) { + crate::events::js_event_emitter_get_domain(handle) + } else { + ee_get_domain_hook(handle) + } } #[cfg(not(feature = "bundled-events"))] #[inline] -fn ee_get_domain(_handle: Handle) -> Handle { - 0 +fn ee_get_domain(handle: Handle) -> Handle { + ee_get_domain_hook(handle) } #[cfg(feature = "bundled-events")] #[inline] fn ee_set_domain(handle: Handle, domain: Handle) { - let _ = crate::events::js_event_emitter_set_domain(handle, domain); + if crate::events::is_event_emitter_handle(handle) { + let _ = crate::events::js_event_emitter_set_domain(handle, domain); + } else { + ee_set_domain_hook(handle, domain); + } } #[cfg(not(feature = "bundled-events"))] #[inline] -fn ee_set_domain(_handle: Handle, _domain: Handle) {} +fn ee_set_domain(handle: Handle, domain: Handle) { + ee_set_domain_hook(handle, domain); +} const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; @@ -124,6 +153,8 @@ fn handle_from_value(value: f64) -> Handle { let bits = value.to_bits(); if (bits >> 48) == 0x7FFD { (bits & 0x0000_FFFF_FFFF_FFFF) as Handle + } else if value.is_finite() && value > 0.0 && value.fract() == 0.0 { + value as Handle } else { bits as Handle } @@ -250,7 +281,8 @@ unsafe fn emit_domain_event(handle: Handle, event: &str, args: &[f64]) -> bool { true } -pub unsafe fn js_domain_emit_error( +#[no_mangle] +pub unsafe extern "C" fn js_domain_emit_error( handle: Handle, error: f64, emitter: f64, diff --git a/crates/perry-stdlib/src/events.rs b/crates/perry-stdlib/src/events.rs index 227025e2cd..4b8f3a0eb2 100644 --- a/crates/perry-stdlib/src/events.rs +++ b/crates/perry-stdlib/src/events.rs @@ -1001,6 +1001,8 @@ pub unsafe extern "C" fn js_event_emitter_emit( }; let mut had_listeners = false; + let mut domain_error: Option<(Handle, f64)> = None; + let mut throw_error: Option = None; if let Some(emitter) = get_handle_mut::(handle) { // Snapshot the listener vec, then prune `once`-listeners from // the live vec before dispatching. This matches Node semantics: @@ -1032,34 +1034,40 @@ pub unsafe extern "C" fn js_event_emitter_emit( 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 _ = crate::domain::js_domain_emit_error( - domain, - first_arg, - js_nanbox_pointer(handle), - false, - ); - return TAG_FALSE_F64; + domain_error = Some((domain, first_arg)); + } else { + throw_error = Some(first_arg); } - perry_runtime::exception::js_throw(first_arg); } } - // Resolve any pending `events.once` Promises before dispatch. - drain_pending_once_promises(emitter, &event_name, args_ptr); + if domain_error.is_none() && throw_error.is_none() { + // Resolve any pending `events.once` Promises before dispatch. + drain_pending_once_promises(emitter, &event_name, args_ptr); - let capture_rejections = emitter.capture_rejections && event_name != "error"; - let async_resource_handle = emitter.async_resource_handle; - for l in snapshot { - if l.callback != 0 { - let result = - call_emitter_listener(handle, async_resource_handle, l.callback, &emitted_args); - if capture_rejections { - capture_listener_rejection(handle, result); + let capture_rejections = emitter.capture_rejections && event_name != "error"; + let async_handle = emitter.async_resource_handle; + for l in snapshot { + if l.callback != 0 { + let result = + call_emitter_listener(handle, async_handle, l.callback, &emitted_args); + if capture_rejections { + capture_listener_rejection(handle, result); + } } } } } + if let Some((domain, error)) = domain_error { + let _ = + crate::domain::js_domain_emit_error(domain, error, js_nanbox_pointer(handle), false); + return TAG_FALSE_F64; + } + if let Some(error) = throw_error { + perry_runtime::exception::js_throw(error); + } + bool_to_js(had_listeners) } @@ -1072,6 +1080,8 @@ pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) }; let mut had_listeners = false; + let mut domain_error: Option<(Handle, f64)> = None; + let mut throw_error: Option = None; if let Some(emitter) = get_handle_mut::(handle) { let snapshot: Vec = match emitter.events.get(&event_name) { Some(v) if !v.is_empty() => v.clone(), @@ -1088,47 +1098,48 @@ pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) } let empty_args = js_array_alloc(0); - let emitted_args: &[f64] = &[]; if event_name == "error" { + let error_value = f64::from_bits(TAG_UNDEFINED_F64_BITS); dispatch_error_monitor(emitter, None); let has_error_once = emitter .pending_once_promises .get("error") .is_some_and(|pending| !pending.is_empty()); - let rejected_once = reject_pending_once_promises_for_error( - emitter, - f64::from_bits(TAG_UNDEFINED_F64_BITS), - ); + 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 err = f64::from_bits(TAG_UNDEFINED_F64_BITS); - let _ = crate::domain::js_domain_emit_error( - domain, - err, - js_nanbox_pointer(handle), - false, - ); - return TAG_FALSE_F64; + domain_error = Some((domain, error_value)); + } else { + throw_error = Some(error_value); } - perry_runtime::exception::js_throw(f64::from_bits(TAG_UNDEFINED_F64_BITS)); } } - drain_pending_once_promises(emitter, &event_name, empty_args); + if domain_error.is_none() && throw_error.is_none() { + drain_pending_once_promises(emitter, &event_name, empty_args); - let capture_rejections = emitter.capture_rejections && event_name != "error"; - let async_resource_handle = emitter.async_resource_handle; - for l in snapshot { - if l.callback != 0 { - let result = - call_emitter_listener(handle, async_resource_handle, l.callback, emitted_args); - if capture_rejections { - capture_listener_rejection(handle, result); + let capture_rejections = emitter.capture_rejections && event_name != "error"; + let async_handle = emitter.async_resource_handle; + for l in snapshot { + if l.callback != 0 { + let result = call_emitter_listener(handle, async_handle, l.callback, &[]); + if capture_rejections { + capture_listener_rejection(handle, result); + } } } } } + if let Some((domain, error)) = domain_error { + let _ = + crate::domain::js_domain_emit_error(domain, error, js_nanbox_pointer(handle), false); + return TAG_FALSE_F64; + } + if let Some(error) = throw_error { + perry_runtime::exception::js_throw(error); + } + bool_to_js(had_listeners) } diff --git a/crates/perry/src/commands/compile/strip_dedup.rs b/crates/perry/src/commands/compile/strip_dedup.rs index 1c1416cde8..a5178a29a0 100644 --- a/crates/perry/src/commands/compile/strip_dedup.rs +++ b/crates/perry/src/commands/compile/strip_dedup.rs @@ -16,6 +16,27 @@ use super::{find_library, find_llvm_tool, find_stdlib_library}; const FORCE_EXCLUDE_SYMBOLS: &[&str] = &["js_stdlib_init_dispatch", "js_stdlib_process_pending"]; +const RUST_ALLOCATOR_SYMBOL_PARTS: &[&str] = &[ + "__rust_alloc", + "__rust_dealloc", + "__rust_realloc", + "__rust_alloc_zeroed", + "__rust_alloc_error_handler", + "__rust_no_alloc_shim_is_unstable", + "__rdl_alloc", + "__rdl_dealloc", + "__rdl_realloc", + "__rdl_alloc_zeroed", + "__rdl_alloc_error_handler", +]; + +fn force_localize_symbol(symbol: &str) -> bool { + FORCE_EXCLUDE_SYMBOLS.iter().any(|forced| symbol == *forced) + || RUST_ALLOCATOR_SYMBOL_PARTS + .iter() + .any(|part| symbol.contains(part)) +} + fn find_path_tool(name: &str) -> Option { let paths = std::env::var_os("PATH")?; std::env::split_paths(&paths) @@ -562,18 +583,24 @@ pub(super) fn strip_duplicate_objects_from_well_known_lib(lib_path: &PathBuf) -> let abs_staticlib = std::fs::canonicalize(lib_path)?; let symbols_by_member = collect_archive_symbols_by_member(&nm, &abs_staticlib) .ok_or_else(|| anyhow::anyhow!("failed to inspect archive symbols"))?; - let affected: std::collections::HashSet = symbols_by_member - .iter() - .filter(|(_, symbols)| { - symbols.iter().any(|s| { - FORCE_EXCLUDE_SYMBOLS + let forced_symbols_by_member: std::collections::BTreeMap> = + symbols_by_member + .iter() + .filter_map(|(member, symbols)| { + let mut forced_symbols: Vec = symbols .iter() - .any(|forced| s.as_str() == *forced) + .filter(|symbol| force_localize_symbol(symbol)) + .cloned() + .collect(); + if forced_symbols.is_empty() { + None + } else { + forced_symbols.sort(); + Some((member.clone(), forced_symbols)) + } }) - }) - .map(|(member, _)| member.clone()) - .collect(); - if affected.is_empty() { + .collect(); + if forced_symbols_by_member.is_empty() { return Ok(lib_path.clone()); } @@ -606,9 +633,9 @@ pub(super) fn strip_duplicate_objects_from_well_known_lib(lib_path: &PathBuf) -> return Err(anyhow::anyhow!("failed to extract {lib_name}: {stderr}")); } - for member in &affected { + for (member, symbols) in &forced_symbols_by_member { let member_path = extract_dir.join(member); - for symbol in FORCE_EXCLUDE_SYMBOLS { + for symbol in symbols { let out = Command::new(&objcopy) .arg("--localize-symbol") .arg(symbol) @@ -637,8 +664,8 @@ pub(super) fn strip_duplicate_objects_from_well_known_lib(lib_path: &PathBuf) -> } eprintln!( - "[strip-dedup] {lib_name}: localized stdlib dispatch stubs in {} member(s)", - affected.len() + "[strip-dedup] {lib_name}: localized wrapper-only globals in {} member(s)", + forced_symbols_by_member.len() ); let _ = std::fs::remove_dir_all(&extract_dir); Ok(trimmed_lib)