From 3cf4544803107feaf3450af42a0668bfeb0ba223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 25 Aug 2026 11:23:43 +0200 Subject: [PATCH 1/4] fix(async_hooks): address lifecycle review feedback --- .../perry-codegen/src/expr/this_super_call.rs | 157 +++++++++++----- crates/perry-codegen/src/ext_registry.rs | 6 + .../perry-codegen/src/lower_call/builtin.rs | 20 ++- .../lower_call/native_table/http_client.rs | 2 +- .../lower_call/native_table/http_server.rs | 2 +- .../src/runtime_decls/stdlib_ffi/net_http.rs | 2 + crates/perry-ext-events/src/lib.rs | 95 +++++++--- .../perry-ext-events/src/module_iterators.rs | 61 ++++++- crates/perry-ext-events/src/module_on.rs | 34 ++-- crates/perry-ext-events/src/tests.rs | 20 ++- .../src/client_request_surface.rs | 24 +++ crates/perry-ext-http/src/lib.rs | 45 +++++ .../src/server/handle_dispatch.rs | 10 ++ crates/perry-ext-http/src/server/request.rs | 23 +++ crates/perry-ext-http/src/server/server.rs | 10 +- .../src/server/server/deferred_events.rs | 87 +++++---- crates/perry-ext-net/src/dispatch.rs | 39 ++-- crates/perry-ext-net/src/gc_roots.rs | 5 + crates/perry-ext-net/src/lib.rs | 92 ++++++---- crates/perry-ext-net/src/lifecycle.rs | 146 ++++++++++++--- .../perry-ext-net/src/provider_lifecycle.rs | 5 +- crates/perry-ext-net/src/raw_bridge.rs | 2 +- crates/perry-ext-zlib/src/stream.rs | 31 +++- crates/perry-runtime/src/async_context.rs | 8 + crates/perry-runtime/src/async_hooks.rs | 86 +++------ .../src/async_hooks/provider_ffi.rs | 168 ++++++++++++++++++ .../src/child_process/reactor.rs | 22 ++- crates/perry-runtime/src/dns.rs | 70 ++++++-- crates/perry-runtime/src/dns/ffi.rs | 8 +- .../src/fs/dir_glob_watch/watch.rs | 8 +- crates/perry-runtime/src/gc/mod.rs | 30 +++- crates/perry-runtime/src/module_require.rs | 13 +- .../src/node_stream_constructors/builders.rs | 4 +- .../src/node_stream_constructors/pipeline.rs | 5 - .../perry-runtime/src/node_stream_dispatch.rs | 140 ++++++++++----- .../src/node_submodules/fs_promises.rs | 17 +- crates/perry-runtime/src/object/instanceof.rs | 4 +- .../native_module_dispatch/dispatch_a_c.rs | 9 +- .../perry-runtime/src/promise/assimilate.rs | 18 +- .../perry-runtime/src/promise/async_step.rs | 8 + .../perry-runtime/src/promise/microtasks.rs | 17 +- crates/perry-runtime/src/promise/then.rs | 34 +++- crates/perry-runtime/src/proxy.rs | 36 ++-- crates/perry-runtime/src/timer.rs | 13 +- .../perry-stdlib/src/async_local_storage.rs | 74 +++++--- .../src/common/dispatch/emitter_als.rs | 22 ++- .../perry-stdlib/src/common/dispatch_http.rs | 15 +- crates/perry-stdlib/src/tls/event_pump.rs | 115 ++++++++---- crates/perry-stdlib/src/webcrypto/digest.rs | 32 +++- crates/perry-stdlib/src/webcrypto/hmac.rs | 12 +- crates/perry-stdlib/src/webcrypto/util.rs | 5 - .../src/worker_threads/worker_pump.rs | 14 +- crates/perry-stdlib/src/zlib.rs | 10 ++ .../perry/src/commands/compile/build_cache.rs | 2 - scripts/raw_handle_debt_baseline.txt | 2 +- scripts/thread_local_cold_allowlist.json | 2 +- 56 files changed, 1445 insertions(+), 496 deletions(-) create mode 100644 crates/perry-runtime/src/async_hooks/provider_ffi.rs diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 42f62ef709..ad8b47a3a8 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -9,6 +9,7 @@ use perry_hir::Expr; use crate::lower_call::{bind_inline_constructor_params, restore_inline_constructor_scope}; use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::rooting::{self, Repr}; use crate::types::{DOUBLE, I1, I32, I64, PTR}; use super::{ @@ -257,6 +258,67 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Some(slot) => ctx.block().load(DOUBLE, &slot), None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), }; + let async_parent = ctx + .classes + .get(¤t_class_name) + .and_then(|class| class.extends_name.clone()); + if matches!( + async_parent.as_deref(), + Some("EventEmitterAsyncResource" | "AsyncLocalStorage" | "AsyncResource") + ) { + let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + let zero_idx = "0".to_string(); + let one_idx = "1".to_string(); + let first = + ctx.block() + .call(DOUBLE, "js_array_get_f64", &[(I64, &arr), (I32, &zero_idx)]); + let second = + ctx.block() + .call(DOUBLE, "js_array_get_f64", &[(I64, &arr), (I32, &one_idx)]); + rooting::with_rooted_group(ctx, 3, |ctx, group| { + let this_root = group.adopt_emitted(ctx, Repr::Boxed, &this_box, true); + let first_root = group.adopt_emitted(ctx, Repr::Boxed, &first, true); + let second_root = group.adopt_emitted(ctx, Repr::Boxed, &second, true); + let this_box = group.reread_emitted(ctx, this_root); + match async_parent.as_deref() { + Some("EventEmitterAsyncResource") => { + let options = group.reread_emitted(ctx, first_root); + lower_event_emitter_async_resource_subclass_init( + ctx, &this_box, &options, + ); + } + Some("AsyncLocalStorage") => { + ctx.block().call( + DOUBLE, + "js_async_local_storage_subclass_init", + &[(DOUBLE, &this_box)], + ); + } + Some("AsyncResource") => { + let type_value = group.reread_emitted(ctx, first_root); + let options = group.reread_emitted(ctx, second_root); + ctx.block().call( + DOUBLE, + "js_async_resource_subclass_init", + &[ + (DOUBLE, &this_box), + (DOUBLE, &type_value), + (DOUBLE, &options), + ], + ); + } + _ => unreachable!(), + } + bind_derived_this_after_super(ctx); + crate::lower_call::apply_field_initializers_recursive( + ctx, + ¤t_class_name, + crate::lower_call::FieldInitMode::SelfOnly, + )?; + Ok(undef.clone()) + })?; + return Ok(undef); + } // `class X extends Map | Set` with a spread super (`super(...args)`, // e.g. NestJS's `ModulesContainer`'s `super(...arguments)`) — install // the hidden collection backing from the (possibly spread) args @@ -828,27 +890,28 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); } if parent_name.as_str() == "EventEmitterAsyncResource" { - let mut lowered = Vec::with_capacity(super_args.len()); - for arg in super_args { - lowered.push(lower_expr(ctx, arg)?); - } - let options = lowered.first().cloned().unwrap_or_else(|| { - double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + let operands: Vec<_> = super_args.iter().collect(); + return rooting::with_operands_rooted(ctx, &operands, |ctx, lowered| { + let options = lowered.first().cloned().unwrap_or_else(|| { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + }); + let this_box = match ctx.this_stack.last().cloned() { + Some(slot) => ctx.block().load(DOUBLE, &slot), + None => { + double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) + } + }; + lower_event_emitter_async_resource_subclass_init( + ctx, &this_box, &options, + ); + bind_derived_this_after_super(ctx); + crate::lower_call::apply_field_initializers_recursive( + ctx, + ¤t_class_name, + crate::lower_call::FieldInitMode::SelfOnly, + )?; + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) }); - let this_box = match ctx.this_stack.last().cloned() { - Some(slot) => ctx.block().load(DOUBLE, &slot), - None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), - }; - lower_event_emitter_async_resource_subclass_init(ctx, &this_box, &options); - bind_derived_this_after_super(ctx); - let current_class_name = - ctx.class_stack.last().cloned().unwrap_or_default(); - crate::lower_call::apply_field_initializers_recursive( - ctx, - ¤t_class_name, - crate::lower_call::FieldInitMode::SelfOnly, - )?; - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); } if parent_name.as_str() == "AsyncLocalStorage" { for arg in super_args { @@ -875,34 +938,32 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } if parent_name.as_str() == "AsyncResource" { let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - let mut lowered = Vec::with_capacity(super_args.len()); - for arg in super_args { - lowered.push(lower_expr(ctx, arg)?); - } - let type_value = lowered.first().cloned().unwrap_or_else(|| undef.clone()); - let options = lowered.get(1).cloned().unwrap_or_else(|| undef.clone()); - let this_box = match ctx.this_stack.last().cloned() { - Some(slot) => ctx.block().load(DOUBLE, &slot), - None => undef, - }; - ctx.block().call( - DOUBLE, - "js_async_resource_subclass_init", - &[ - (DOUBLE, &this_box), - (DOUBLE, &type_value), - (DOUBLE, &options), - ], - ); - bind_derived_this_after_super(ctx); - let current_class_name = - ctx.class_stack.last().cloned().unwrap_or_default(); - crate::lower_call::apply_field_initializers_recursive( - ctx, - ¤t_class_name, - crate::lower_call::FieldInitMode::SelfOnly, - )?; - return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); + let operands: Vec<_> = super_args.iter().collect(); + return rooting::with_operands_rooted(ctx, &operands, |ctx, lowered| { + let type_value = + lowered.first().cloned().unwrap_or_else(|| undef.clone()); + let options = lowered.get(1).cloned().unwrap_or_else(|| undef.clone()); + let this_box = match ctx.this_stack.last().cloned() { + Some(slot) => ctx.block().load(DOUBLE, &slot), + None => undef.clone(), + }; + ctx.block().call( + DOUBLE, + "js_async_resource_subclass_init", + &[ + (DOUBLE, &this_box), + (DOUBLE, &type_value), + (DOUBLE, &options), + ], + ); + bind_derived_this_after_super(ctx); + crate::lower_call::apply_field_initializers_recursive( + ctx, + ¤t_class_name, + crate::lower_call::FieldInitMode::SelfOnly, + )?; + Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))) + }); } // `class X extends Request` / `extends Response`: // `super(input, init)` allocates the underlying native diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index cd1a27c466..f8994e1185 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -220,6 +220,7 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_https_request", OwnerKind::WellKnown("http")), ("js_https_get", OwnerKind::WellKnown("http")), ("js_http_on", OwnerKind::WellKnown("http")), + ("js_http_once", OwnerKind::WellKnown("http")), ("js_http_set_header", OwnerKind::WellKnown("http")), ("js_http_set_timeout", OwnerKind::WellKnown("http")), ("js_http_set_timeout_full", OwnerKind::WellKnown("http")), @@ -300,6 +301,7 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_node_http_server_ref", OwnerKind::WellKnown("http")), ("js_node_http_server_unref", OwnerKind::WellKnown("http")), ("js_node_http_im_on", OwnerKind::WellKnown("http")), + ("js_node_http_im_once", OwnerKind::WellKnown("http")), ("js_node_http_im_pause", OwnerKind::WellKnown("http")), ("js_node_http_im_resume", OwnerKind::WellKnown("http")), ("js_node_http_im_pause_self", OwnerKind::WellKnown("http")), @@ -549,6 +551,8 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_event_emitter_new", OwnerKind::WellKnown("events")), ("js_event_emitter_new_with_options", OwnerKind::WellKnown("events")), ("js_event_emitter_async_resource_new", OwnerKind::WellKnown("events")), + ("js_event_emitter_async_resource_call", OwnerKind::WellKnown("events")), + ("js_event_emitter_async_resource_subclass_init", OwnerKind::WellKnown("events")), ("js_event_emitter_async_resource_async_id", OwnerKind::WellKnown("events")), ("js_event_emitter_async_resource_trigger_async_id", OwnerKind::WellKnown("events")), ("js_event_emitter_async_resource_async_resource", OwnerKind::WellKnown("events")), @@ -1126,6 +1130,8 @@ mod tests { "js_event_emitter_set_max_listeners", "js_event_emitter_get_max_listeners", "js_event_emitter_domain_value", + "js_event_emitter_async_resource_call", + "js_event_emitter_async_resource_subclass_init", ] { assert_symbol_routes_to(symbol, OwnerKind::WellKnown("events")); } diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index 637ec3b3a2..6e3e29f1ba 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -145,9 +145,14 @@ pub(super) fn lower_builtin_new<'a>( // the native-module call table used by `dns.Resolver()`. Route it // to the same runtime constructor and preserve evaluation of any // superfluous arguments. - for arg in args { + let options_idx = adopt_optional_arg(ctx, args, 0, group)?; + for arg in args.iter().skip(1) { let _ = lower_expr(ctx, arg)?; } + let options = match options_idx { + Some(index) => group.reread(ctx, index)?, + None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), + }; let runtime = if import_src.is_some_and(|source| { source.strip_prefix("node:").unwrap_or(source) == "dns/promises" }) { @@ -157,7 +162,18 @@ pub(super) fn lower_builtin_new<'a>( }; ctx.pending_declares .push((runtime.to_string(), DOUBLE, vec![I64])); - Ok(Some(ctx.block().call(DOUBLE, runtime, &[(I64, "0")]))) + let zero = "0".to_string(); + let args_array = ctx.block().call(I64, "js_array_alloc", &[(I32, &zero)]); + let args_array = ctx.block().call( + I64, + "js_array_push_f64", + &[(I64, &args_array), (DOUBLE, &options)], + ); + Ok(Some(ctx.block().call( + DOUBLE, + runtime, + &[(I64, &args_array)], + ))) } "Utf8Stream" if import_src diff --git a/crates/perry-codegen/src/lower_call/native_table/http_client.rs b/crates/perry-codegen/src/lower_call/native_table/http_client.rs index 2da650f54b..eb43c085f1 100644 --- a/crates/perry-codegen/src/lower_call/native_table/http_client.rs +++ b/crates/perry-codegen/src/lower_call/native_table/http_client.rs @@ -141,7 +141,7 @@ pub(super) const HTTP_CLIENT_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "once", class_filter: Some("ClientRequest"), - runtime: "js_http_on", + runtime: "js_http_once", args: &[NA_STR, NA_PTR], ret: NR_PTR, }, diff --git a/crates/perry-codegen/src/lower_call/native_table/http_server.rs b/crates/perry-codegen/src/lower_call/native_table/http_server.rs index 38f960482d..dc044db30a 100644 --- a/crates/perry-codegen/src/lower_call/native_table/http_server.rs +++ b/crates/perry-codegen/src/lower_call/native_table/http_server.rs @@ -391,7 +391,7 @@ pub(super) const HTTP_SERVER_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "once", class_filter: Some("IncomingMessage"), - runtime: "js_node_http_im_on", + runtime: "js_node_http_im_once", args: &[NA_STR, NA_PTR], ret: NR_F64, }, diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs index 1b0f9ec8ce..4628dbdfec 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs @@ -112,6 +112,7 @@ pub(crate) fn declare_net_http(module: &mut LlModule) { module.declare_function("js_https_get_overload", I64, &[I64]); module.declare_function("js_https_request_overload", I64, &[I64]); module.declare_function("js_http_on", I64, &[I64, I64, I64]); + module.declare_function("js_http_once", I64, &[I64, I64, I64]); module.declare_function("js_http_request", I64, &[DOUBLE, I64]); module.declare_function("js_http_request_body", I64, &[I64]); module.declare_function("js_http_request_body_length", DOUBLE, &[I64]); @@ -222,6 +223,7 @@ pub(crate) fn declare_net_http(module: &mut LlModule) { module.declare_function("js_node_http_im_resume", VOID, &[I64]); module.declare_function("js_node_http_im_destroy", VOID, &[I64]); module.declare_function("js_node_http_im_on", DOUBLE, &[I64, I64, I64]); + module.declare_function("js_node_http_im_once", DOUBLE, &[I64, I64, I64]); module.declare_function("js_node_http_im_read", DOUBLE, &[I64]); module.declare_function("js_node_http_im_set_timeout", I64, &[I64, DOUBLE, I64]); // ServerResponse: diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index 3863c15c99..cfeb124c79 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -62,6 +62,7 @@ use module_iterators::{ events_on_queue_listener, events_on_state_new, events_on_state_set_target, events_once_abort_listener, events_once_event_target_listener, events_once_stream_reject_listener, events_once_stream_resolve_listener, + EVENTS_ON_EVENT_EMITTER, EVENTS_ON_EVENT_TARGET, EVENTS_ON_NET_HANDLE, EVENTS_ON_STREAM, }; const MIN_HEAP_POINTER: u64 = 0x1000; @@ -201,8 +202,11 @@ extern "C" { fn js_async_resource_emit_destroy(handle: i64) -> i64; fn js_async_resource_set_event_emitter(handle: i64, event_emitter: i64); fn js_event_emitter_async_resource_subclass_backing(receiver: i64) -> i64; - fn js_async_hooks_provider_enter(async_id: u64); - fn js_async_hooks_provider_leave(async_id: u64); + fn js_async_hooks_provider_run_catching( + async_id: u64, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, + ) -> f64; } /// #3072: validate an EventEmitter listener argument, returning the closure @@ -1291,16 +1295,47 @@ pub unsafe extern "C" fn js_event_emitter_emit( handle: Handle, event_bits: i64, args_ptr: *mut ArrayHeader, +) -> f64 { + if event_name_from_bits(event_bits).is_none() { + return f64::from_bits(0x7FFC_0000_0000_0003); + } + let async_id = event_emitter_async_id(handle); + if async_id == 0 { + return js_event_emitter_emit_impl(handle, event_bits, args_ptr); + } + let mut call = EventEmitterEmitCall { + handle, + event_bits, + args_ptr, + }; + js_async_hooks_provider_run_catching( + async_id, + event_emitter_emit_thunk, + &mut call as *mut EventEmitterEmitCall as *mut std::ffi::c_void, + ) +} + +struct EventEmitterEmitCall { + handle: Handle, + event_bits: i64, + args_ptr: *mut ArrayHeader, +} + +unsafe extern "C" fn event_emitter_emit_thunk(data: *mut std::ffi::c_void) -> f64 { + let call = &mut *(data as *mut EventEmitterEmitCall); + js_event_emitter_emit_impl(call.handle, call.event_bits, call.args_ptr) +} + +unsafe fn js_event_emitter_emit_impl( + handle: Handle, + 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) = event_name_from_bits(event_bits) else { return TAG_FALSE_F64; }; - let async_id = event_emitter_async_id(handle); - if async_id != 0 { - js_async_hooks_provider_enter(async_id); - } let mut had_listeners = false; let mut domain_error: Option<(Handle, f64)> = None; let mut throw_error: Option = None; @@ -1354,23 +1389,16 @@ pub unsafe extern "C" fn js_event_emitter_emit( } if let Some((domain, error)) = domain_error { let _ = js_domain_emit_error(domain, error, nanbox_pointer_bits(handle), false); - if async_id != 0 { - js_async_hooks_provider_leave(async_id); - } return TAG_FALSE_F64; } if let Some(error) = throw_error { js_throw(error); } - let result = if had_listeners { + if had_listeners { TAG_TRUE_F64 } else { TAG_FALSE_F64 - }; - if async_id != 0 { - js_async_hooks_provider_leave(async_id); } - result } /// `emitter.emit(eventName)` — no-args variant. @@ -1384,15 +1412,37 @@ 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_bits: i64) -> f64 { + if event_name_from_bits(event_bits).is_none() { + return f64::from_bits(0x7FFC_0000_0000_0003); + } + let async_id = event_emitter_async_id(handle); + if async_id == 0 { + return js_event_emitter_emit0_impl(handle, event_bits); + } + let mut call = EventEmitterEmit0Call { handle, event_bits }; + js_async_hooks_provider_run_catching( + async_id, + event_emitter_emit0_thunk, + &mut call as *mut EventEmitterEmit0Call as *mut std::ffi::c_void, + ) +} + +struct EventEmitterEmit0Call { + handle: Handle, + event_bits: i64, +} + +unsafe extern "C" fn event_emitter_emit0_thunk(data: *mut std::ffi::c_void) -> f64 { + let call = &mut *(data as *mut EventEmitterEmit0Call); + js_event_emitter_emit0_impl(call.handle, call.event_bits) +} + +unsafe fn js_event_emitter_emit0_impl(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) = event_name_from_bits(event_bits) else { return TAG_FALSE_F64; }; - let async_id = event_emitter_async_id(handle); - if async_id != 0 { - js_async_hooks_provider_enter(async_id); - } let mut had_listeners = false; let mut domain_error: Option<(Handle, f64)> = None; let mut throw_error: Option = None; @@ -1445,23 +1495,16 @@ pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) } if let Some((domain, error)) = domain_error { let _ = js_domain_emit_error(domain, error, nanbox_pointer_bits(handle), false); - if async_id != 0 { - js_async_hooks_provider_leave(async_id); - } return TAG_FALSE_F64; } if let Some(error) = throw_error { js_throw(error); } - let result = if had_listeners { + if had_listeners { TAG_TRUE_F64 } else { TAG_FALSE_F64 - }; - if async_id != 0 { - js_async_hooks_provider_leave(async_id); } - result } /// `emitter.removeListener(event, listener)`. Removes the most recently added diff --git a/crates/perry-ext-events/src/module_iterators.rs b/crates/perry-ext-events/src/module_iterators.rs index 24e38bb39d..e7584299df 100644 --- a/crates/perry-ext-events/src/module_iterators.rs +++ b/crates/perry-ext-events/src/module_iterators.rs @@ -141,11 +141,17 @@ const EVENTS_ON_DONE: u32 = 2; const EVENTS_ON_ABORT: u32 = 3; const EVENTS_ON_HANDLE: u32 = 4; const EVENTS_ON_LISTENER: u32 = 5; +const EVENTS_ON_EVENT_NAME: u32 = 6; +const EVENTS_ON_TARGET_KIND: u32 = 7; +pub(super) const EVENTS_ON_EVENT_EMITTER: u32 = 0; +pub(super) const EVENTS_ON_EVENT_TARGET: u32 = 1; +pub(super) const EVENTS_ON_NET_HANDLE: u32 = 2; +pub(super) const EVENTS_ON_STREAM: u32 = 3; const EVENTS_ON_ITER_SHAPE_ID: u32 = 0x7FFF_FF60; pub(super) unsafe fn events_on_state_new() -> *mut ArrayHeader { let scope = TransientRootScope::enter(); - let state = js_array_alloc(6); + let state = js_array_alloc(8); let state_root = scope.root_nanbox(nanbox_pointer_bits(state as i64)); let buffer = js_array_alloc(0); let buffer_root = scope.root_nanbox(nanbox_pointer_bits(buffer as i64)); @@ -158,6 +164,8 @@ pub(super) unsafe fn events_on_state_new() -> *mut ArrayHeader { let _ = js_array_push_f64(state_ptr(), undefined_value()); let _ = js_array_push_f64(state_ptr(), undefined_value()); let _ = js_array_push_f64(state_ptr(), undefined_value()); + let _ = js_array_push_f64(state_ptr(), undefined_value()); + let _ = js_array_push_f64(state_ptr(), undefined_value()); state_ptr() } @@ -172,15 +180,19 @@ unsafe fn events_on_state_set(state: *mut ArrayHeader, index: u32, value: f64) { pub(super) unsafe fn events_on_state_set_target( state: *mut ArrayHeader, - handle: Handle, + target: f64, listener: *mut RawClosureHeader, + event_name: f64, + target_kind: u32, ) { - events_on_state_set(state, EVENTS_ON_HANDLE, handle as f64); + events_on_state_set(state, EVENTS_ON_HANDLE, target); events_on_state_set( state, EVENTS_ON_LISTENER, nanbox_pointer_bits(listener as i64), ); + events_on_state_set(state, EVENTS_ON_EVENT_NAME, event_name); + events_on_state_set(state, EVENTS_ON_TARGET_KIND, target_kind as f64); } fn events_on_iter_result(value: f64, done: bool) -> f64 { @@ -246,6 +258,12 @@ pub(super) extern "C" fn events_on_queue_listener( if !state.is_null() { let scope = TransientRootScope::enter(); let state_root = scope.root_nanbox(nanbox_pointer_bits(state as i64)); + let current_state = (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader; + if f64::from_bits(js_array_get(current_state, EVENTS_ON_DONE).bits()).to_bits() + == TAG_TRUE_F64_BITS + { + return f64::from_bits(TAG_UNDEFINED_F64_BITS); + } let mut args = js_array_alloc(0); args = js_array_push_f64(args, arg0); let args_root = scope.root_nanbox(nanbox_pointer_bits(args as i64)); @@ -307,11 +325,40 @@ extern "C" fn events_on_return(closure: *const RawClosureHeader) -> f64 { return events_on_resolved(undefined_value(), true); } events_on_state_set(state, EVENTS_ON_DONE, f64::from_bits(TAG_TRUE_F64_BITS)); - let handle = f64::from_bits(js_array_get(state, EVENTS_ON_HANDLE).bits()); + let target = f64::from_bits(js_array_get(state, EVENTS_ON_HANDLE).bits()); let listener = f64::from_bits(js_array_get(state, EVENTS_ON_LISTENER).bits()); - if handle.is_finite() && listener.to_bits() != TAG_UNDEFINED_F64_BITS { - if let Some(emitter) = get_event_emitter_mut(handle as Handle) { - remove_listener_by_callback(emitter, (listener.to_bits() & POINTER_MASK) as i64); + let event_name = f64::from_bits(js_array_get(state, EVENTS_ON_EVENT_NAME).bits()); + let target_kind = f64::from_bits(js_array_get(state, EVENTS_ON_TARGET_KIND).bits()) as u32; + if listener.to_bits() != TAG_UNDEFINED_F64_BITS { + let listener_ptr = (listener.to_bits() & POINTER_MASK) as i64; + match target_kind { + EVENTS_ON_EVENT_EMITTER => { + if let Some(emitter) = get_event_emitter_mut(target as Handle) { + remove_listener_by_callback(emitter, listener_ptr); + } + } + EVENTS_ON_EVENT_TARGET => { + let target_ptr = (target.to_bits() & POINTER_MASK) as *mut u8; + let event_ptr = (event_name.to_bits() & POINTER_MASK) as *const StringHeader; + if !target_ptr.is_null() && !event_ptr.is_null() { + js_event_target_remove_event_listener(target_ptr, event_ptr, listener_ptr); + } + } + EVENTS_ON_NET_HANDLE => { + let _ = call_net_socket_method( + target as Handle, + "removeListener", + &[event_name, listener], + ); + } + EVENTS_ON_STREAM => { + let _ = js_node_stream_method_remove_listener( + target as Handle, + event_name, + listener, + ); + } + _ => {} } } events_on_finish_pending(state, None); diff --git a/crates/perry-ext-events/src/module_on.rs b/crates/perry-ext-events/src/module_on.rs index c6b7ff789e..a583696c80 100644 --- a/crates/perry-ext-events/src/module_on.rs +++ b/crates/perry-ext-events/src/module_on.rs @@ -8,8 +8,12 @@ pub unsafe extern "C" fn js_events_on( ) -> *mut ArrayHeader { ensure_gc_scanner_registered(); let root_scope = TransientRootScope::enter(); - let target = - event_helper_target(target_value).unwrap_or_else(|| throw_invalid_emitter(target_value)); + let target_root = root_scope.root_nanbox(target_value); + let event_name_root = root_scope.root_nanbox(f64::from_bits(nanbox_string_bits( + string_header_ptr_from_arg(event_name_ptr) as *mut StringHeader, + ))); + let _ = event_helper_target(target_root.get()) + .unwrap_or_else(|| throw_invalid_emitter(target_root.get())); let queue = js_array_alloc(0); let queue_root = root_scope.root_nanbox(nanbox_pointer_bits(queue as i64)); let state = events_on_state_new(); @@ -18,10 +22,10 @@ pub unsafe extern "C" fn js_events_on( (queue_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader, (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader, ); - let Some(event_name) = event_name_from_bits(event_name_ptr as i64) else { + let Some(event_name) = event_name_from_bits(event_name_root.get().to_bits() as i64) else { return (queue_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader; }; - let event_name_ptr = string_header_ptr_from_arg(event_name_ptr); + let event_name_ptr = (event_name_root.get().to_bits() & POINTER_MASK) as *const StringHeader; let signal = options_signal_or_throw(options); if signal.is_some_and(signal_is_aborted) { js_throw(js_abort_error_value()); @@ -33,26 +37,32 @@ pub unsafe extern "C" fn js_events_on( (state_root.get().to_bits() & POINTER_MASK) as i64, ); let listener_root = root_scope.root_addr(listener as i64); - let handle = match target { + let target = event_helper_target(target_root.get()) + .unwrap_or_else(|| throw_invalid_emitter(target_root.get())); + let (handle, cleanup_target, cleanup_kind) = match target { EventHelperTarget::EventEmitter(handle) => { if let Some(emitter) = get_event_emitter_mut(handle) { emitter.add_listener(handle, &event_name, listener_root.get(), false, false); } - handle + (handle, handle as f64, EVENTS_ON_EVENT_EMITTER) } EventHelperTarget::EventTarget(target) => { if !event_name_ptr.is_null() { js_event_target_add_event_listener(target, event_name_ptr, listener_root.get()); } - target as Handle + ( + target as Handle, + nanbox_pointer_bits(target as i64), + EVENTS_ON_EVENT_TARGET, + ) } EventHelperTarget::NetSocket(handle) | EventHelperTarget::NativeHandle(handle) => { if !event_name_ptr.is_null() { let event = f64::from_bits(nanbox_string_bits(event_name_ptr as *mut StringHeader)); - let listener_value = nanbox_pointer_bits(listener as i64); + let listener_value = nanbox_pointer_bits(listener_root.get()); let _ = call_net_socket_method(handle, "on", &[event, listener_value]); } - handle + (handle, handle as f64, EVENTS_ON_NET_HANDLE) } EventHelperTarget::Stream(handle) => { if !event_name_ptr.is_null() { @@ -60,13 +70,15 @@ pub unsafe extern "C" fn js_events_on( let listener_value = nanbox_pointer_bits(listener_root.get()); let _ = js_node_stream_method_on(handle, event, listener_value); } - handle + (handle, handle as f64, EVENTS_ON_STREAM) } }; events_on_state_set_target( (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader, - handle, + cleanup_target, listener_root.get() as *mut RawClosureHeader, + event_name_root.get(), + cleanup_kind, ); if let Some(close) = get_object_property(options, b"close") { if js_array_is_array(close).to_bits() == TAG_TRUE_F64_BITS { diff --git a/crates/perry-ext-events/src/tests.rs b/crates/perry-ext-events/src/tests.rs index 44f1767e9a..d65797ac45 100644 --- a/crates/perry-ext-events/src/tests.rs +++ b/crates/perry-ext-events/src/tests.rs @@ -7,6 +7,7 @@ static GC_TEST_LOCK: Mutex<()> = Mutex::new(()); struct GcTestGuard { frame: u64, + previous_force_evacuation: i32, _lock: MutexGuard<'static, ()>, } @@ -16,15 +17,17 @@ impl GcTestGuard { .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); // This test asserts that mutable roots are rewritten, which is only - // observable when the collector moves its survivors. Force evacuation - // for the serialized GC-test window; the policy may otherwise choose - // a valid non-moving collection under unit-test pressure. - // - // SAFETY: `GC_TEST_LOCK` is held for the guard's whole lifetime. - unsafe { std::env::set_var("PERRY_GC_FORCE_EVACUATE", "1") }; + // observable when the collector moves its survivors. Use the + // runtime's thread-local override so unrelated test threads never + // observe a process-wide environment mutation. + let previous_force_evacuation = perry_runtime::gc::js_gc_force_evacuation_test_override(1); perry_runtime::gc::js_gc_write_barriers_emitted(1); let frame = perry_runtime::gc::js_shadow_frame_push(0); - Self { frame, _lock: lock } + Self { + frame, + previous_force_evacuation, + _lock: lock, + } } } @@ -32,8 +35,7 @@ impl Drop for GcTestGuard { fn drop(&mut self) { perry_runtime::gc::js_shadow_frame_pop(self.frame); perry_runtime::gc::js_gc_write_barriers_emitted(0); - // SAFETY: still under `GC_TEST_LOCK` (dropped after this body). - unsafe { std::env::remove_var("PERRY_GC_FORCE_EVACUATE") }; + perry_runtime::gc::js_gc_force_evacuation_test_override(self.previous_force_evacuation); } } diff --git a/crates/perry-ext-http/src/client_request_surface.rs b/crates/perry-ext-http/src/client_request_surface.rs index db87e79fbf..faad8fe08e 100644 --- a/crates/perry-ext-http/src/client_request_surface.rs +++ b/crates/perry-ext-http/src/client_request_surface.rs @@ -108,6 +108,30 @@ extern "C" fn client_once_wrapper(closure: *const RawClosureHeader, rest: f64) - listeners.remove(position); true }) + .or_else(|| { + with_handle_mut::(handle, |response| { + let Some(listeners) = response.listeners.get_mut(&event) else { + return false; + }; + let Some(position) = listeners.iter().rposition(|entry| *entry == wrapper) else { + return false; + }; + listeners.remove(position); + true + }) + }) + .or_else(|| { + with_handle_mut::(handle, |request| { + let Some(listeners) = request.listeners.get_mut(&event) else { + return false; + }; + let Some(position) = listeners.iter().rposition(|entry| *entry == wrapper) else { + return false; + }; + listeners.remove(position); + true + }) + }) .unwrap_or(false); if !removed || callback == 0 { return undefined_value(); diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index c020357abb..1e357215ab 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -756,6 +756,13 @@ extern "C" { fn js_async_hooks_provider_enter(async_id: u64); fn js_async_hooks_provider_leave(async_id: u64); fn js_async_hooks_provider_destroy(async_id: u64); + fn js_async_hooks_provider_run_catching_with_this( + async_id: u64, + this_value: f64, + destroy_after: i32, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, + ) -> f64; } fn pending_request_handle(event: &PendingHttpEvent) -> Handle { @@ -1780,6 +1787,44 @@ pub unsafe extern "C" fn js_http_on( http_on_impl(handle, event_ptr, callback) } +/// `req.once(event, cb)` / client `res.once(event, cb)` — register a wrapper +/// that removes itself before invoking the original callback. +#[no_mangle] +pub unsafe extern "C" fn js_http_once( + handle: Handle, + event_ptr: *const StringHeader, + callback: i64, +) -> Handle { + ensure_gc_scanner_registered(); + let Some(event) = read_str(event_ptr) else { + return handle; + }; + if callback == 0 { + return handle; + } + let wrapper = + client_request_surface::create_client_once_wrapper(handle, &event, callback, false); + let mut matched = false; + with_handle_mut::(handle, |request| { + request + .listeners + .entry(event.clone()) + .or_default() + .push(ClientEventListener { + callback, + raw_wrapper: wrapper, + once: true, + }); + matched = true; + }); + if !matched { + with_handle_mut::(handle, |response| { + response.listeners.entry(event).or_default().push(wrapper); + }); + } + handle +} + unsafe fn http_on_impl(handle: Handle, event_ptr: *const StringHeader, callback: i64) -> Handle { ensure_gc_scanner_registered(); let event = match read_str(event_ptr) { diff --git a/crates/perry-ext-http/src/server/handle_dispatch.rs b/crates/perry-ext-http/src/server/handle_dispatch.rs index 398677b020..86ad3ea08b 100644 --- a/crates/perry-ext-http/src/server/handle_dispatch.rs +++ b/crates/perry-ext-http/src/server/handle_dispatch.rs @@ -126,6 +126,8 @@ extern "C" { fn js_node_http_im_resume(handle: i64); fn js_node_http_im_destroy(handle: i64); fn js_node_http_im_on(handle: i64, event_name_ptr: *const StringHeader, callback: i64) -> f64; + fn js_node_http_im_once(handle: i64, event_name_ptr: *const StringHeader, callback: i64) + -> f64; fn js_node_http_im_set_encoding(handle: i64, encoding_ptr: *const StringHeader) -> i64; fn js_node_http_im_set_timeout(handle: i64, msecs: f64, callback: i64) -> i64; fn js_node_http_im_read(handle: i64) -> f64; @@ -372,6 +374,14 @@ pub unsafe extern "C" fn js_ext_http_server_dispatch_method( } self_ref } + "once" if args.len() >= 2 => { + let event_ptr = string_arg(args[0]); + if event_ptr.is_null() { + return self_ref; + } + js_node_http_im_once(handle, event_ptr, closure_arg(Some(args[1]))); + self_ref + } "once" if args.len() >= 2 => { let event = read_string_header(string_arg(args[0]) as *mut StringHeader).unwrap_or_default(); diff --git a/crates/perry-ext-http/src/server/request.rs b/crates/perry-ext-http/src/server/request.rs index fbb6e8af5a..7d675a11bc 100644 --- a/crates/perry-ext-http/src/server/request.rs +++ b/crates/perry-ext-http/src/server/request.rs @@ -704,6 +704,29 @@ pub unsafe extern "C" fn js_node_http_im_on( f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)) } +/// `IncomingMessage#once` for both server requests and client responses. +#[no_mangle] +pub unsafe extern "C" fn js_node_http_im_once( + handle: i64, + event_name_ptr: *const StringHeader, + callback: i64, +) -> f64 { + if get_handle_mut::(handle).is_none() { + extern "C" { + fn js_http_once(handle: i64, event_ptr: *const StringHeader, callback: i64) -> i64; + } + let _ = js_http_once(handle, event_name_ptr, callback); + return f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)); + } + let event = read_string_header(event_name_ptr as *mut _).unwrap_or_default(); + if event.is_empty() || callback == 0 { + return f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)); + } + let wrapper = + crate::client_request_surface::create_client_once_wrapper(handle, &event, callback, false); + js_node_http_im_on(handle, event_name_ptr, wrapper) +} + /// `req.setEncoding(encoding)` — switch future `'data'` events from Buffer /// chunks to decoded string chunks. Returns the receiver for chaining. #[no_mangle] diff --git a/crates/perry-ext-http/src/server/server.rs b/crates/perry-ext-http/src/server/server.rs index 16af91d477..6cb0a0212b 100644 --- a/crates/perry-ext-http/src/server/server.rs +++ b/crates/perry-ext-http/src/server/server.rs @@ -776,9 +776,6 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr .host .unwrap_or_else(|| extract_host(opts_f64, "0.0.0.0")); let callback = parsed.callback; - let server_async_id = - crate::js_async_hooks_provider_init(b"TCPSERVERWRAP".as_ptr(), b"TCPSERVERWRAP".len()); - let (request_tx, request_rx) = mpsc::channel::(1024); let (upgrade_tx, upgrade_rx) = mpsc::channel::(256); let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); @@ -941,9 +938,16 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr // `server`, so `server.address()` inside the callback threw // "Cannot read properties of undefined". The pump fires both with // `this` bound to the server (#2132), via `drain_deferred_listen_events`. + // Initialize the provider only after every synchronous setup step has + // succeeded. Failure returns above therefore cannot leak a resource with + // no matching destroy edge. + let server_async_id = + crate::js_async_hooks_provider_init(b"TCPSERVERWRAP".as_ptr(), b"TCPSERVERWRAP".len()); if let Some(s) = get_handle_mut::(server_handle) { s.async_id = server_async_id; queue_deferred_listening_emit(s, callback); + } else { + crate::js_async_hooks_provider_destroy(server_async_id); } // Closes #604 — `listen()` is now non-blocking. The accept loop is diff --git a/crates/perry-ext-http/src/server/server/deferred_events.rs b/crates/perry-ext-http/src/server/server/deferred_events.rs index 075b57b59d..0d1cba9b24 100644 --- a/crates/perry-ext-http/src/server/server/deferred_events.rs +++ b/crates/perry-ext-http/src/server/server/deferred_events.rs @@ -2,6 +2,29 @@ use super::*; +struct DeferredCallbacksCall { + callbacks: *const perry_ffi::TransientRootedAddr, + len: usize, +} + +unsafe extern "C" fn call_deferred_callbacks(data: *mut std::ffi::c_void) -> f64 { + let call = &*(data as *const DeferredCallbacksCall); + let callbacks = std::slice::from_raw_parts(call.callbacks, call.len); + let mut fired = 0; + for callback in callbacks { + let callback = callback.get(); + if callback == 0 { + continue; + } + let closure = JsClosure::from_raw(callback as *const RawClosureHeader); + if !closure.is_null() { + let _ = closure.call0(); + fired += 1; + } + } + fired as f64 +} + /// #4903 — record a pending `'listening'` emit on a server (http / https / /// http2 all share the `HttpServer` base). Node registers the /// `listen(port, cb)` callback as a *once* `'listening'` listener inside @@ -68,32 +91,23 @@ where } None => return 0, }; - if async_id != 0 { - unsafe { crate::js_async_hooks_provider_enter(async_id) }; - } let this_val = handle_to_pointer_f64(server_handle); - let mut fired = 0i32; // #8082: the drained snapshot crosses each callback — root it. let scope = perry_ffi::TransientRootScope::enter(); let rooted = scope.root_addrs(&cbs); - for cb in &rooted { - let addr = cb.get(); - if addr == 0 { - continue; - } - let raw = addr as *const RawClosureHeader; - let closure = unsafe { JsClosure::from_raw(raw) }; - if !closure.is_null() { - with_implicit_this(this_val, || { - let _ = unsafe { closure.call0() }; - }); - fired += 1; - } - } - if async_id != 0 { - unsafe { crate::js_async_hooks_provider_leave(async_id) }; + let mut call = DeferredCallbacksCall { + callbacks: rooted.as_ptr(), + len: rooted.len(), + }; + unsafe { + crate::js_async_hooks_provider_run_catching_with_this( + async_id, + this_val, + 0, + call_deferred_callbacks, + &mut call as *mut DeferredCallbacksCall as *mut std::ffi::c_void, + ) as i32 } - fired } pub(crate) fn drain_deferred_close_for(server_handle: i64, base_of: F) -> i32 @@ -101,7 +115,7 @@ where T: Send + Sync + 'static, F: FnOnce(&mut T) -> &mut HttpServer, { - let callbacks = match get_handle_mut::(server_handle) { + let (callbacks, async_id) = match get_handle_mut::(server_handle) { Some(server) => { let base = base_of(server); if !std::mem::take(&mut base.pending_close_emit) { @@ -116,28 +130,27 @@ where } } } - callbacks + let async_id = std::mem::take(&mut base.async_id); + (callbacks, async_id) } None => return 0, }; let this_value = handle_to_pointer_f64(server_handle); let scope = perry_ffi::TransientRootScope::enter(); let callbacks = scope.root_addrs(&callbacks); - let mut fired = 0; - for callback in &callbacks { - let callback = callback.get(); - if callback == 0 { - continue; - } - let closure = unsafe { JsClosure::from_raw(callback as *const RawClosureHeader) }; - if !closure.is_null() { - with_implicit_this(this_value, || unsafe { - let _ = closure.call0(); - }); - fired += 1; - } + let mut call = DeferredCallbacksCall { + callbacks: callbacks.as_ptr(), + len: callbacks.len(), + }; + unsafe { + crate::js_async_hooks_provider_run_catching_with_this( + async_id, + this_value, + 1, + call_deferred_callbacks, + &mut call as *mut DeferredCallbacksCall as *mut std::ffi::c_void, + ) as i32 } - fired } pub(super) fn server_is_active(s: &HttpServer) -> bool { // #5011 — an `unref()`ed server no longer keeps the event loop alive diff --git a/crates/perry-ext-net/src/dispatch.rs b/crates/perry-ext-net/src/dispatch.rs index 8ecfd7b829..6789e01d8d 100644 --- a/crates/perry-ext-net/src/dispatch.rs +++ b/crates/perry-ext-net/src/dispatch.rs @@ -204,36 +204,21 @@ unsafe fn socket_method(handle: i64, method: &str, args: &[f64]) -> Option let result = match method { "write" if !args.is_empty() => { - // #5021 — call the DISTINCT, twin-free symbol directly so the - // write reaches ext-net's registry regardless of link order. - crate::js_ext_net_socket_write(handle, args[0].to_bits() as i64); - if let Some(callback) = args.iter().copied().skip(1).find(|value| { - extern "C" { - fn js_value_is_closure(value_bits: i64) -> i32; - } - js_value_is_closure(value.to_bits() as i64) != 0 - }) { - let raw = unbox_to_i64(callback) as *const RawClosureHeader; - if !raw.is_null() { - let _ = JsClosure::from_raw(raw).call0(); - } - } + crate::js_ext_net_socket_write3( + handle, + args[0], + args.get(1).copied().unwrap_or_else(undefined), + args.get(2).copied().unwrap_or_else(undefined), + ); undefined() } "end" => { - let chunk = args.first().copied().unwrap_or_else(undefined); - crate::js_ext_net_socket_end(handle, chunk.to_bits() as i64); - if let Some(callback) = args.iter().copied().find(|value| { - extern "C" { - fn js_value_is_closure(value_bits: i64) -> i32; - } - js_value_is_closure(value.to_bits() as i64) != 0 - }) { - let raw = unbox_to_i64(callback) as *const RawClosureHeader; - if !raw.is_null() { - let _ = JsClosure::from_raw(raw).call0(); - } - } + crate::js_ext_net_socket_end3( + handle, + args.first().copied().unwrap_or_else(undefined), + args.get(1).copied().unwrap_or_else(undefined), + args.get(2).copied().unwrap_or_else(undefined), + ); undefined() } "emit" if !args.is_empty() => { diff --git a/crates/perry-ext-net/src/gc_roots.rs b/crates/perry-ext-net/src/gc_roots.rs index df109d4016..35b7524cc6 100644 --- a/crates/perry-ext-net/src/gc_roots.rs +++ b/crates/perry-ext-net/src/gc_roots.rs @@ -60,6 +60,11 @@ pub(crate) fn scan_net_roots(visitor: &mut GcRootVisitor<'_>) { } } } + if let Ok(mut completions) = crate::lifecycle::socket_completions().lock() { + for (_, callback) in completions.values_mut() { + visitor.visit_i64_slot(callback); + } + } // #8259 — the pump's in-flight dispatch frames (snapshotted callbacks + // parked payloads), which the table walks above cannot see. dispatch_custody::scan(visitor); diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 4594c3459b..98ee742735 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -36,7 +36,7 @@ use bytes::{BufMut, Bytes}; use perry_ffi::{ alloc_buffer, alloc_string, gc_register_mutable_root_scanner_named, GcRootVisitor, JsClosure, - JsPromise, JsValue, RawClosureHeader, StringHeader, + JsPromise, JsValue, RawClosureHeader, StringHeader, TransientRootScope, }; use std::collections::HashMap; use std::net::SocketAddr; @@ -311,8 +311,8 @@ impl SocketState { } pub(crate) enum SocketCommand { - Write(Vec), - End, + Write(Vec, u64), + End(u64), Destroy, /// `socket.setNoDelay(enable)` — applies `TCP_NODELAY` to the live socket. /// Carried as a command (rather than a flag on `SocketState`) because the @@ -350,22 +350,16 @@ enum PendingNetEvent { /// so the path from the receive buffer to the main-thread drain handler /// (which only borrows it as `&[u8]`) stays alloc-free per read. Data(i64, Bytes), - /// Issue #1852 — peer half-closed (FIN received, `read()` returned 0). - /// Node fires `'end'` on the readable side *before* `'close'`; lots of - /// net tests block on `socket.on('end', …)` to learn the peer is done, - /// so without this the connection lifecycle never completes and the - /// test hangs. + /// Peer half-closed (FIN received); public readable-side `end` event. End(i64), - /// Completion of the writable-side shutdown requested by `socket.end()`. - /// This is distinct from `End`, which is the peer's readable-side FIN and + /// Writable-side shutdown requested by `socket.end()`, distinct from FIN; /// fires the public `end` event. - ShutdownComplete(i64), + WriteComplete(i64, u64, Option), + ShutdownComplete(i64, u64, Option), Close(i64), Error(i64, String), AbortError(i64), - /// Issue #1123 followup — accept-loop on a `net.Server` produced - /// a new client socket. Fires the server's `'connection'` - /// listeners with the new socket handle. + /// Accept-loop produced a socket for the server's `connection` listeners. /// `.0` = server id (for listener lookup) /// `.1` = socket id (passed to listeners as the arg) /// `.2` = loopback client callback has crossed a pump boundary @@ -657,10 +651,9 @@ pub unsafe extern "C" fn js_ext_net_create_server( #[no_mangle] pub unsafe extern "C" fn js_net_server_listen(handle: i64, port: f64, arg2: f64, arg3: f64) { ensure_gc_scanner_registered(); - let callback_i64 = match js_net_callback_ptr(arg3) { - 0 => js_net_callback_ptr(arg2), - cb => cb, - }; + let roots = TransientRootScope::enter(); + let arg2 = roots.root_nanbox(arg2); + let arg3 = roots.root_nanbox(arg3); let path = ipc::string_value(port) .or_else(|| is_nanboxed_pointer(port).then(|| get_object_string_field(port, "path"))?); let (port_u16, host) = if path.is_some() { @@ -676,11 +669,15 @@ pub unsafe extern "C" fn js_net_server_listen(handle: i64, port: f64, arg2: f64, // #2013: a numeric `port` must be an integer in [0, 65536); Node throws // RangeError [ERR_SOCKET_BAD_PORT] otherwise. js_net_validate_listen_port(port); - let host = string_from_header_i64(js_get_string_pointer_unified(arg2)) + let host = string_from_header_i64(js_get_string_pointer_unified(arg2.get())) .unwrap_or_else(|| "0.0.0.0".to_string()); (port as u16, host) }; let server_async_id = init_provider(b"TCPSERVERWRAP"); + let callback_i64 = match js_net_callback_ptr(arg3.get()) { + 0 => js_net_callback_ptr(arg2.get()), + cb => cb, + }; let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); @@ -1240,16 +1237,35 @@ pub(crate) async fn run_socket_task( rx.recv().await }; match command { - Some(SocketCommand::Write(bytes)) => { + Some(SocketCommand::Write(bytes, completion)) => { if let Err(e) = t.write_all(&bytes).await { - push_event(PendingNetEvent::Error(id, format!("{}", e))); + let msg = format!("{}", e); + if completion != 0 { + push_event(PendingNetEvent::WriteComplete( + id, + completion, + Some(msg.clone()), + )); + } + push_event(PendingNetEvent::Error(id, msg)); break; } + if completion != 0 { + push_event(PendingNetEvent::WriteComplete( + id, + completion, + None, + )); + } } - Some(SocketCommand::End) => { - let _ = t.shutdown().await; + Some(SocketCommand::End(completion)) => { + let error = t.shutdown().await.err().map(|e| e.to_string()); writable_ended = true; - push_event(PendingNetEvent::ShutdownComplete(id)); + push_event(PendingNetEvent::ShutdownComplete( + id, + completion, + error, + )); } Some(SocketCommand::SetNoDelay(enable)) => { let _ = t.set_nodelay(enable); @@ -1271,7 +1287,7 @@ pub(crate) async fn run_socket_task( } if !writable_ended { let _ = t.shutdown().await; - push_event(PendingNetEvent::ShutdownComplete(id)); + push_event(PendingNetEvent::ShutdownComplete(id, 0, None)); } push_event(PendingNetEvent::Close(id)); mark_closed(id); @@ -1324,9 +1340,16 @@ pub(crate) async fn run_socket_task( drop(window); buffer_pool::checkin(buf); match cmd { - Some(SocketCommand::Write(bytes)) => { + Some(SocketCommand::Write(bytes, completion)) => { if let Err(e) = t.write_all(&bytes).await { let msg = format!("{}", e); + if completion != 0 { + push_event(PendingNetEvent::WriteComplete( + id, + completion, + Some(msg.clone()), + )); + } if !raw_bridge::mark_terminal(id, Some(msg.clone())) { push_event(PendingNetEvent::Error(id, msg)); push_event(PendingNetEvent::Close(id)); @@ -1334,11 +1357,14 @@ pub(crate) async fn run_socket_task( mark_closed(id); break; } + if completion != 0 { + push_event(PendingNetEvent::WriteComplete(id, completion, None)); + } } - Some(SocketCommand::End) => { - let _ = t.shutdown().await; + Some(SocketCommand::End(completion)) => { + let error = t.shutdown().await.err().map(|e| e.to_string()); writable_ended = true; - push_event(PendingNetEvent::ShutdownComplete(id)); + push_event(PendingNetEvent::ShutdownComplete(id, completion, error)); } Some(SocketCommand::SetNoDelay(enable)) => { // Best-effort, matching Node: a failed setsockopt (e.g. @@ -1618,7 +1644,7 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { .ok() .and_then(|sockets| sockets.get(id).map(|socket| vec![socket.connect_async_id])) .unwrap_or_default(), - PendingNetEvent::ShutdownComplete(id) => statics::sockets() + PendingNetEvent::ShutdownComplete(id, _, _) => statics::sockets() .lock() .ok() .and_then(|sockets| sockets.get(id).map(|socket| vec![socket.shutdown_async_id])) @@ -1765,8 +1791,12 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { drop(frame); lifecycle::drain_once_listeners(id, "end"); } - PendingNetEvent::ShutdownComplete(_) => {} + PendingNetEvent::WriteComplete(_, completion, error) + | PendingNetEvent::ShutdownComplete(_, completion, error) => { + lifecycle::dispatch_socket_completion(completion, error); + } PendingNetEvent::Close(id) => { + lifecycle::drop_socket_completions(id); extern "C" { fn js_tls_client_record_closed(handle: i64); } diff --git a/crates/perry-ext-net/src/lifecycle.rs b/crates/perry-ext-net/src/lifecycle.rs index 9875eccaf0..2dee4476c3 100644 --- a/crates/perry-ext-net/src/lifecycle.rs +++ b/crates/perry-ext-net/src/lifecycle.rs @@ -20,11 +20,10 @@ //! `NativeModSig` rows live in //! `perry-codegen/src/lower_call/native_table/net_events.rs`. -use perry_ffi::{ - alloc_string, nanbox_string_bits, ArrayHeader, JsClosure, JsValue, RawClosureHeader, - StringHeader, -}; +use perry_ffi::{alloc_string, nanbox_string_bits, ArrayHeader, JsValue, StringHeader}; use std::collections::HashSet; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Mutex, OnceLock}; use crate::statics; use crate::string_from_header_i64; @@ -57,6 +56,40 @@ fn nanbox_undefined() -> f64 { f64::from_bits(TAG_UNDEFINED_BITS) } +/// Main-thread custody for write/end callbacks awaiting socket-task I/O. +pub(crate) fn socket_completions() -> &'static Mutex> { + static COMPLETIONS: OnceLock>> = + OnceLock::new(); + COMPLETIONS.get_or_init(|| Mutex::new(std::collections::HashMap::new())) +} + +pub(crate) unsafe fn dispatch_socket_completion(completion: u64, error: Option) { + let callback = (completion != 0) + .then(|| socket_completions().lock().unwrap().remove(&completion)) + .flatten() + .map(|(_, callback)| callback) + .unwrap_or(0); + if callback == 0 { + return; + } + let mut frame = crate::dispatch_custody::DispatchFrame::park(vec![callback]); + if let Some(message) = error { + frame.set_payload(crate::build_error_object(&message).to_bits()); + let _ = perry_ffi::JsClosure::from_raw(frame.cb(0) as *const perry_ffi::RawClosureHeader) + .call1(f64::from_bits(frame.payload_bits())); + } else { + let _ = perry_ffi::JsClosure::from_raw(frame.cb(0) as *const perry_ffi::RawClosureHeader) + .call0(); + } +} + +pub(crate) fn drop_socket_completions(socket_id: i64) { + socket_completions() + .lock() + .unwrap() + .retain(|_, (owner, _)| *owner != socket_id); +} + /// NaN-box a freshly allocated runtime string as an `f64` JS value. fn nanbox_string_value(s: &str) -> f64 { let header = alloc_string(s).as_raw(); @@ -298,10 +331,22 @@ pub unsafe extern "C" fn js_ext_net_socket_write(handle: i64, chunk_bits: i64) { Some(b) => b, None => return, }; + enqueue_socket_write(handle, bytes, 0); +} + +fn enqueue_socket_write(handle: i64, bytes: Vec, completion: u64) { let mut sockets = statics::sockets().lock().unwrap(); if let Some(s) = sockets.get_mut(&handle) { s.bytes_written = s.bytes_written.saturating_add(bytes.len() as u64); - let _ = s.cmd_tx.send(crate::SocketCommand::Write(bytes)); + if s.cmd_tx + .send(crate::SocketCommand::Write(bytes, completion)) + .is_err() + && completion != 0 + { + socket_completions().lock().unwrap().remove(&completion); + } + } else if completion != 0 { + socket_completions().lock().unwrap().remove(&completion); } } @@ -318,20 +363,31 @@ pub unsafe extern "C" fn js_net_socket_write(handle: i64, chunk_bits: i64) { js_ext_net_socket_write(handle, chunk_bits); } -unsafe fn call_socket_completion(values: [f64; 3]) { +unsafe fn socket_completion(values: [f64; 3]) -> i64 { extern "C" { fn js_value_is_closure(value_bits: i64) -> i32; } - if let Some(callback) = values + values .into_iter() .find(|value| js_value_is_closure(value.to_bits() as i64) != 0) - { - const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - let raw = (callback.to_bits() & POINTER_MASK) as *const RawClosureHeader; - if !raw.is_null() { - let _ = JsClosure::from_raw(raw).call0(); - } + .map(|callback| { + const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; + (callback.to_bits() & POINTER_MASK) as i64 + }) + .unwrap_or(0) +} + +fn register_socket_completion(handle: i64, callback: i64) -> u64 { + static NEXT_COMPLETION: AtomicU64 = AtomicU64::new(1); + if callback == 0 { + return 0; } + let token = NEXT_COMPLETION.fetch_add(1, Ordering::Relaxed); + socket_completions() + .lock() + .unwrap() + .insert(token, (handle, callback)); + token } /// Full Node overload for `socket.write(chunk[, encoding][, callback])`. @@ -342,8 +398,18 @@ pub unsafe extern "C" fn js_ext_net_socket_write3( encoding_or_callback: f64, callback: f64, ) { - js_ext_net_socket_write(handle, chunk.to_bits() as i64); - call_socket_completion([chunk, encoding_or_callback, callback]); + let roots = perry_ffi::TransientRootScope::enter(); + let callback = roots.root_nanbox(callback); + let encoding_or_callback = roots.root_nanbox(encoding_or_callback); + let completion = socket_completion([chunk, encoding_or_callback.get(), callback.get()]); + let completion = register_socket_completion(handle, completion); + let Some(bytes) = crate::jsvalue_to_socket_bytes(chunk) else { + if completion != 0 { + socket_completions().lock().unwrap().remove(&completion); + } + return; + }; + enqueue_socket_write(handle, bytes, completion); } /// `socket.end([data])` — optionally write a final chunk, then half-close the @@ -361,6 +427,9 @@ pub unsafe extern "C" fn js_ext_net_socket_write3( /// must reference live runtime allocations. #[no_mangle] pub unsafe extern "C" fn js_ext_net_socket_end(handle: i64, chunk_bits: i64) { + // Decode the GC-managed input before provider init can run user hooks and + // move it. Only owned bytes survive across that callback boundary. + let final_bytes = crate::jsvalue_to_socket_bytes(f64::from_bits(chunk_bits as u64)); let trigger = statics::sockets().lock().ok().and_then(|sockets| { sockets .get(&handle) @@ -374,13 +443,13 @@ pub unsafe extern "C" fn js_ext_net_socket_end(handle: i64, chunk_bits: i64) { } let mut sockets = statics::sockets().lock().unwrap(); if let Some(s) = sockets.get_mut(&handle) { - if let Some(bytes) = crate::jsvalue_to_socket_bytes(f64::from_bits(chunk_bits as u64)) { + if let Some(bytes) = final_bytes { if !bytes.is_empty() { s.bytes_written = s.bytes_written.saturating_add(bytes.len() as u64); - let _ = s.cmd_tx.send(crate::SocketCommand::Write(bytes)); + let _ = s.cmd_tx.send(crate::SocketCommand::Write(bytes, 0)); } } - let _ = s.cmd_tx.send(crate::SocketCommand::End); + let _ = s.cmd_tx.send(crate::SocketCommand::End(0)); } } @@ -403,8 +472,45 @@ pub unsafe extern "C" fn js_ext_net_socket_end3( encoding_or_callback: f64, callback: f64, ) { - js_ext_net_socket_end(handle, chunk_or_callback.to_bits() as i64); - call_socket_completion([chunk_or_callback, encoding_or_callback, callback]); + let roots = perry_ffi::TransientRootScope::enter(); + let chunk_or_callback = roots.root_nanbox(chunk_or_callback); + let encoding_or_callback = roots.root_nanbox(encoding_or_callback); + let callback = roots.root_nanbox(callback); + let completion = socket_completion([ + chunk_or_callback.get(), + encoding_or_callback.get(), + callback.get(), + ]); + let completion = register_socket_completion(handle, completion); + let final_bytes = crate::jsvalue_to_socket_bytes(chunk_or_callback.get()); + let trigger = statics::sockets().lock().ok().and_then(|sockets| { + sockets + .get(&handle) + .and_then(|socket| (socket.shutdown_async_id == 0).then_some(socket.tcp_async_id)) + }); + if let Some(trigger) = trigger { + let async_id = crate::init_provider_with_trigger(b"SHUTDOWNWRAP", trigger); + if let Some(socket) = statics::sockets().lock().unwrap().get_mut(&handle) { + socket.shutdown_async_id = async_id; + } + } + let mut sockets = statics::sockets().lock().unwrap(); + if let Some(socket) = sockets.get_mut(&handle) { + if let Some(bytes) = final_bytes.filter(|bytes| !bytes.is_empty()) { + socket.bytes_written = socket.bytes_written.saturating_add(bytes.len() as u64); + let _ = socket.cmd_tx.send(crate::SocketCommand::Write(bytes, 0)); + } + if socket + .cmd_tx + .send(crate::SocketCommand::End(completion)) + .is_err() + && completion != 0 + { + socket_completions().lock().unwrap().remove(&completion); + } + } else if completion != 0 { + socket_completions().lock().unwrap().remove(&completion); + } } /// `socket.destroy()` — hard close. Flags the handle destroyed (so diff --git a/crates/perry-ext-net/src/provider_lifecycle.rs b/crates/perry-ext-net/src/provider_lifecycle.rs index b6c848e8d3..7f276cd07c 100644 --- a/crates/perry-ext-net/src/provider_lifecycle.rs +++ b/crates/perry-ext-net/src/provider_lifecycle.rs @@ -51,7 +51,7 @@ pub(super) unsafe fn prepare_event_provider(ev: &PendingNetEvent) { } } } - PendingNetEvent::ShutdownComplete(id) => { + PendingNetEvent::ShutdownComplete(id, _, _) => { let trigger = statics::sockets().lock().ok().and_then(|sockets| { sockets.get(id).and_then(|socket| { (socket.shutdown_async_id == 0).then_some(socket.tcp_async_id) @@ -78,6 +78,7 @@ pub(super) fn event_provider_id(ev: &PendingNetEvent) -> u64 { PendingNetEvent::SecureConnect(id) | PendingNetEvent::Data(id, _) | PendingNetEvent::End(id) + | PendingNetEvent::WriteComplete(id, _, _) | PendingNetEvent::Error(id, _) | PendingNetEvent::AbortError(id) | PendingNetEvent::Close(id) => statics::sockets() @@ -85,7 +86,7 @@ pub(super) fn event_provider_id(ev: &PendingNetEvent) -> u64 { .ok() .and_then(|sockets| sockets.get(id).map(|socket| socket.tcp_async_id)) .unwrap_or(0), - PendingNetEvent::ShutdownComplete(id) => statics::sockets() + PendingNetEvent::ShutdownComplete(id, _, _) => statics::sockets() .lock() .ok() .and_then(|sockets| sockets.get(id).map(|socket| socket.shutdown_async_id)) diff --git a/crates/perry-ext-net/src/raw_bridge.rs b/crates/perry-ext-net/src/raw_bridge.rs index 6503fca510..9a51c88d76 100644 --- a/crates/perry-ext-net/src/raw_bridge.rs +++ b/crates/perry-ext-net/src/raw_bridge.rs @@ -105,7 +105,7 @@ extern "C" fn perry_net_raw_write(socket_id: i64, ptr: *const u8, len: usize) -> }; if let Ok(g) = statics::sockets().lock() { if let Some(s) = g.get(&socket_id) { - return i32::from(s.cmd_tx.send(SocketCommand::Write(bytes)).is_ok()); + return i32::from(s.cmd_tx.send(SocketCommand::Write(bytes, 0)).is_ok()); } } 0 diff --git a/crates/perry-ext-zlib/src/stream.rs b/crates/perry-ext-zlib/src/stream.rs index f67aaaa771..dc33937822 100644 --- a/crates/perry-ext-zlib/src/stream.rs +++ b/crates/perry-ext-zlib/src/stream.rs @@ -20,6 +20,7 @@ use perry_ffi::{ alloc_buffer, alloc_string, gc_register_mutable_root_scanner_named, notify_main_thread, BufferHeader, ErrorKind, GcRootVisitor, JsClosure, JsValue, RawClosureHeader, StringHeader, + TransientRootScope, }; use std::collections::{HashMap, HashSet, VecDeque}; use std::io::{Read, Write}; @@ -67,6 +68,7 @@ extern "C" { // synchronously before queuing codec work. pub(crate) fn js_zlib_validate_callback(callback: f64) -> i64; fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64; + fn js_async_hooks_provider_defer_destroy(async_id: u64, check_turns: u32); fn js_async_hooks_provider_enter(async_id: u64); fn js_async_hooks_provider_leave(async_id: u64); fn js_native_call_method_str_key( @@ -706,7 +708,9 @@ pub(crate) unsafe fn queue_one_shot_callback( ) where F: FnOnce(&[u8]) -> std::io::Result>, { - let callback = js_zlib_validate_callback(callback_value); + let scope = TransientRootScope::enter(); + let callback_value = scope.root_nanbox(callback_value); + let _ = js_zlib_validate_callback(callback_value.get()); let data_bits = data_value.to_bits() as i64; js_zlib_validate_buffer_arg(data_bits); let result = match read_input_from_bits(data_bits) { @@ -716,6 +720,10 @@ pub(crate) unsafe fn queue_one_shot_callback( ensure_aux_pump_registered(); ensure_gc_scanner_registered(); let async_id = js_async_hooks_provider_init(b"ZLIB".as_ptr(), b"ZLIB".len()); + // Provider init delivers user hooks and may move the callback. Re-read the + // rooted value only after it returns, immediately before publishing it in + // the scanned pending queue. + let callback = js_zlib_validate_callback(callback_value.get()); statics() .lock() .unwrap() @@ -1337,6 +1345,7 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { }; count += 1; let event_async_id = event_stream_handle(&ev).map(stream_async_id).unwrap_or(0); + let mut destroy_after_dispatch = 0; if event_async_id != 0 { js_async_hooks_provider_enter(event_async_id); } @@ -1368,7 +1377,10 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { } } ZlibEvent::Finish(id) => { - for cb in listeners_for(id, "finish") { + let scope = TransientRootScope::enter(); + let callbacks = scope.root_addrs(&listeners_for(id, "finish")); + for cb in callbacks { + let cb = cb.get(); if cb != 0 { let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); } @@ -1422,6 +1434,7 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { } } drop_buffered_stream(&mut statics().lock().unwrap(), id); + destroy_after_dispatch = event_async_id; } ZlibEvent::Callback(cb) => { if cb != 0 { @@ -1429,11 +1442,19 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { } } ZlibEvent::OneShotCallback(cb, result, async_id) => { + let scope = TransientRootScope::enter(); + let callback = scope.root_addr(cb); + // Node exposes the native codec completion and delivery of the + // JavaScript callback as two phases of the same ZLIB resource. js_async_hooks_provider_enter(async_id); js_async_hooks_provider_leave(async_id); js_async_hooks_provider_enter(async_id); - call_one_shot_callback(cb, result); + call_one_shot_callback(callback.get(), result); js_async_hooks_provider_leave(async_id); + // This is queued before the callback's Promise continuation + // schedules its first user immediate, so zlib needs one more + // check turn than synchronously closed handles. + js_async_hooks_provider_defer_destroy(async_id, 4); } ZlibEvent::Error(id, msg) => { let err_f64 = build_error_object(&msg); @@ -1443,11 +1464,15 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { } } drop_buffered_stream(&mut statics().lock().unwrap(), id); + destroy_after_dispatch = event_async_id; } } if event_async_id != 0 { js_async_hooks_provider_leave(event_async_id); } + if destroy_after_dispatch != 0 { + js_async_hooks_provider_defer_destroy(destroy_after_dispatch, 4); + } } count } diff --git a/crates/perry-runtime/src/async_context.rs b/crates/perry-runtime/src/async_context.rs index 53567fcdd0..fa80f61c1c 100644 --- a/crates/perry-runtime/src/async_context.rs +++ b/crates/perry-runtime/src/async_context.rs @@ -309,6 +309,14 @@ pub fn clear_store(handle: i64) { .entries .iter() .any(|entry| entry.handle == handle) + }) || CONTEXT_GUARDS.with(|guards| { + guards.borrow().iter().any(|guard| { + matches!( + &guard.action, + ContextGuardAction::RestoreStores(saved_handle, Some(_)) + if *saved_handle == handle + ) + }) }); if was_active { HANDLE_GENERATIONS.with(|generations| { diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 9b4ddc0e52..961190f9c1 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -19,6 +19,14 @@ use crate::object::{js_object_get_field_by_name, ObjectHeader}; use crate::string::{js_string_from_bytes, StringHeader}; use crate::value::{JSValue, POINTER_MASK}; +mod provider_ffi; +pub use provider_ffi::{ + defer_destroy_after_check_turns, js_async_hooks_provider_defer_destroy, + js_async_hooks_provider_destroy, js_async_hooks_provider_enter, js_async_hooks_provider_init, + js_async_hooks_provider_init_with_trigger, js_async_hooks_provider_leave, + js_async_hooks_provider_run_catching, js_async_hooks_provider_run_catching_with_this, +}; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; const TAG_MASK: u64 = 0xFFFF_0000_0000_0000; @@ -623,12 +631,21 @@ fn with_hook_callbacks( .iter() .map(|callbacks| scope.root_raw_const_ptr(callbacks.for_phase(phase))) .collect(); + let mut thrown = None; for callback in rooted { - callback.with_const_ptr::(|callback| { + let outcome = callback.with_const_ptr::(|callback| { if !callback.is_null() { - f(callback); + return crate::exception::js_call_catching(|| { + f(callback); + f64::from_bits(crate::value::TAG_UNDEFINED) + }); } + Ok(f64::from_bits(crate::value::TAG_UNDEFINED)) }); + if let Err(error) = outcome { + thrown = Some(scope.root_nanbox_f64(error)); + break; + } } let outermost = HOOK_CALLBACK_DEPTH.with(|depth| { let next = depth.get().saturating_sub(1); @@ -641,6 +658,9 @@ fn with_hook_callbacks( set_hook_enabled(index, enabled); } } + if let Some(error) = thrown { + crate::exception::js_throw(error.get_nanbox_f64()); + } } /// Model the Promise that Node uses to evaluate an ESM entry module. Perry's @@ -910,66 +930,6 @@ pub fn run_resource_scope(ids: AsyncResourceIds, completion: impl FnOnce()) { leave_resource_scope(ids.async_id); } -/// C ABI used by separately-linked native providers such as perry-ext-zlib. -#[no_mangle] -pub unsafe extern "C" fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64 { - if type_ptr.is_null() { - return 0; - } - let type_name = std::str::from_utf8_unchecked(std::slice::from_raw_parts(type_ptr, type_len)); - let resource = crate::object::js_object_alloc_null_proto(0, 0); - init_resource( - type_name, - crate::value::js_nanbox_pointer(resource as i64), - true, - ) - .async_id -} - -#[no_mangle] -pub unsafe extern "C" fn js_async_hooks_provider_init_with_trigger( - type_ptr: *const u8, - type_len: usize, - trigger_async_id: u64, -) -> u64 { - if type_ptr.is_null() { - return 0; - } - let type_name = std::str::from_utf8_unchecked(std::slice::from_raw_parts(type_ptr, type_len)); - let resource = crate::object::js_object_alloc_null_proto(0, 0); - init_resource_with_trigger( - type_name, - crate::value::js_nanbox_pointer(resource as i64), - true, - trigger_async_id, - ) - .async_id -} - -#[no_mangle] -pub extern "C" fn js_async_hooks_provider_enter(async_id: u64) { - let trigger_async_id = RESOURCES - .lock() - .unwrap() - .get(&async_id) - .map(|meta| meta.trigger_async_id) - .unwrap_or(0); - enter_resource_scope(AsyncResourceIds { - async_id, - trigger_async_id, - }); -} - -#[no_mangle] -pub extern "C" fn js_async_hooks_provider_leave(async_id: u64) { - leave_resource_scope(async_id); -} - -#[no_mangle] -pub extern "C" fn js_async_hooks_provider_destroy(async_id: u64) { - destroy(async_id); -} - pub fn enqueue_gc_destroy(async_id: u64) { if async_id != 0 { GC_DESTROY_QUEUE.lock().unwrap().push_back(async_id); @@ -1476,7 +1436,6 @@ pub fn try_async_resource_method_dispatch( ) { return None; } - let handle = resolve_async_resource_handle(receiver)?; let scope = crate::gc::RuntimeHandleScope::new(); let raw_args: Vec = if args_ptr.is_null() || args_len == 0 { Vec::new() @@ -1484,6 +1443,7 @@ pub fn try_async_resource_method_dispatch( unsafe { std::slice::from_raw_parts(args_ptr, args_len).to_vec() } }; let arg_handles = scope.root_nanbox_f64_slice(&raw_args); + let handle = resolve_async_resource_handle(receiver)?; let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); Some(match method_name { "asyncId" => js_async_resource_async_id(handle), diff --git a/crates/perry-runtime/src/async_hooks/provider_ffi.rs b/crates/perry-runtime/src/async_hooks/provider_ffi.rs new file mode 100644 index 0000000000..b50e7f3657 --- /dev/null +++ b/crates/perry-runtime/src/async_hooks/provider_ffi.rs @@ -0,0 +1,168 @@ +//! Exception-safe callback bridges for separately linked async providers. + +use super::{ + destroy, enter_resource_scope, init_resource, init_resource_with_trigger, leave_resource_scope, + AsyncResourceIds, RESOURCES, +}; + +extern "C" fn deferred_destroy_step(closure: *const crate::closure::ClosureHeader) -> f64 { + let async_id = crate::closure::js_closure_get_capture_f64(closure, 0) as u64; + let remaining = crate::closure::js_closure_get_capture_f64(closure, 1) as u32; + if remaining == 0 { + destroy(async_id); + } else { + schedule_deferred_destroy_step(async_id, remaining - 1); + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +fn schedule_deferred_destroy_step(async_id: u64, remaining: u32) { + crate::closure::js_register_closure_arity(deferred_destroy_step as *const u8, 0); + let scope = crate::gc::RuntimeHandleScope::new(); + let callback = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc( + deferred_destroy_step as *const u8, + 2, + )); + callback.with_mut_ptr(|callback| { + crate::closure::js_closure_set_capture_f64(callback, 0, async_id as f64); + crate::closure::js_closure_set_capture_f64(callback, 1, remaining as f64); + crate::timer::js_set_immediate_callback(callback as i64); + }); +} + +/// Retire a native provider after a fixed number of check phases. libuv handle +/// close callbacks do not fire synchronously with APIs such as `unwatchFile` +/// or one-shot zlib completion, so their destroy hooks must remain observable +/// only after the corresponding close turns have run. +pub fn defer_destroy_after_check_turns(async_id: u64, check_turns: u32) { + if async_id == 0 { + return; + } + if check_turns == 0 { + destroy(async_id); + } else { + schedule_deferred_destroy_step(async_id, check_turns - 1); + } +} + +/// C ABI used by separately-linked native providers such as perry-ext-zlib. +#[no_mangle] +pub unsafe extern "C" fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64 { + if type_ptr.is_null() { + return 0; + } + let type_name = std::str::from_utf8_unchecked(std::slice::from_raw_parts(type_ptr, type_len)); + let resource = crate::object::js_object_alloc_null_proto(0, 0); + init_resource( + type_name, + crate::value::js_nanbox_pointer(resource as i64), + true, + ) + .async_id +} + +#[no_mangle] +pub unsafe extern "C" fn js_async_hooks_provider_init_with_trigger( + type_ptr: *const u8, + type_len: usize, + trigger_async_id: u64, +) -> u64 { + if type_ptr.is_null() { + return 0; + } + let type_name = std::str::from_utf8_unchecked(std::slice::from_raw_parts(type_ptr, type_len)); + let resource = crate::object::js_object_alloc_null_proto(0, 0); + init_resource_with_trigger( + type_name, + crate::value::js_nanbox_pointer(resource as i64), + true, + trigger_async_id, + ) + .async_id +} + +#[no_mangle] +pub extern "C" fn js_async_hooks_provider_enter(async_id: u64) { + let trigger_async_id = RESOURCES + .lock() + .unwrap() + .get(&async_id) + .map(|meta| meta.trigger_async_id) + .unwrap_or(0); + enter_resource_scope(AsyncResourceIds { + async_id, + trigger_async_id, + }); +} + +#[no_mangle] +pub extern "C" fn js_async_hooks_provider_leave(async_id: u64) { + leave_resource_scope(async_id); +} + +#[no_mangle] +pub extern "C" fn js_async_hooks_provider_destroy(async_id: u64) { + destroy(async_id); +} + +#[no_mangle] +pub extern "C" fn js_async_hooks_provider_defer_destroy(async_id: u64, check_turns: u32) { + defer_destroy_after_check_turns(async_id, check_turns); +} + +/// Run an external-provider callback while guaranteeing that the provider +/// scope is restored before a JS exception resumes unwinding into generated +/// code. Rust `Drop` guards cannot provide this guarantee because Perry's JS +/// exception transport deliberately skips runtime Rust cleanup frames. +#[no_mangle] +pub unsafe extern "C" fn js_async_hooks_provider_run_catching( + async_id: u64, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, +) -> f64 { + js_async_hooks_provider_enter(async_id); + let outcome = crate::exception::js_call_catching(|| callback(data)); + let scope = crate::gc::RuntimeHandleScope::new(); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + js_async_hooks_provider_leave(async_id); + if threw { + crate::exception::js_throw(result.get_nanbox_f64()); + } + result.get_nanbox_f64() +} + +/// Provider callback wrapper for external EventEmitter-style dispatch. It +/// additionally restores implicit `this` and can retire a one-shot provider +/// before propagating a JavaScript exception. +#[no_mangle] +pub unsafe extern "C" fn js_async_hooks_provider_run_catching_with_this( + async_id: u64, + this_value: f64, + destroy_after: i32, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, +) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let this_value = scope.root_nanbox_f64(this_value); + js_async_hooks_provider_enter(async_id); + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( + this_value.get_nanbox_f64(), + )); + let outcome = crate::exception::js_call_catching(|| callback(data)); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); + js_async_hooks_provider_leave(async_id); + if destroy_after != 0 { + js_async_hooks_provider_destroy(async_id); + } + if threw { + crate::exception::js_throw(result.get_nanbox_f64()); + } + result.get_nanbox_f64() +} diff --git a/crates/perry-runtime/src/child_process/reactor.rs b/crates/perry-runtime/src/child_process/reactor.rs index ef4136a0e1..dfca1ece2e 100644 --- a/crates/perry-runtime/src/child_process/reactor.rs +++ b/crates/perry-runtime/src/child_process/reactor.rs @@ -201,7 +201,7 @@ static CP_LIVE: Mutex>> = Mutex::new(None); fn cp_init_async_resources( cp: f64, - stdin_obj: f64, + stdin_obj: Option, stdout_obj: f64, stderr_obj: f64, ) -> ( @@ -209,8 +209,14 @@ fn cp_init_async_resources( [crate::async_hooks::AsyncResourceIds; 3], ) { let process_ids = crate::async_hooks::init_resource("PROCESSWRAP", cp, true); + let stdin_ids = stdin_obj + .map(|object| crate::async_hooks::init_resource("PIPEWRAP", object, true)) + .unwrap_or(crate::async_hooks::AsyncResourceIds { + async_id: 0, + trigger_async_id: 0, + }); let pipe_ids = [ - crate::async_hooks::init_resource("PIPEWRAP", stdin_obj, true), + stdin_ids, crate::async_hooks::init_resource("PIPEWRAP", stdout_obj, true), crate::async_hooks::init_resource("PIPEWRAP", stderr_obj, true), ]; @@ -507,7 +513,8 @@ fn cp_register_live_child_parts( for (_, stream, _) in &extra_pipes { cp_set_field(*stream, b"__cpHandle", handle_f); } - let (process_ids, pipe_ids) = cp_init_async_resources(cp, stdin_obj, stdout_obj, stderr_obj); + let (process_ids, pipe_ids) = + cp_init_async_resources(cp, Some(stdin_obj), stdout_obj, stderr_obj); // For fork, keep a clone of the IPC socket for send/disconnect; the reader // thread owns the original. @@ -1120,8 +1127,7 @@ pub(super) fn cp_exec_async( exceeded: false, timed_out: false, }); - let (process_ids, pipe_ids) = - cp_init_async_resources(cp, TAG_NULL_F64, stdout_obj, stderr_obj); + let (process_ids, pipe_ids) = cp_init_async_resources(cp, None, stdout_obj, stderr_obj); { let mut guard = cp_live_lock(); @@ -1637,8 +1643,10 @@ pub(super) fn cp_async_scope_for_target( child .pipe_bits .iter() - .position(|bits| *bits == target_bits) - .map(|index| child.pipe_ids[index]) + .enumerate() + .filter(|(index, bits)| *index != 0 || **bits != TAG_NULL_F64.to_bits()) + .find(|(_, bits)| **bits == target_bits) + .map(|(index, _)| child.pipe_ids[index]) } // ============================================================================ diff --git a/crates/perry-runtime/src/dns.rs b/crates/perry-runtime/src/dns.rs index 76a9fa7293..ea60a53806 100644 --- a/crates/perry-runtime/src/dns.rs +++ b/crates/perry-runtime/src/dns.rs @@ -56,6 +56,8 @@ const RESOLVER_RESOLVE_METHODS: &[&str] = &[ "reverse", ]; const RESOLVER_SERVERS_FIELD: &str = "__dns_servers"; +const RESOLVER_TIMEOUT_FIELD: &str = "__dns_timeout"; +const RESOLVER_TRIES_FIELD: &str = "__dns_tries"; #[derive(Clone, Copy)] pub(crate) enum RecordKind { @@ -1244,22 +1246,66 @@ fn method_value(name: &str) -> f64 { js_nanbox_pointer(closure as i64) } -fn resolver_object(initial_servers: Vec) -> *mut ObjectHeader { - let method_count = RESOLVER_CONTROL_METHODS.len() + RESOLVER_RESOLVE_METHODS.len() + 1; - let obj = js_object_alloc(0, method_count as u32); - js_object_set_field_by_name( - obj, - key(RESOLVER_SERVERS_FIELD), - servers_array_value(&initial_servers), - ); +fn resolver_object(initial_servers: Vec, options: f64) -> *mut ObjectHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let options = scope.root_nanbox_f64(options); + let timeout_key = scope.root_string_ptr(key("timeout")); + let timeout = resolver_object_from_value(options.get_nanbox_f64()) + .map(|object| { + timeout_key.with_mut_ptr::(|key| { + crate::object::js_object_get_field_by_name_f64(object, key) + }) + }) + .unwrap_or_else(undefined_value); + let timeout = scope.root_nanbox_f64(timeout); + let tries_key = scope.root_string_ptr(key("tries")); + let tries = resolver_object_from_value(options.get_nanbox_f64()) + .map(|object| { + tries_key.with_mut_ptr::(|key| { + crate::object::js_object_get_field_by_name_f64(object, key) + }) + }) + .unwrap_or_else(undefined_value); + let tries = scope.root_nanbox_f64(tries); + let method_count = RESOLVER_CONTROL_METHODS.len() + RESOLVER_RESOLVE_METHODS.len() + 3; + let obj = scope.root_raw_mut_ptr(js_object_alloc(0, method_count as u32)); + let servers = scope.root_nanbox_f64(servers_array_value(&initial_servers)); + let servers_key = scope.root_string_ptr(key(RESOLVER_SERVERS_FIELD)); + obj.with_mut_ptr::(|object| { + servers_key.with_mut_ptr::(|key| { + js_object_set_field_by_name(object, key, servers.get_nanbox_f64()); + }); + }); + let timeout_key = scope.root_string_ptr(key(RESOLVER_TIMEOUT_FIELD)); + obj.with_mut_ptr::(|object| { + timeout_key.with_mut_ptr::(|key| { + js_object_set_field_by_name(object, key, timeout.get_nanbox_f64()); + }); + }); + let tries_key = scope.root_string_ptr(key(RESOLVER_TRIES_FIELD)); + obj.with_mut_ptr::(|object| { + tries_key.with_mut_ptr::(|key| { + js_object_set_field_by_name(object, key, tries.get_nanbox_f64()); + }); + }); for method in RESOLVER_CONTROL_METHODS { - js_object_set_field_by_name(obj, key(method), method_value(method)); + let value = scope.root_nanbox_f64(method_value(method)); + let field_key = scope.root_string_ptr(key(method)); + obj.with_mut_ptr::(|object| { + field_key.with_mut_ptr::(|field_key| { + js_object_set_field_by_name(object, field_key, value.get_nanbox_f64()); + }); + }); } for method in RESOLVER_RESOLVE_METHODS { - js_object_set_field_by_name(obj, key(method), method_value(method)); + let value = scope.root_nanbox_f64(method_value(method)); + let field_key = scope.root_string_ptr(key(method)); + obj.with_mut_ptr::(|object| { + field_key.with_mut_ptr::(|field_key| { + js_object_set_field_by_name(object, field_key, value.get_nanbox_f64()); + }); + }); } - let scope = crate::gc::RuntimeHandleScope::new(); - let obj = scope.root_raw_mut_ptr(obj); let (_, obj_ptr) = obj.across_mut::(|| { obj.with_mut_ptr::(|obj_ptr| { let _ = crate::async_hooks::init_resource( diff --git a/crates/perry-runtime/src/dns/ffi.rs b/crates/perry-runtime/src/dns/ffi.rs index fe239d503c..41311f4665 100644 --- a/crates/perry-runtime/src/dns/ffi.rs +++ b/crates/perry-runtime/src/dns/ffi.rs @@ -257,13 +257,13 @@ pub extern "C" fn js_dns_get_default_result_order(_args: i64) -> f64 { } #[no_mangle] -pub extern "C" fn js_dns_resolver_new(_args: i64) -> f64 { - boxed_pointer(resolver_object(stored_servers()) as *const u8) +pub extern "C" fn js_dns_resolver_new(args: i64) -> f64 { + boxed_pointer(resolver_object(stored_servers(), first_arg(args)) as *const u8) } #[no_mangle] -pub extern "C" fn js_dns_promises_resolver_new(_args: i64) -> f64 { - boxed_pointer(resolver_object(stored_promise_servers()) as *const u8) +pub extern "C" fn js_dns_promises_resolver_new(args: i64) -> f64 { + boxed_pointer(resolver_object(stored_promise_servers(), first_arg(args)) as *const u8) } #[no_mangle] diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index 905cf6cae6..1848fc5308 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -83,6 +83,7 @@ struct WatchFileState { path: String, object_value: f64, timer_id: i64, + async_id: u64, bigint: bool, previous: Option, listeners: HashMap>, @@ -602,6 +603,10 @@ fn close_watch_file_state(id: usize) { let removed = WATCH_FILE_STATES.with(|states| states.borrow_mut().remove(&id)); if let Some(state) = removed { crate::timer::clearInterval(state.timer_id); + // Node retires the underlying uv_fs_poll handle from its close + // callback, after three check phases rather than synchronously from + // unwatchFile(). Preserve that observable destroy-hook timing. + crate::async_hooks::defer_destroy_after_check_turns(state.async_id, 3); WATCH_FILE_PATHS.with(|paths| { paths.borrow_mut().remove(&state.path); }); @@ -1530,7 +1535,7 @@ pub extern "C" fn js_fs_watch_file(path_value: f64, arg1: f64, arg2: f64) -> f64 } let id = next_watch_id(); let object_value = build_stat_watcher_object(id); - let _ = crate::async_hooks::init_resource("STATWATCHER", object_value, true); + let async_id = crate::async_hooks::init_resource("STATWATCHER", object_value, true).async_id; let interval = option_interval_ms(options_value); let persistent = option_bool_default_local(options_value, b"persistent", true); let bigint = unsafe { options_bool_field(options_value, b"bigint") }; @@ -1548,6 +1553,7 @@ pub extern "C" fn js_fs_watch_file(path_value: f64, arg1: f64, arg2: f64) -> f64 path: path.clone(), object_value, timer_id, + async_id, bigint, previous: stat_snapshot(&path), listeners, diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 3068562925..2b0552a4ed 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -442,7 +442,6 @@ pub fn gen_gc_enabled() -> bool { // decision that hasn't been made". fn gc_force_evacuate_enabled() -> bool { - #[cfg(test)] if let Some(forced) = knob_overrides::FORCE_EVACUATE_TEST_OVERRIDE.with(std::cell::Cell::get) { return forced; } @@ -456,7 +455,6 @@ fn gc_force_evacuate_enabled() -> bool { } fn gc_verify_evacuation_enabled() -> bool { - #[cfg(test)] if let Some(forced) = knob_overrides::VERIFY_EVACUATION_TEST_OVERRIDE.with(std::cell::Cell::get) { return forced; @@ -488,11 +486,10 @@ fn gc_verify_evacuation_enabled() -> bool { /// `gc::tests::evacuation::explicit_gc_under_forced_evacuation_runs_a_moving_minor`, /// whose comment says in as many words that "an `EnvVarGuard` would set a /// process-global every other test in this crate shares". -#[cfg(test)] pub(super) mod knob_overrides { use std::cell::Cell; - thread_local! { + crate::perry_thread_local! { pub(super) static FORCE_EVACUATE_TEST_OVERRIDE: Cell> = const { Cell::new(None) }; pub(super) static VERIFY_EVACUATION_TEST_OVERRIDE: Cell> = @@ -500,14 +497,17 @@ pub(super) mod knob_overrides { } /// Pin `gc_force_evacuate_enabled()` for this thread only. + #[cfg(test)] pub(crate) struct ForcedEvacuationTestGuard(Option); + #[cfg(test)] impl ForcedEvacuationTestGuard { pub(crate) fn on() -> Self { Self(FORCE_EVACUATE_TEST_OVERRIDE.with(|c| c.replace(Some(true)))) } } + #[cfg(test)] impl Drop for ForcedEvacuationTestGuard { fn drop(&mut self) { FORCE_EVACUATE_TEST_OVERRIDE.with(|c| c.set(self.0)); @@ -515,14 +515,17 @@ pub(super) mod knob_overrides { } /// Pin `gc_verify_evacuation_enabled()` for this thread only. + #[cfg(test)] pub(crate) struct VerifyEvacuationTestGuard(Option); + #[cfg(test)] impl VerifyEvacuationTestGuard { pub(crate) fn on() -> Self { Self(VERIFY_EVACUATION_TEST_OVERRIDE.with(|c| c.replace(Some(true)))) } } + #[cfg(test)] impl Drop for VerifyEvacuationTestGuard { fn drop(&mut self) { VERIFY_EVACUATION_TEST_OVERRIDE.with(|c| c.set(self.0)); @@ -530,6 +533,25 @@ pub(super) mod knob_overrides { } } +/// Test-only control surface used by separately compiled extension-crate +/// tests. The override is thread-local, so it cannot race unrelated tests the +/// way mutating `PERRY_GC_FORCE_EVACUATE` did. `enabled`: `1` = on, `0` = off, +/// any negative value = clear. Returns the previous state using the same +/// encoding. +#[doc(hidden)] +pub fn js_gc_force_evacuation_test_override(enabled: i32) -> i32 { + let next = match enabled { + 1.. => Some(true), + 0 => Some(false), + _ => None, + }; + knob_overrides::FORCE_EVACUATE_TEST_OVERRIDE.with(|cell| match cell.replace(next) { + Some(true) => 1, + Some(false) => 0, + None => -1, + }) +} + #[cfg(test)] thread_local! { /// `PERRY_GC_SCAVENGE` — **ON by default since #7056**, kill switch diff --git a/crates/perry-runtime/src/module_require.rs b/crates/perry-runtime/src/module_require.rs index 015ccea408..97a31561ee 100644 --- a/crates/perry-runtime/src/module_require.rs +++ b/crates/perry-runtime/src/module_require.rs @@ -1265,10 +1265,15 @@ fn dynamic_import_javascript_data_url(specifier: &str) -> Option { .unwrap_or(expression.trim()); let scope = crate::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(crate::node_vm::eval_dynamic_module_expression(expression)); - let namespace = crate::object::js_object_alloc_null_proto(0, 1); - let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::object::js_object_set_field_by_name(namespace, key, value.get_nanbox_f64()); - Some(js_nanbox_pointer(namespace as i64)) + let namespace = scope.root_raw_mut_ptr(crate::object::js_object_alloc_null_proto(0, 1)); + let key = scope.root_string_ptr(js_string_from_bytes(name.as_ptr(), name.len() as u32)); + let namespace_value = namespace.with_mut_ptr::(|object| { + key.with_mut_ptr::(|key| { + crate::object::js_object_set_field_by_name(object, key, value.get_nanbox_f64()); + }); + js_nanbox_pointer(object as i64) + }); + Some(namespace_value) } /// Codegen entry for the unresolved / no-match dynamic-`import()` fallthrough diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index 6f8588d421..aee7a3b187 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -134,12 +134,14 @@ pub extern "C" fn js_event_emitter_async_resource_subclass_init(this: f64, optio crate::string::js_string_from_bytes(default_name.as_ptr(), default_name.len() as u32); name = f64::from_bits(JSValue::string_ptr(name_ptr).bits()); } + let name_handle = scope.root_nanbox_f64(name); let async_options = if options_value.is_any_string() { f64::from_bits(crate::value::TAG_UNDEFINED) } else { options_handle.get_nanbox_f64() }; - let resource = crate::async_hooks::js_async_resource_new(name, async_options); + let resource = + crate::async_hooks::js_async_resource_new(name_handle.get_nanbox_f64(), async_options); let obj = this_handle.get_nanbox_f64(); let raw = raw_ptr_from_value(obj); crate::async_hooks::js_async_resource_set_event_emitter(resource, raw as i64); diff --git a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs index f8b370a622..27ab7fa395 100644 --- a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs +++ b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs @@ -231,11 +231,6 @@ pub extern "C" fn js_node_stream_finished(args: *const crate::array::ArrayHeader if let Some(signal) = options_signal(options) { add_finished_signal_abort_listener(stream, signal, callback); } - if get_hidden_value(options, hidden_key(b"cleanup")) - .is_some_and(|v| crate::value::js_is_truthy(v) != 0) - { - add_finished_cleanup_completion_listener(stream, callback); - } f64::from_bits(TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/node_stream_dispatch.rs b/crates/perry-runtime/src/node_stream_dispatch.rs index 913fcb8e9e..150ebb834e 100644 --- a/crates/perry-runtime/src/node_stream_dispatch.rs +++ b/crates/perry-runtime/src/node_stream_dispatch.rs @@ -263,7 +263,9 @@ enum EventEmitterAsyncResourceBacking { } fn event_emitter_async_resource_backing(receiver: f64) -> Option { - let bits = receiver.to_bits(); + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let bits = receiver.get_nanbox_f64().to_bits(); if bits >> 48 == 0x7FFD { let handle = (bits & crate::value::POINTER_MASK) as i64; if crate::object::event_emitter_async_resource_handle_probe() @@ -273,8 +275,11 @@ fn event_emitter_async_resource_backing(receiver: f64) -> Option(|key| { + js_object_get_field_by_name_f64(raw as *const ObjectHeader, key) + }); if value.to_bits() >> 48 == 0x7FFD { let resource = (value.to_bits() & crate::value::POINTER_MASK) as i64; if crate::async_hooks::is_async_resource_handle(resource) { @@ -353,6 +358,8 @@ extern "C" fn ns_ee_async_resource_getter(closure: *const ClosureHeader) -> f64 /// EventEmitter methods remain available, while `emit` and the resource /// accessors enforce Node's private-brand receiver validation. pub(crate) unsafe fn install_event_emitter_async_resource_prototype(proto: *mut ObjectHeader) { + let scope = crate::gc::RuntimeHandleScope::new(); + let proto = scope.root_raw_mut_ptr(proto); crate::closure::js_register_closure_rest(ns_ee_async_resource_emit_rest as *const u8, 1); crate::closure::js_register_closure_arity(ns_ee_async_resource_destroy as *const u8, 0); crate::closure::js_register_closure_arity(ns_ee_async_resource_getter as *const u8, 0); @@ -360,16 +367,26 @@ pub(crate) unsafe fn install_event_emitter_async_resource_prototype(proto: *mut let install_method = |name: &str, function: *const u8| { let closure = js_closure_alloc(function, 1); crate::closure::js_closure_set_capture_ptr(closure, 0, crate::value::TAG_UNDEFINED as i64); - js_object_set_field_by_name( - proto, - hidden_key(name.as_bytes()), - f64::from_bits(JSValue::pointer(closure as *const u8).bits()), - ); - crate::object::set_builtin_property_attrs( - proto as usize, - name.to_string(), - crate::object::PropertyAttrs::new(true, false, true), - ); + let closure = scope.root_raw_mut_ptr(closure); + let key = scope.root_string_ptr(hidden_key(name.as_bytes())); + proto.with_mut_ptr::(|proto| { + key.with_const_ptr::(|key| { + closure.with_const_ptr::(|closure| { + js_object_set_field_by_name( + proto, + key, + f64::from_bits(JSValue::pointer(closure as *const u8).bits()), + ); + }); + }); + }); + proto.with_mut_ptr::(|proto| { + crate::object::set_builtin_property_attrs( + proto as usize, + name.to_string(), + crate::object::PropertyAttrs::new(true, false, true), + ); + }); }; install_method("emit", ns_ee_async_resource_emit_rest as *const u8); install_method("emitDestroy", ns_ee_async_resource_destroy as *const u8); @@ -382,17 +399,30 @@ pub(crate) unsafe fn install_event_emitter_async_resource_prototype(proto: *mut let closure = js_closure_alloc(ns_ee_async_resource_getter as *const u8, 2); crate::closure::js_closure_set_capture_ptr(closure, 0, crate::value::TAG_UNDEFINED as i64); crate::closure::js_closure_set_capture_ptr(closure, 1, operation); - let key = hidden_key(name.as_bytes()); - js_object_set_field_by_name(proto, key, f64::from_bits(crate::value::TAG_UNDEFINED)); - crate::object::set_builtin_accessor_descriptor( - proto as usize, - name.to_string(), - crate::object::AccessorDescriptor { - get: JSValue::pointer(closure as *const u8).bits(), - set: 0, - }, - crate::object::PropertyAttrs::new(true, false, true), - ); + let closure = scope.root_raw_mut_ptr(closure); + let key = scope.root_string_ptr(hidden_key(name.as_bytes())); + proto.with_mut_ptr::(|proto| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name( + proto, + key, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + }); + }); + proto.with_mut_ptr::(|proto| { + closure.with_const_ptr::(|closure| { + crate::object::set_builtin_accessor_descriptor( + proto as usize, + name.to_string(), + crate::object::AccessorDescriptor { + get: JSValue::pointer(closure as *const u8).bits(), + set: 0, + }, + crate::object::PropertyAttrs::new(true, false, true), + ); + }); + }); } } @@ -400,6 +430,9 @@ pub(crate) unsafe fn install_event_emitter_async_resource_instance_methods( obj: *mut ObjectHeader, this_value: f64, ) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj = scope.root_raw_mut_ptr(obj); + let this_value = scope.root_nanbox_f64(this_value); crate::closure::js_register_closure_rest(ns_ee_async_resource_emit_rest as *const u8, 1); crate::closure::js_register_closure_arity(ns_ee_async_resource_destroy as *const u8, 0); crate::closure::js_register_closure_arity(ns_ee_async_resource_getter as *const u8, 0); @@ -408,12 +441,24 @@ pub(crate) unsafe fn install_event_emitter_async_resource_instance_methods( ("emitDestroy", ns_ee_async_resource_destroy as *const u8), ] { let closure = js_closure_alloc(function, 1); - crate::closure::js_closure_set_capture_ptr(closure, 0, this_value.to_bits() as i64); - js_object_set_field_by_name( - obj, - hidden_key(name.as_bytes()), - f64::from_bits(JSValue::pointer(closure as *const u8).bits()), + crate::closure::js_closure_set_capture_ptr( + closure, + 0, + this_value.get_nanbox_f64().to_bits() as i64, ); + let closure = scope.root_raw_mut_ptr(closure); + let key = scope.root_string_ptr(hidden_key(name.as_bytes())); + obj.with_mut_ptr::(|obj| { + key.with_const_ptr::(|key| { + closure.with_const_ptr::(|closure| { + js_object_set_field_by_name( + obj, + key, + f64::from_bits(JSValue::pointer(closure as *const u8).bits()), + ); + }); + }); + }); } // Source-compiled subclasses are plain runtime objects rather than @@ -426,19 +471,32 @@ pub(crate) unsafe fn install_event_emitter_async_resource_instance_methods( ("asyncResource", 2), ] { let closure = js_closure_alloc(ns_ee_async_resource_getter as *const u8, 2); - crate::closure::js_closure_set_capture_ptr(closure, 0, this_value.to_bits() as i64); - crate::closure::js_closure_set_capture_ptr(closure, 1, operation); - let key = hidden_key(name.as_bytes()); - js_object_set_field_by_name(obj, key, f64::from_bits(crate::value::TAG_UNDEFINED)); - crate::object::set_builtin_accessor_descriptor( - obj as usize, - name.to_string(), - crate::object::AccessorDescriptor { - get: JSValue::pointer(closure as *const u8).bits(), - set: 0, - }, - crate::object::PropertyAttrs::new(true, false, true), + crate::closure::js_closure_set_capture_ptr( + closure, + 0, + this_value.get_nanbox_f64().to_bits() as i64, ); + crate::closure::js_closure_set_capture_ptr(closure, 1, operation); + let closure = scope.root_raw_mut_ptr(closure); + let key = scope.root_string_ptr(hidden_key(name.as_bytes())); + obj.with_mut_ptr::(|obj| { + key.with_const_ptr::(|key| { + js_object_set_field_by_name(obj, key, f64::from_bits(crate::value::TAG_UNDEFINED)); + }); + }); + obj.with_mut_ptr::(|obj| { + closure.with_const_ptr::(|closure| { + crate::object::set_builtin_accessor_descriptor( + obj as usize, + name.to_string(), + crate::object::AccessorDescriptor { + get: JSValue::pointer(closure as *const u8).bits(), + set: 0, + }, + crate::object::PropertyAttrs::new(true, false, true), + ); + }); + }); } } diff --git a/crates/perry-runtime/src/node_submodules/fs_promises.rs b/crates/perry-runtime/src/node_submodules/fs_promises.rs index abf3d92f4e..5b1a2251a7 100644 --- a/crates/perry-runtime/src/node_submodules/fs_promises.rs +++ b/crates/perry-runtime/src/node_submodules/fs_promises.rs @@ -129,12 +129,16 @@ pub(crate) extern "C" fn thunk_fs_promises_open( match catch_fs_promises_throw(|| { match unsafe { crate::fs::js_fs_filehandle_open_result(path, flags) } { Ok(handle) => { - let promise = crate::fs::promise_value_fs(handle); - let ids = crate::async_hooks::init_resource("FILEHANDLE", handle, true); + let scope = crate::gc::RuntimeHandleScope::new(); + let handle = scope.root_nanbox_f64(handle); + let promise = + scope.root_nanbox_f64(crate::fs::promise_value_fs(handle.get_nanbox_f64())); + let ids = + crate::async_hooks::init_resource("FILEHANDLE", handle.get_nanbox_f64(), true); // FILEHANDLE is owned by the public handle and remains live // after open; Node does not emit before/after/destroy for it. let _ = ids; - promise + promise.get_nanbox_f64() } Err(err_val) => crate::fs::promise_rejected_fs(err_val), } @@ -399,8 +403,11 @@ pub(crate) extern "C" fn thunk_fs_promises_opendir( ) -> f64 { match crate::fs::js_fs_opendir_value_with_path(path) { Ok(directory) => { - let _ = crate::async_hooks::init_resource("DIRHANDLE", directory, true); - crate::fs::promise_value_fs(directory) + let scope = crate::gc::RuntimeHandleScope::new(); + let directory = scope.root_nanbox_f64(directory); + let _ = + crate::async_hooks::init_resource("DIRHANDLE", directory.get_nanbox_f64(), true); + crate::fs::promise_value_fs(directory.get_nanbox_f64()) } Err(err) => crate::fs::promise_rejected_fs(err), } diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index b0927a44e9..e45fa4ecd8 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -344,7 +344,7 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { let raw = value_addr(value); let matched = if method == "AsyncResource" { crate::async_hooks::resolve_async_resource_handle(raw as i64).is_some() - || (raw >= crate::value::addr_class::HANDLE_BAND_MAX + || (crate::value::addr_class::is_plausible_heap_addr(raw) && ordinary_has_instance_prototype_walk(value, type_ref)) } else { let candidate = small_native_handle_id(value).unwrap_or(raw as i64); @@ -357,7 +357,7 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { }) }; native - || (raw >= crate::value::addr_class::HANDLE_BAND_MAX + || (crate::value::addr_class::is_plausible_heap_addr(raw) && ordinary_has_instance_prototype_walk(value, type_ref)) }; return f64::from_bits(if matched { diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs index 12c923bd97..7eaee3cd6a 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_a_c.rs @@ -155,8 +155,13 @@ pub(crate) unsafe fn nm_dispatch_async_hooks( ("async_hooks", "AsyncLocalStorage") | ("async_hooks", "AsyncResource") => { let message = format!("Class constructor {method_name} cannot be invoked without 'new'"); - let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = crate::error::js_typeerror_new(msg); + let scope = crate::gc::RuntimeHandleScope::new(); + let msg = scope.root_string_ptr(crate::string::js_string_from_bytes( + message.as_ptr(), + message.len() as u32, + )); + let err = msg + .with_mut_ptr::(|msg| crate::error::js_typeerror_new(msg)); crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) } ("async_hooks", "createHook") => { diff --git a/crates/perry-runtime/src/promise/assimilate.rs b/crates/perry-runtime/src/promise/assimilate.rs index 122aa716d7..e499d6f52d 100644 --- a/crates/perry-runtime/src/promise/assimilate.rs +++ b/crates/perry-runtime/src/promise/assimilate.rs @@ -270,16 +270,26 @@ extern "C" fn native_promise_adoption_job(closure: *const crate::closure::Closur Some((value, is_error)) => { // Null-closure AsyncStep = a pure propagation task: the runner // resolves/rejects `outer` with `value` on the next tick. + let scope = crate::gc::RuntimeHandleScope::new(); + let outer_handle = scope.root_raw_mut_ptr(outer); + let value_handle = scope.root_nanbox_f64(value); + let context = capture_context(); + let ((async_id, trigger_async_id), outer) = + outer_handle.across_mut::(|| { + outer_handle.with_mut_ptr::(|outer| unsafe { + ((*outer).async_id, (*outer).trigger_async_id) + }) + }); TASK_QUEUE.with(|q| { q.borrow_mut().push_back(Task::AsyncStep( std::ptr::null(), - value, + value_handle.get_nanbox_f64(), outer, is_error, - capture_context(), + context, std::ptr::null_mut(), - unsafe { (*outer).async_id }, - unsafe { (*outer).trigger_async_id }, + async_id, + trigger_async_id, )); }); crate::event_pump::js_notify_promise_progress(); diff --git a/crates/perry-runtime/src/promise/async_step.rs b/crates/perry-runtime/src/promise/async_step.rs index 3286ac0d4d..08af32de52 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -678,6 +678,14 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * } else { unsafe { ((*next).async_id, (*next).trigger_async_id) } }; + let next = { + let value = next_handle.get_nanbox_f64(); + if value.to_bits() == crate::value::TAG_UNDEFINED { + std::ptr::null_mut() + } else { + crate::value::js_nanbox_get_pointer(value) as *mut Promise + } + }; crate::r#box::retain_async_box_activation(trap.box_activation); TASK_QUEUE.with(|q| { q.borrow_mut().push_back(Task::AsyncStep( diff --git a/crates/perry-runtime/src/promise/microtasks.rs b/crates/perry-runtime/src/promise/microtasks.rs index db67ddb45a..12df3176fe 100644 --- a/crates/perry-runtime/src/promise/microtasks.rs +++ b/crates/perry-runtime/src/promise/microtasks.rs @@ -415,11 +415,14 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { CURRENT_MICROTASK_VALUE.with(|c| c.set(value)); CURRENT_MICROTASK_NEXT.with(|c| c.set((*promise).next)); crate::async_hooks::before_promise(async_id, trigger_async_id); - if !(*promise).next.is_null() { + let promise = rooted_promise(&task_promise_handle); + let value = task_value_handle.get_nanbox_f64(); + let next = (*promise).next; + if !next.is_null() { if is_fulfilled { - js_promise_resolve((*promise).next, value); + js_promise_resolve(next, value); } else { - js_promise_reject((*promise).next, value); + js_promise_reject(next, value); } } crate::async_hooks::after_promise(async_id); @@ -611,6 +614,8 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // source promise — now dispatch directly: invoke the // stored callback, propagate the result to `next`. if callback.is_null() { + let next = rooted_promise(&next_handle); + let value = value_handle.get_nanbox_f64(); if !next.is_null() { if is_fulfilled { js_promise_resolve(next, value); @@ -658,7 +663,7 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { } else { None }; - crate::v8::promise_hook_before(next); + crate::v8::promise_hook_before(rooted_promise(&next_handle)); let callback = rooted_closure(&callback_handle); let result = crate::closure::js_closure_call1(callback, value_handle.get_nanbox_f64()); @@ -780,6 +785,9 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // here with two fewer indirections (closure alloc + // closure call). if step_closure.is_null() { + crate::async_hooks::before_promise(step_async_id, step_trigger_id); + let next = rooted_promise(&next_handle); + let value = value_handle.get_nanbox_f64(); if !next.is_null() { if is_error { js_promise_reject(next, value); @@ -787,6 +795,7 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { js_promise_resolve(next, value); } } + crate::async_hooks::after_promise(step_async_id); restore_microtask_context(); if !box_activation.is_null() { pop_async_box_execution_ref(box_activation); diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index 1601cc99b7..9f9eb55cb2 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -1759,16 +1759,25 @@ extern "C" fn finally_passthrough_fulfill( // () => value)`, whose then-return propagation costs one more tick // than this passthrough's old direct `js_promise_resolve(next, v)`. // Settle `next` via a propagation task instead. + let scope = crate::gc::RuntimeHandleScope::new(); + let next_handle = scope.root_raw_mut_ptr(next); + let value_handle = scope.root_nanbox_f64(value); + let context = capture_context(); + let ((async_id, trigger_async_id), next) = next_handle.across_mut::(|| { + next_handle.with_mut_ptr::(|next| unsafe { + ((*next).async_id, (*next).trigger_async_id) + }) + }); TASK_QUEUE.with(|q| { q.borrow_mut().push_back(Task::AsyncStep( std::ptr::null(), - value, + value_handle.get_nanbox_f64(), next, false, - capture_context(), + context, std::ptr::null_mut(), - unsafe { (*next).async_id }, - unsafe { (*next).trigger_async_id }, + async_id, + trigger_async_id, )); }); crate::event_pump::js_notify_promise_progress(); @@ -1787,16 +1796,25 @@ extern "C" fn finally_passthrough_reject( let reason = js_closure_get_capture_f64(closure, 1); if !next.is_null() { // Same extra tick as the fulfilled passthrough (V8 hop parity). + let scope = crate::gc::RuntimeHandleScope::new(); + let next_handle = scope.root_raw_mut_ptr(next); + let reason_handle = scope.root_nanbox_f64(reason); + let context = capture_context(); + let ((async_id, trigger_async_id), next) = next_handle.across_mut::(|| { + next_handle.with_mut_ptr::(|next| unsafe { + ((*next).async_id, (*next).trigger_async_id) + }) + }); TASK_QUEUE.with(|q| { q.borrow_mut().push_back(Task::AsyncStep( std::ptr::null(), - reason, + reason_handle.get_nanbox_f64(), next, true, - capture_context(), + context, std::ptr::null_mut(), - unsafe { (*next).async_id }, - unsafe { (*next).trigger_async_id }, + async_id, + trigger_async_id, )); }); crate::event_pump::js_notify_promise_progress(); diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index ef88bdd357..db09ed5520 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -1084,20 +1084,30 @@ fn async_resource_handle_from_value(value: f64) -> Option { } fn set_handle_property(target: f64, key: f64, value: f64) -> Option { - if let Some(handle) = async_resource_handle_from_value(target) { - if unsafe { crate::symbol::js_is_symbol(key) } != 0 { - unsafe { crate::symbol::js_object_set_symbol_property(target, key, value) }; + let scope = crate::gc::RuntimeHandleScope::new(); + let target = scope.root_nanbox_f64(target); + let key = scope.root_nanbox_f64(key); + let value = scope.root_nanbox_f64(value); + if let Some(handle) = async_resource_handle_from_value(target.get_nanbox_f64()) { + if unsafe { crate::symbol::js_is_symbol(key.get_nanbox_f64()) } != 0 { + unsafe { + crate::symbol::js_object_set_symbol_property( + target.get_nanbox_f64(), + key.get_nanbox_f64(), + value.get_nanbox_f64(), + ) + }; return Some(true); } - let Some(name) = key_to_rust_string(key) else { + let Some(name) = key_to_rust_string(key.get_nanbox_f64()) else { return Some(false); }; - crate::object::handle_expando::handle_expando_set(handle, &name, value); + crate::object::handle_expando::handle_expando_set(handle, &name, value.get_nanbox_f64()); return Some(true); } - let handle = small_handle_from_value(target)?; - let Some(name) = key_to_rust_string(key) else { + let handle = small_handle_from_value(target.get_nanbox_f64())?; + let Some(name) = key_to_rust_string(key.get_nanbox_f64()) else { // A SYMBOL-keyed write on a small native handle (e.g. the // @hono/node-server `incoming[wrapBodyStream] = true` on the HTTP // IncomingMessage handle). The handle is not a heap ObjectHeader, so @@ -1106,14 +1116,20 @@ fn set_handle_property(target: f64, key: f64, value: f64) -> Option { // object) and report success. Returning `Some(false)` here made // strict-mode assignment throw `TypeError: Cannot assign to read only // property` and 500 every POST/PUT served by Hono's node adapter. - if unsafe { crate::symbol::js_is_symbol(key) } != 0 { - unsafe { crate::symbol::js_object_set_symbol_property(target, key, value) }; + if unsafe { crate::symbol::js_is_symbol(key.get_nanbox_f64()) } != 0 { + unsafe { + crate::symbol::js_object_set_symbol_property( + target.get_nanbox_f64(), + key.get_nanbox_f64(), + value.get_nanbox_f64(), + ) + }; return Some(true); } return Some(false); }; if let Some(dispatch) = crate::object::handle_property_set_dispatch() { - unsafe { dispatch(handle, name.as_ptr(), name.len(), value) }; + unsafe { dispatch(handle, name.as_ptr(), name.len(), value.get_nanbox_f64()) }; } Some(true) } diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 0697c30bf1..c703489592 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -602,12 +602,19 @@ fn set_timer_ref_state(id: i64, has_ref: bool) { } fn record_timer_handle_kind(id: i64, kind: CallbackTimerKind) { - TIMER_HANDLE_KINDS.lock().unwrap().insert(id, kind); + let mut kinds = TIMER_HANDLE_KINDS.lock().unwrap(); + if kinds.len() >= TIMER_REF_STATES_CAP && !kinds.contains_key(&id) { + if let Some(oldest) = kinds.keys().copied().min() { + kinds.remove(&oldest); + } + } + kinds.insert(id, kind); } /// Synthetic constructor object for `Timeout`/`Immediate` native handles. -/// Timer ids outlive queue removal, so the kind table intentionally retains -/// the entry after clear/fire just as Node retains the wrapper's prototype. +/// Timer ids outlive queue removal, so the kind table retains recent entries +/// after clear/fire just as Node retains the wrapper's prototype. The bounded +/// inventory avoids unbounded growth in long-running processes. pub(crate) fn timer_constructor_value(id: i64) -> Option { let kind = TIMER_HANDLE_KINDS.lock().unwrap().get(&id).copied()?; let name = match kind { diff --git a/crates/perry-stdlib/src/async_local_storage.rs b/crates/perry-stdlib/src/async_local_storage.rs index 56dc0c90cc..5fc45afd9e 100644 --- a/crates/perry-stdlib/src/async_local_storage.rs +++ b/crates/perry-stdlib/src/async_local_storage.rs @@ -41,8 +41,10 @@ unsafe fn validate_callback(callback: f64) -> *const ClosureHeader { } } let message = "callback is not a function"; + let scope = perry_runtime::gc::RuntimeHandleScope::new(); let msg = perry_runtime::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = perry_runtime::error::js_typeerror_new(msg); + let msg = scope.root_string_ptr(msg); + let err = msg.with_mut_ptr(|msg| perry_runtime::error::js_typeerror_new(msg)); perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(err as i64)) } @@ -85,8 +87,10 @@ impl AsyncLocalStorageHandle { fn throw_invalid_receiver() -> ! { let message = b"Value of \"this\" must be of type AsyncLocalStorage"; + let scope = perry_runtime::gc::RuntimeHandleScope::new(); let msg = perry_runtime::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let err = perry_runtime::error::js_typeerror_new(msg); + let msg = scope.root_string_ptr(msg); + let err = msg.with_mut_ptr(|msg| perry_runtime::error::js_typeerror_new(msg)); perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(err as i64)) } @@ -100,14 +104,17 @@ pub(crate) fn resolve_async_local_storage_handle(receiver: Handle) -> Option> 48 != 0x7FFD { return None; } @@ -123,21 +130,23 @@ pub extern "C" fn js_async_local_storage_subclass_init(this_value: f64) -> f64 { let scope = perry_runtime::gc::RuntimeHandleScope::new(); let this_handle = scope.root_nanbox_f64(this_value); let backing = js_async_local_storage_new(); - let current_this = this_handle.get_nanbox_f64(); - let raw = perry_runtime::value::js_nanbox_get_pointer(current_this) + let backing_value = scope.root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(backing)); + let raw = perry_runtime::value::js_nanbox_get_pointer(this_handle.get_nanbox_f64()) as *mut perry_runtime::object::ObjectHeader; if !raw.is_null() && perry_runtime::value::addr_class::is_above_handle_band(raw as usize) && perry_runtime::value::addr_class::is_valid_obj_ptr(raw as *const u8) { - let key = perry_runtime::string::js_string_from_bytes( + let key = scope.root_string_ptr(perry_runtime::string::js_string_from_bytes( SUBCLASS_BACKING_KEY.as_ptr(), SUBCLASS_BACKING_KEY.len() as u32, - ); + )); + let raw = perry_runtime::value::js_nanbox_get_pointer(this_handle.get_nanbox_f64()) + as *mut perry_runtime::object::ObjectHeader; perry_runtime::object::js_object_set_field_by_name( raw, - key, - perry_runtime::value::js_nanbox_pointer(backing), + key.get_raw_mut_ptr(), + backing_value.get_nanbox_f64(), ); for method in [ b"run".as_slice(), @@ -148,14 +157,16 @@ pub extern "C" fn js_async_local_storage_subclass_init(this_value: f64) -> f64 { ] { let value = crate::common::dispatch::unbound_async_local_storage_method(method); let value_handle = scope.root_nanbox_f64(value); - let key = - perry_runtime::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); + let key = scope.root_string_ptr(perry_runtime::string::js_string_from_bytes( + method.as_ptr(), + method.len() as u32, + )); let current_raw = perry_runtime::value::js_nanbox_get_pointer(this_handle.get_nanbox_f64()) as *mut perry_runtime::object::ObjectHeader; perry_runtime::object::js_object_set_field_by_name( current_raw, - key, + key.get_raw_mut_ptr(), value_handle.get_nanbox_f64(), ); } @@ -181,17 +192,24 @@ pub unsafe extern "C" fn js_async_local_storage_run( callback: f64, args_array: i64, ) -> f64 { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(receiver)); + let store = scope.root_nanbox_f64(store); + let callback = scope.root_nanbox_f64(callback); + let args_array = scope.root_raw_const_ptr(args_array as *const ArrayHeader); // Validate before mutating the async context so an invalid callback throws // without leaving a pushed store behind (#3092). - let cb = validate_callback(callback); + let _ = validate_callback(callback.get_nanbox_f64()); + let receiver = perry_runtime::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()); let handle = resolve_async_local_storage_handle(receiver).unwrap_or_else(|| throw_invalid_receiver()); // A context guard mirrors the pop below: if the callback throws, // `js_throw` applies the guard while unwinding so the catch site still // observes the pre-`run` store (#788, Node restores via try/finally). - js_async_context_als_run_enter(handle, store); - let result = call_with_forwarded_args(cb, args_array); + js_async_context_als_run_enter(handle, store.get_nanbox_f64()); + let cb = validate_callback(callback.get_nanbox_f64()); + let result = call_with_forwarded_args(cb, args_array.get_raw_const_ptr::() as i64); js_async_context_als_scope_leave(); result @@ -210,8 +228,12 @@ pub extern "C" fn js_async_local_storage_get_store(receiver: Handle) -> f64 { /// Push store onto stack (caller is responsible for cleanup) #[no_mangle] pub extern "C" fn js_async_local_storage_enter_with(receiver: Handle, store: f64) { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(receiver)); + let store = scope.root_nanbox_f64(store); + let receiver = perry_runtime::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()); if let Some(handle) = resolve_async_local_storage_handle(receiver) { - unsafe { js_async_context_als_enter_with(handle, store) }; + unsafe { js_async_context_als_enter_with(handle, store.get_nanbox_f64()) }; } } @@ -225,14 +247,20 @@ pub unsafe extern "C" fn js_async_local_storage_exit( callback: f64, args_array: i64, ) -> f64 { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(receiver)); + let callback = scope.root_nanbox_f64(callback); + let args_array = scope.root_raw_const_ptr(args_array as *const ArrayHeader); // Validate before clearing the context so an invalid callback throws // without disturbing the saved store (#3092). - let cb = validate_callback(callback); + let _ = validate_callback(callback.get_nanbox_f64()); + let receiver = perry_runtime::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()); let handle = resolve_async_local_storage_handle(receiver).unwrap_or_else(|| throw_invalid_receiver()); js_async_context_als_exit_enter(handle); - let result = call_with_forwarded_args(cb, args_array); + let cb = validate_callback(callback.get_nanbox_f64()); + let result = call_with_forwarded_args(cb, args_array.get_raw_const_ptr::() as i64); js_async_context_als_scope_leave(); diff --git a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs index 48605b8c3e..4a324364cd 100644 --- a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs +++ b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs @@ -55,7 +55,10 @@ extern "C" fn async_local_storage_unbound_method_thunk( let name_len = perry_runtime::closure::js_closure_get_capture_ptr(closure, 1) as usize; let name = std::slice::from_raw_parts(name_ptr as *const u8, name_len); let name_str = std::str::from_utf8(name).unwrap_or(""); - let receiver = perry_runtime::object::js_implicit_this_get(); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let rest = scope.root_nanbox_f64(rest); + let receiver_handle = scope.root_nanbox_f64(perry_runtime::object::js_implicit_this_get()); + let receiver = receiver_handle.get_nanbox_f64(); let receiver_raw = if receiver.to_bits() >> 48 == 0x7FFD { (receiver.to_bits() & POINTER_MASK_BITS) as i64 } else { @@ -71,8 +74,8 @@ extern "C" fn async_local_storage_unbound_method_thunk( return TAG_UNDEFINED_F64; } - let args_array = - perry_runtime::value::js_nanbox_get_pointer(rest) as *const perry_runtime::ArrayHeader; + let args_array = perry_runtime::value::js_nanbox_get_pointer(rest.get_nanbox_f64()) + as *const perry_runtime::ArrayHeader; let args = if args_array.is_null() { Vec::new() } else { @@ -88,6 +91,12 @@ extern "C" fn async_local_storage_unbound_method_thunk( if let Some(value) = dispatch_async_local_storage_method(receiver_raw, name_str, &args) { return value; } + let receiver = receiver_handle.get_nanbox_f64(); + let receiver_raw = if receiver.to_bits() >> 48 == 0x7FFD { + (receiver.to_bits() & POINTER_MASK_BITS) as i64 + } else { + 0 + }; // The remaining cases are invalid receivers. Brand-checked methods // throw; enterWith/disable already returned the deliberate no-op above. @@ -142,7 +151,10 @@ pub(crate) unsafe fn dispatch_async_local_storage_method( ) { return None; } + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let arg_handles = scope.root_nanbox_f64_slice(args); let handle = crate::async_local_storage::resolve_async_local_storage_handle(handle)?; + let args = perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); Some(match method { "getStore" => crate::async_local_storage::js_async_local_storage_get_store(handle), "run" if args.len() >= 2 => { @@ -152,6 +164,8 @@ pub(crate) unsafe fn dispatch_async_local_storage_method( } else { pack_args_array(rest) as i64 }; + let args = + perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); crate::async_local_storage::js_async_local_storage_run( handle, args[0], args[1], rest_array, ) @@ -168,6 +182,8 @@ pub(crate) unsafe fn dispatch_async_local_storage_method( } else { pack_args_array(rest) as i64 }; + let args = + perry_runtime::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); crate::async_local_storage::js_async_local_storage_exit(handle, args[0], rest_array) } "disable" => { diff --git a/crates/perry-stdlib/src/common/dispatch_http.rs b/crates/perry-stdlib/src/common/dispatch_http.rs index c72f963591..aae66413fd 100644 --- a/crates/perry-stdlib/src/common/dispatch_http.rs +++ b/crates/perry-stdlib/src/common/dispatch_http.rs @@ -163,6 +163,11 @@ pub(super) unsafe fn dispatch_client_incoming_method( event_ptr: *const perry_runtime::StringHeader, callback: i64, ) -> i64; + fn js_http_once( + handle: i64, + event_ptr: *const perry_runtime::StringHeader, + callback: i64, + ) -> i64; fn js_http_incoming_message_pipe(handle: i64, dest: f64) -> f64; } @@ -179,7 +184,7 @@ pub(super) unsafe fn dispatch_client_incoming_method( } self_ref } - "on" | "once" | "addListener" if args.len() >= 2 => { + "on" | "addListener" if args.len() >= 2 => { let event = (args[0].to_bits() & PTR_MASK) as *const perry_runtime::StringHeader; let callback = (args[1].to_bits() & PTR_MASK) as i64; unsafe { @@ -187,6 +192,14 @@ pub(super) unsafe fn dispatch_client_incoming_method( } self_ref } + "once" if args.len() >= 2 => { + let event = (args[0].to_bits() & PTR_MASK) as *const perry_runtime::StringHeader; + let callback = (args[1].to_bits() & PTR_MASK) as i64; + unsafe { + js_http_once(handle, event, callback); + } + self_ref + } // `res.pipe(dest)` — register the destination and return it (Node's // pipe-returns-destination contract; node-fetch reads the response // body via `res.pipe(new PassThrough())`). diff --git a/crates/perry-stdlib/src/tls/event_pump.rs b/crates/perry-stdlib/src/tls/event_pump.rs index 114cddbf5f..0d691ebabd 100644 --- a/crates/perry-stdlib/src/tls/event_pump.rs +++ b/crates/perry-stdlib/src/tls/event_pump.rs @@ -45,9 +45,15 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { .and_then(|per| per.remove("listening")) .unwrap_or_default() }; + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks: Vec<_> = callbacks + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); for cb in callbacks { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call0(cb); } } drain_once_listeners(server_id, "listening"); @@ -55,9 +61,15 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { PendingTlsEvent::ServerSecureConnection(server_id, socket_id) => { let socket = nanbox_handle(socket_id); for event_name in ["secureConnection", "connection"] { - for cb in listeners_for(server_id, event_name) { - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, socket); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks: Vec<_> = listeners_for(server_id, event_name) + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call1(cb, socket); } } drain_once_listeners(server_id, event_name); @@ -70,9 +82,15 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { .and_then(|per| per.remove("close")) .unwrap_or_default() }; + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks: Vec<_> = callbacks + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); for cb in callbacks { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call0(cb); } } servers().lock().unwrap().remove(&server_id); @@ -80,45 +98,76 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { once_flags().lock().unwrap().remove(&server_id); } PendingTlsEvent::ServerError(server_id, msg) => { - let err = build_error_object(&msg); - for cb in listeners_for(server_id, "error") { - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, err); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let err = scope.root_nanbox_f64(build_error_object(&msg)); + let callbacks: Vec<_> = listeners_for(server_id, "error") + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call1(cb, err.get_nanbox_f64()); } } drain_once_listeners(server_id, "error"); } PendingTlsEvent::ServerTlsClientError(server_id, socket_id, msg, code) => { - let err = build_error_object_with_code(&msg, code.as_deref()); - let socket = nanbox_handle(socket_id); - for cb in listeners_for(server_id, "tlsClientError") { - if cb != 0 { - js_closure_call2(cb as *const ClosureHeader, err, socket); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let err = + scope.root_nanbox_f64(build_error_object_with_code(&msg, code.as_deref())); + let socket = scope.root_nanbox_f64(nanbox_handle(socket_id)); + let callbacks: Vec<_> = listeners_for(server_id, "tlsClientError") + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call2(cb, err.get_nanbox_f64(), socket.get_nanbox_f64()); } } drain_once_listeners(server_id, "tlsClientError"); } PendingTlsEvent::SocketData(socket_id, bytes) => { - let data = buffer_from_bytes(&bytes); - for cb in listeners_for(socket_id, "data") { - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, data); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let data = scope.root_nanbox_f64(buffer_from_bytes(&bytes)); + let callbacks: Vec<_> = listeners_for(socket_id, "data") + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call1(cb, data.get_nanbox_f64()); } } drain_once_listeners(socket_id, "data"); } PendingTlsEvent::SocketEnd(socket_id) => { - for cb in listeners_for(socket_id, "end") { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks: Vec<_> = listeners_for(socket_id, "end") + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call0(cb); } } drain_once_listeners(socket_id, "end"); } PendingTlsEvent::SocketClose(socket_id) => { - for cb in listeners_for(socket_id, "close") { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks: Vec<_> = listeners_for(socket_id, "close") + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call0(cb); } } sockets().lock().unwrap().remove(&socket_id); @@ -126,10 +175,16 @@ pub unsafe extern "C" fn js_tls_process_pending() -> i32 { once_flags().lock().unwrap().remove(&socket_id); } PendingTlsEvent::SocketError(socket_id, msg) => { - let err = build_error_object(&msg); - for cb in listeners_for(socket_id, "error") { - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, err); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let err = scope.root_nanbox_f64(build_error_object(&msg)); + let callbacks: Vec<_> = listeners_for(socket_id, "error") + .into_iter() + .map(|cb| scope.root_raw_const_ptr(cb as *const ClosureHeader)) + .collect(); + for cb in callbacks { + let cb = cb.get_raw_const_ptr::(); + if !cb.is_null() { + js_closure_call1(cb, err.get_nanbox_f64()); } } drain_once_listeners(socket_id, "error"); diff --git a/crates/perry-stdlib/src/webcrypto/digest.rs b/crates/perry-stdlib/src/webcrypto/digest.rs index 235483ca7d..e56f1396de 100644 --- a/crates/perry-stdlib/src/webcrypto/digest.rs +++ b/crates/perry-stdlib/src/webcrypto/digest.rs @@ -52,14 +52,30 @@ pub unsafe extern "C" fn js_webcrypto_digest(algo_bits: f64, data_bits: f64) -> if buf.is_null() { return reject_with_dom_exception("OperationError", "The operation failed"); } - let value = f64::from_bits(JSValue::pointer(buf as *const u8).bits()); - let promise = perry_runtime::promise::js_promise_new(); - let promise_val = f64::from_bits(JSValue::pointer(promise as *const u8).bits()); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(f64::from_bits(JSValue::pointer(buf as *const u8).bits())); + let promise = scope.root_raw_mut_ptr(perry_runtime::promise::js_promise_new()); + let promise_val = promise.with_mut_ptr(|promise: *mut Promise| { + f64::from_bits(JSValue::pointer(promise as *const u8).bits()) + }); let cl = perry_runtime::closure::js_closure_alloc(webcrypto_digest_settle as *const u8, 3); - perry_runtime::closure::js_closure_set_capture_ptr(cl, 0, promise_val.to_bits() as i64); - perry_runtime::closure::js_closure_set_capture_ptr(cl, 1, value.to_bits() as i64); + let cl = scope.root_raw_mut_ptr(cl); + perry_runtime::closure::js_closure_set_capture_ptr( + cl.get_raw_mut_ptr(), + 0, + promise_val.to_bits() as i64, + ); + perry_runtime::closure::js_closure_set_capture_ptr( + cl.get_raw_mut_ptr(), + 1, + value.get_nanbox_f64().to_bits() as i64, + ); // Remaining macrotask hops (Node's threadpool digest = 2 setImmediate ticks). - perry_runtime::closure::js_closure_set_capture_ptr(cl, 2, 2); - perry_runtime::timer::schedule_native_callback(cl as i64, &[], "HASHREQUEST"); - promise + perry_runtime::closure::js_closure_set_capture_ptr(cl.get_raw_mut_ptr(), 2, 2); + perry_runtime::timer::schedule_native_callback( + cl.get_raw_mut_ptr::() as i64, + &[], + "HASHREQUEST", + ); + promise.get_raw_mut_ptr() } diff --git a/crates/perry-stdlib/src/webcrypto/hmac.rs b/crates/perry-stdlib/src/webcrypto/hmac.rs index ac7b6fc395..762efecdcc 100644 --- a/crates/perry-stdlib/src/webcrypto/hmac.rs +++ b/crates/perry-stdlib/src/webcrypto/hmac.rs @@ -271,7 +271,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( Err((name, message)) => return reject_with_dom_exception(name, message), }; if output_length == 0 { - return resolve_with_bool(false); + return resolve_with_bool_provider(false, "SIGNREQUEST"); } let customization = object_field_bytes(algo_bits.to_bits(), b"customization").unwrap_or_else(Vec::new); @@ -308,7 +308,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( }; let sig = match P256EcdsaSignature::from_slice(&provided_sig) { Ok(s) => s, - Err(_) => return resolve_with_bool(false), + Err(_) => return resolve_with_bool_provider(false, "SIGNREQUEST"), }; verifying_key.verify(&data_bytes, &sig).is_ok() } @@ -321,7 +321,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( }; let sig = match P384EcdsaSignature::from_slice(&provided_sig) { Ok(s) => s, - Err(_) => return resolve_with_bool(false), + Err(_) => return resolve_with_bool_provider(false, "SIGNREQUEST"), }; verifying_key.verify(&data_bytes, &sig).is_ok() } @@ -334,7 +334,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( }; let sig = match P521EcdsaSignature::from_slice(&provided_sig) { Ok(s) => s, - Err(_) => return resolve_with_bool(false), + Err(_) => return resolve_with_bool_provider(false, "SIGNREQUEST"), }; verifying_key.verify(&data_bytes, &sig).is_ok() } @@ -362,7 +362,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( }; let signature = match ed25519_dalek::Signature::try_from(provided_sig.as_slice()) { Ok(sig) => sig, - Err(_) => return resolve_with_bool(false), + Err(_) => return resolve_with_bool_provider(false, "SIGNREQUEST"), }; use ed25519_dalek::Verifier as _; verifying_key.verify(&data_bytes, &signature).is_ok() @@ -388,7 +388,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( }; let signature = match ed448_goldilocks::Signature::from_slice(&provided_sig) { Ok(sig) => sig, - Err(_) => return resolve_with_bool(false), + Err(_) => return resolve_with_bool_provider(false, "SIGNREQUEST"), }; verifying_key.verify_raw(&signature, &data_bytes).is_ok() } else if algo_upper == "RSASSA-PKCS1-V1_5" { diff --git a/crates/perry-stdlib/src/webcrypto/util.rs b/crates/perry-stdlib/src/webcrypto/util.rs index 258a90bebb..f647b7b5e2 100644 --- a/crates/perry-stdlib/src/webcrypto/util.rs +++ b/crates/perry-stdlib/src/webcrypto/util.rs @@ -962,11 +962,6 @@ pub(super) unsafe fn resolve_with_bytes_provider( resolve_with_bits_provider(val, provider_type) } -pub(super) unsafe fn resolve_with_bool(b: bool) -> *mut Promise { - let bits = if b { TAG_TRUE } else { TAG_FALSE }; - resolve_with_bits(bits) -} - pub(super) unsafe fn resolve_with_bool_provider( b: bool, provider_type: &'static str, diff --git a/crates/perry-stdlib/src/worker_threads/worker_pump.rs b/crates/perry-stdlib/src/worker_threads/worker_pump.rs index 814b3407a2..6e05f1d72e 100644 --- a/crates/perry-stdlib/src/worker_threads/worker_pump.rs +++ b/crates/perry-stdlib/src/worker_threads/worker_pump.rs @@ -237,10 +237,6 @@ fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { (worker.object_bits, callbacks, worker.async_resources) }; - for resource in async_resources { - perry_runtime::async_hooks::enter_resource_scope(resource); - } - // Web-style `addEventListener` listeners receive a `MessageEvent` wrapper // (with `.data`) for "message" events; Node-style `on` listeners receive the // raw payload. Lazily build the event object only if a web listener exists. @@ -260,6 +256,12 @@ fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { }) .collect::>(); let arg_handle = arg.map(|a| scope.root_nanbox_f64(a)); + let resource = match event { + "online" => async_resources[1], + "message" | "messageerror" => async_resources[2], + _ => async_resources[0], + }; + perry_runtime::async_hooks::enter_resource_scope(resource); let property_name = match event { "message" => Some("onmessage"), "error" => Some("onerror"), @@ -305,7 +307,5 @@ fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { perry_runtime::closure::js_closure_call0(closure); } } - for resource in async_resources.into_iter().rev() { - perry_runtime::async_hooks::leave_resource_scope(resource.async_id); - } + perry_runtime::async_hooks::leave_resource_scope(resource.async_id); } diff --git a/crates/perry-stdlib/src/zlib.rs b/crates/perry-stdlib/src/zlib.rs index 415b037903..0b4bd1902e 100644 --- a/crates/perry-stdlib/src/zlib.rs +++ b/crates/perry-stdlib/src/zlib.rs @@ -1354,6 +1354,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { if let Some(ids) = event_ids { perry_runtime::async_hooks::enter_resource_scope(ids); } + let mut destroy_after_dispatch = None; match ev { ZlibEvent::Data(id, bytes) => { publish_zlib_bytes_written(id); @@ -1395,6 +1396,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { } ZLIB_LISTENERS.lock().unwrap().remove(&id); ZLIB_STREAMS.lock().unwrap().remove(&id); + destroy_after_dispatch = event_ids.map(|ids| ids.async_id); } ZlibEvent::Callback(cb) => { if cb != 0 { @@ -1402,6 +1404,9 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { } } ZlibEvent::OneShotCallback(cb, result, ids) => { + // Node exposes two ZLIB provider phases for one-shot helpers: + // native compression completion, followed by delivery of the + // JavaScript callback. They intentionally share one resource. perry_runtime::async_hooks::run_resource_scope(ids, || {}); perry_runtime::async_hooks::enter_resource_scope(ids); if cb != 0 { @@ -1433,6 +1438,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { } } perry_runtime::async_hooks::leave_resource_scope(ids.async_id); + perry_runtime::async_hooks::defer_destroy_after_check_turns(ids.async_id, 4); } ZlibEvent::Error(id, msg) => { let err_f64 = build_zlib_error(&msg); @@ -1443,11 +1449,15 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { } ZLIB_LISTENERS.lock().unwrap().remove(&id); ZLIB_STREAMS.lock().unwrap().remove(&id); + destroy_after_dispatch = event_ids.map(|ids| ids.async_id); } } if let Some(ids) = event_ids { perry_runtime::async_hooks::leave_resource_scope(ids.async_id); } + if let Some(async_id) = destroy_after_dispatch { + perry_runtime::async_hooks::defer_destroy_after_check_turns(async_id, 4); + } } count } diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index ffa282300a..776e0e5c6a 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -51,7 +51,6 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[ "PERRY_ROOT_SPILL_RELOCATIONS", // #8583: selects the descriptor-backed lowering for large constant arrays. // The two paths emit different IR and therefore require distinct cache keys. - "PERRY_CONST_ARRAY_DESCRIPTOR", "PERRY_GC_SAFEPOINT_ONLY", "PERRY_INLINE_SHADOW_SLOT", "PERRY_DISABLE_BUFFER_FAST_PATH", @@ -143,7 +142,6 @@ const BUILD_CACHE_ENV_EXCLUSIONS: &[&str] = &[ "PERRY_REPSEL_DEBUG", "PERRY_STATEPOINT_REPORT", // Writes malformed dialect IR for diagnostics without changing emitted code. - "PERRY_DIALECT_DUMP", // `opt_report`'s own module doc states the contract this exclusion rests // on: "Observational only. Nothing in this module is read by codegen … // the returned fact sets are bit-identical with the report on and off, diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index 9036459752..e2cab550c5 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -955 +950 diff --git a/scripts/thread_local_cold_allowlist.json b/scripts/thread_local_cold_allowlist.json index 566ab8a28c..db158dedeb 100644 --- a/scripts/thread_local_cold_allowlist.json +++ b/scripts/thread_local_cold_allowlist.json @@ -1,6 +1,6 @@ { "_comment": "Files still declaring raw `thread_local!`. Every entry is a declaration that pays `_tlv_get_addr` on Darwin; the count is a ratchet, so adding one to an already-listed file fails too. New code should use `crate::perry_thread_local!` \u2014 see crates/perry-runtime/src/tls_hot.rs. Regenerate with scripts/check_thread_locals.py --update.", - "_hot_declarations": 259, + "_hot_declarations": 263, "files": { "crates/perry-runtime/src/agent.rs": 1, "crates/perry-runtime/src/arena/block.rs": 2, From c03773cd5f77cbc5d5d324e3e8be3bed7115331b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 25 Aug 2026 13:53:12 +0200 Subject: [PATCH 2/4] fix(async_hooks): address lifecycle review feedback --- changelog.d/8815-async-hooks-lifecycle.md | 3 + .../perry-codegen/src/expr/this_super_call.rs | 25 +- .../perry-codegen/src/lower_call/builtin.rs | 11 +- crates/perry-ext-events/src/lib.rs | 60 ++-- crates/perry-ext-http/src/lib.rs | 6 +- .../src/server/handle_dispatch.rs | 10 - crates/perry-ext-net/src/lib.rs | 6 +- crates/perry-ext-net/src/lifecycle.rs | 35 +- crates/perry-ext-zlib/src/stream.rs | 340 +++++++++++------- crates/perry-runtime/src/async_hooks.rs | 331 +++++++++++------ .../src/async_hooks/provider_ffi.rs | 152 ++++++-- .../src/async_hooks/test_support.rs | 47 +++ .../runtime_roots/hook_dispatch_handles.rs | 38 ++ crates/perry-stdlib/src/webcrypto/digest.rs | 4 +- .../src/worker_threads/worker_pump.rs | 90 ++--- crates/perry-stdlib/src/zlib.rs | 245 ++++++++----- scripts/thread_local_cold_allowlist.json | 2 +- .../integrations/events-emitter.ts | 15 + .../resource/shadowed-spread-parent.ts | 19 + 19 files changed, 986 insertions(+), 453 deletions(-) create mode 100644 changelog.d/8815-async-hooks-lifecycle.md create mode 100644 test-parity/node-suite/async_hooks/resource/shadowed-spread-parent.ts diff --git a/changelog.d/8815-async-hooks-lifecycle.md b/changelog.d/8815-async-hooks-lifecycle.md new file mode 100644 index 0000000000..1f1b1c9a50 --- /dev/null +++ b/changelog.d/8815-async-hooks-lifecycle.md @@ -0,0 +1,3 @@ +### Fixed + +- Completed Node `async_hooks` lifecycle support across async resources, event emitters, HTTP, sockets, workers, zlib, DNS, and WebCrypto. Provider scopes now restore execution and `AsyncLocalStorage` state when hooks or callbacks throw, deferred destroy hooks run at the correct lifecycle boundary, and allocation-sensitive values remain rooted across moving garbage collections. diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index ad8b47a3a8..48fdc8bdd9 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -261,7 +261,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let async_parent = ctx .classes .get(¤t_class_name) - .and_then(|class| class.extends_name.clone()); + .filter(|class| class.extends_expr.is_none() && !class.heritage_lexically_shadowed) + .and_then(|class| class.extends_name.clone()) + .filter(|parent| !ctx.classes.contains_key(parent.as_str())); if matches!( async_parent.as_deref(), Some("EventEmitterAsyncResource" | "AsyncLocalStorage" | "AsyncResource") @@ -269,15 +271,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); let zero_idx = "0".to_string(); let one_idx = "1".to_string(); - let first = - ctx.block() - .call(DOUBLE, "js_array_get_f64", &[(I64, &arr), (I32, &zero_idx)]); - let second = - ctx.block() - .call(DOUBLE, "js_array_get_f64", &[(I64, &arr), (I32, &one_idx)]); - rooting::with_rooted_group(ctx, 3, |ctx, group| { + rooting::with_rooted_group(ctx, 4, |ctx, group| { let this_root = group.adopt_emitted(ctx, Repr::Boxed, &this_box, true); + let arr_root = group.adopt_emitted(ctx, Repr::Ptr, &arr, true); + let arr = group.reread_emitted(ctx, arr_root); + let first = ctx.block().call( + DOUBLE, + "js_array_get_f64", + &[(I64, &arr), (I32, &zero_idx)], + ); let first_root = group.adopt_emitted(ctx, Repr::Boxed, &first, true); + let arr = group.reread_emitted(ctx, arr_root); + let second = ctx.block().call( + DOUBLE, + "js_array_get_f64", + &[(I64, &arr), (I32, &one_idx)], + ); let second_root = group.adopt_emitted(ctx, Repr::Boxed, &second, true); let this_box = group.reread_emitted(ctx, this_root); match async_parent.as_deref() { diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index 6e3e29f1ba..41c766ed3f 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -153,6 +153,7 @@ pub(super) fn lower_builtin_new<'a>( Some(index) => group.reread(ctx, index)?, None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), }; + let options = group.adopt_emitted(ctx, crate::rooting::Repr::Boxed, &options, true); let runtime = if import_src.is_some_and(|source| { source.strip_prefix("node:").unwrap_or(source) == "dns/promises" }) { @@ -163,12 +164,10 @@ pub(super) fn lower_builtin_new<'a>( ctx.pending_declares .push((runtime.to_string(), DOUBLE, vec![I64])); let zero = "0".to_string(); - let args_array = ctx.block().call(I64, "js_array_alloc", &[(I32, &zero)]); - let args_array = ctx.block().call( - I64, - "js_array_push_f64", - &[(I64, &args_array), (DOUBLE, &options)], - ); + let args_array = group.begin_array(ctx, &zero); + let options = group.reread_emitted(ctx, options); + group.push_array(ctx, args_array, &options); + let args_array = group.read_array(ctx, args_array); Ok(Some(ctx.block().call( DOUBLE, runtime, diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index cfeb124c79..169a35c312 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -23,7 +23,7 @@ use perry_ffi::{ error_value_with_code, js_array_alloc, js_array_get, js_array_length, js_array_push, js_array_set, js_object_alloc_with_shape, js_object_set_field, nanbox_string_bits, read_string, throw_with_code, ArrayHeader, ErrorKind, Handle, JsPromise, JsString, JsValue, ObjectHeader, - Promise, RawClosureHeader, StringHeader, TransientRootScope, + Promise, RawClosureHeader, StringHeader, TransientRootScope, TransientRootedAddr, }; use std::collections::{HashMap, HashSet}; use std::ffi::c_void; @@ -1296,16 +1296,18 @@ pub unsafe extern "C" fn js_event_emitter_emit( event_bits: i64, args_ptr: *mut ArrayHeader, ) -> f64 { - if event_name_from_bits(event_bits).is_none() { + let roots = TransientRootScope::enter(); + let args_ptr = roots.root_addr(args_ptr as i64); + let Some(event_name) = event_name_from_bits(event_bits) else { return f64::from_bits(0x7FFC_0000_0000_0003); - } + }; let async_id = event_emitter_async_id(handle); if async_id == 0 { - return js_event_emitter_emit_impl(handle, event_bits, args_ptr); + return js_event_emitter_emit_impl(handle, &event_name, args_ptr.get() as *mut ArrayHeader); } let mut call = EventEmitterEmitCall { handle, - event_bits, + event_name, args_ptr, }; js_async_hooks_provider_run_catching( @@ -1317,40 +1319,41 @@ pub unsafe extern "C" fn js_event_emitter_emit( struct EventEmitterEmitCall { handle: Handle, - event_bits: i64, - args_ptr: *mut ArrayHeader, + event_name: String, + args_ptr: TransientRootedAddr, } unsafe extern "C" fn event_emitter_emit_thunk(data: *mut std::ffi::c_void) -> f64 { let call = &mut *(data as *mut EventEmitterEmitCall); - js_event_emitter_emit_impl(call.handle, call.event_bits, call.args_ptr) + js_event_emitter_emit_impl( + call.handle, + &call.event_name, + call.args_ptr.get() as *mut ArrayHeader, + ) } unsafe fn js_event_emitter_emit_impl( handle: Handle, - event_bits: i64, + event_name: &str, 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) = event_name_from_bits(event_bits) else { - return TAG_FALSE_F64; - }; let mut had_listeners = false; 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) { + let snapshot: Vec = match emitter.events.get(event_name) { Some(v) if !v.is_empty() => v.clone(), _ => Vec::new(), }; if !snapshot.is_empty() { had_listeners = true; if snapshot.iter().any(|l| l.once) { - if let Some(v) = emitter.events.get_mut(&event_name) { + if let Some(v) = emitter.events.get_mut(event_name) { v.retain(|l| !l.once); } - emitter.prune_event_if_empty(&event_name); + emitter.prune_event_if_empty(event_name); } } @@ -1374,7 +1377,7 @@ unsafe fn js_event_emitter_emit_impl( } if domain_error.is_none() && throw_error.is_none() { - drain_pending_once_promises(emitter, &event_name, args_ptr); + drain_pending_once_promises(emitter, event_name, args_ptr); let capture_rejections = emitter.capture_rejections && event_name != "error"; for l in snapshot { @@ -1412,14 +1415,14 @@ unsafe fn js_event_emitter_emit_impl( /// `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_bits: i64) -> f64 { - if event_name_from_bits(event_bits).is_none() { + let Some(event_name) = event_name_from_bits(event_bits) else { return f64::from_bits(0x7FFC_0000_0000_0003); - } + }; let async_id = event_emitter_async_id(handle); if async_id == 0 { - return js_event_emitter_emit0_impl(handle, event_bits); + return js_event_emitter_emit0_impl(handle, &event_name); } - let mut call = EventEmitterEmit0Call { handle, event_bits }; + let mut call = EventEmitterEmit0Call { handle, event_name }; js_async_hooks_provider_run_catching( async_id, event_emitter_emit0_thunk, @@ -1429,35 +1432,32 @@ pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) struct EventEmitterEmit0Call { handle: Handle, - event_bits: i64, + event_name: String, } unsafe extern "C" fn event_emitter_emit0_thunk(data: *mut std::ffi::c_void) -> f64 { let call = &mut *(data as *mut EventEmitterEmit0Call); - js_event_emitter_emit0_impl(call.handle, call.event_bits) + js_event_emitter_emit0_impl(call.handle, &call.event_name) } -unsafe fn js_event_emitter_emit0_impl(handle: Handle, event_bits: i64) -> f64 { +unsafe fn js_event_emitter_emit0_impl(handle: Handle, event_name: &str) -> 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) = event_name_from_bits(event_bits) else { - return TAG_FALSE_F64; - }; let mut had_listeners = false; 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) { + let snapshot: Vec = match emitter.events.get(event_name) { Some(v) if !v.is_empty() => v.clone(), _ => Vec::new(), }; if !snapshot.is_empty() { had_listeners = true; if snapshot.iter().any(|l| l.once) { - if let Some(v) = emitter.events.get_mut(&event_name) { + if let Some(v) = emitter.events.get_mut(event_name) { v.retain(|l| !l.once); } - emitter.prune_event_if_empty(&event_name); + emitter.prune_event_if_empty(event_name); } } @@ -1480,7 +1480,7 @@ unsafe fn js_event_emitter_emit0_impl(handle: Handle, event_bits: i64) -> f64 { } } if domain_error.is_none() && throw_error.is_none() { - drain_pending_once_promises(emitter, &event_name, empty_args); + drain_pending_once_promises(emitter, event_name, empty_args); let capture_rejections = emitter.capture_rejections && event_name != "error"; for l in snapshot { diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index 1e357215ab..5e4407fdb2 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -1802,8 +1802,10 @@ pub unsafe extern "C" fn js_http_once( if callback == 0 { return handle; } + let roots = perry_ffi::TransientRootScope::enter(); + let callback = roots.root_addr(callback); let wrapper = - client_request_surface::create_client_once_wrapper(handle, &event, callback, false); + client_request_surface::create_client_once_wrapper(handle, &event, callback.get(), false); let mut matched = false; with_handle_mut::(handle, |request| { request @@ -1811,7 +1813,7 @@ pub unsafe extern "C" fn js_http_once( .entry(event.clone()) .or_default() .push(ClientEventListener { - callback, + callback: callback.get(), raw_wrapper: wrapper, once: true, }); diff --git a/crates/perry-ext-http/src/server/handle_dispatch.rs b/crates/perry-ext-http/src/server/handle_dispatch.rs index 86ad3ea08b..398677b020 100644 --- a/crates/perry-ext-http/src/server/handle_dispatch.rs +++ b/crates/perry-ext-http/src/server/handle_dispatch.rs @@ -126,8 +126,6 @@ extern "C" { fn js_node_http_im_resume(handle: i64); fn js_node_http_im_destroy(handle: i64); fn js_node_http_im_on(handle: i64, event_name_ptr: *const StringHeader, callback: i64) -> f64; - fn js_node_http_im_once(handle: i64, event_name_ptr: *const StringHeader, callback: i64) - -> f64; fn js_node_http_im_set_encoding(handle: i64, encoding_ptr: *const StringHeader) -> i64; fn js_node_http_im_set_timeout(handle: i64, msecs: f64, callback: i64) -> i64; fn js_node_http_im_read(handle: i64) -> f64; @@ -374,14 +372,6 @@ pub unsafe extern "C" fn js_ext_http_server_dispatch_method( } self_ref } - "once" if args.len() >= 2 => { - let event_ptr = string_arg(args[0]); - if event_ptr.is_null() { - return self_ref; - } - js_node_http_im_once(handle, event_ptr, closure_arg(Some(args[1]))); - self_ref - } "once" if args.len() >= 2 => { let event = read_string_header(string_arg(args[0]) as *mut StringHeader).unwrap_or_default(); diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 98ee742735..748ca0248f 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -352,9 +352,11 @@ enum PendingNetEvent { Data(i64, Bytes), /// Peer half-closed (FIN received); public readable-side `end` event. End(i64), - /// Writable-side shutdown requested by `socket.end()`, distinct from FIN; - /// fires the public `end` event. + /// A queued `socket.write` finished. `.1` is the completion token and + /// `.2` is the write error message when the write failed. WriteComplete(i64, u64, Option), + /// Writable-side shutdown requested by `socket.end()`, distinct from FIN. + /// `.1` is the completion token and `.2` is the shutdown error message. ShutdownComplete(i64, u64, Option), Close(i64), Error(i64, String), diff --git a/crates/perry-ext-net/src/lifecycle.rs b/crates/perry-ext-net/src/lifecycle.rs index 2dee4476c3..5b1ea5f512 100644 --- a/crates/perry-ext-net/src/lifecycle.rs +++ b/crates/perry-ext-net/src/lifecycle.rs @@ -84,10 +84,17 @@ pub(crate) unsafe fn dispatch_socket_completion(completion: u64, error: Option>(); + for completion in completions { + unsafe { + dispatch_socket_completion(completion, Some("Socket is closed".to_string())); + } + } } /// NaN-box a freshly allocated runtime string as an `f64` JS value. @@ -336,17 +343,26 @@ pub unsafe extern "C" fn js_ext_net_socket_write(handle: i64, chunk_bits: i64) { fn enqueue_socket_write(handle: i64, bytes: Vec, completion: u64) { let mut sockets = statics::sockets().lock().unwrap(); - if let Some(s) = sockets.get_mut(&handle) { + let failure = if let Some(s) = sockets.get_mut(&handle) { s.bytes_written = s.bytes_written.saturating_add(bytes.len() as u64); if s.cmd_tx .send(crate::SocketCommand::Write(bytes, completion)) .is_err() - && completion != 0 { - socket_completions().lock().unwrap().remove(&completion); + Some("Socket write failed") + } else { + None + } + } else { + Some("Socket is closed") + }; + drop(sockets); + if completion != 0 { + if let Some(message) = failure { + unsafe { + dispatch_socket_completion(completion, Some(message.to_string())); + } } - } else if completion != 0 { - socket_completions().lock().unwrap().remove(&completion); } } @@ -405,7 +421,10 @@ pub unsafe extern "C" fn js_ext_net_socket_write3( let completion = register_socket_completion(handle, completion); let Some(bytes) = crate::jsvalue_to_socket_bytes(chunk) else { if completion != 0 { - socket_completions().lock().unwrap().remove(&completion); + dispatch_socket_completion( + completion, + Some("Invalid data passed to socket.write".to_string()), + ); } return; }; diff --git a/crates/perry-ext-zlib/src/stream.rs b/crates/perry-ext-zlib/src/stream.rs index dc33937822..8cabbb3899 100644 --- a/crates/perry-ext-zlib/src/stream.rs +++ b/crates/perry-ext-zlib/src/stream.rs @@ -20,9 +20,10 @@ use perry_ffi::{ alloc_buffer, alloc_string, gc_register_mutable_root_scanner_named, notify_main_thread, BufferHeader, ErrorKind, GcRootVisitor, JsClosure, JsValue, RawClosureHeader, StringHeader, - TransientRootScope, + TransientRootScope, TransientRootedAddr, }; use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::c_void; use std::io::{Read, Write}; use std::sync::Mutex; @@ -68,9 +69,23 @@ extern "C" { // synchronously before queuing codec work. pub(crate) fn js_zlib_validate_callback(callback: f64) -> i64; fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64; - fn js_async_hooks_provider_defer_destroy(async_id: u64, check_turns: u32); - fn js_async_hooks_provider_enter(async_id: u64); - fn js_async_hooks_provider_leave(async_id: u64); + fn js_async_hooks_provider_run_catching( + async_id: u64, + callback: unsafe extern "C" fn(*mut c_void) -> f64, + data: *mut c_void, + ) -> f64; + fn js_async_hooks_provider_run_catching_deferred_destroy( + async_id: u64, + check_turns: u32, + callback: unsafe extern "C" fn(*mut c_void) -> f64, + data: *mut c_void, + ) -> f64; + fn js_async_hooks_provider_run_catching_deferred_destroy_on_error( + async_id: u64, + check_turns: u32, + callback: unsafe extern "C" fn(*mut c_void) -> f64, + data: *mut c_void, + ) -> f64; fn js_native_call_method_str_key( object: f64, name_handle: i64, @@ -685,17 +700,22 @@ unsafe fn call_one_shot_callback(callback: i64, result: Result, String>) if callback == 0 { return; } + let roots = TransientRootScope::enter(); + let callback = roots.root_addr(callback); match result { Ok(bytes) => { let err = f64::from_bits(JsValue::NULL.bits()); - let out = make_buffer_f64(&bytes) - .unwrap_or_else(|| f64::from_bits(JsValue::UNDEFINED.bits())); - let _ = JsClosure::from_raw(callback as *const RawClosureHeader).call2(err, out); + let out = roots.root_nanbox( + make_buffer_f64(&bytes) + .unwrap_or_else(|| f64::from_bits(JsValue::UNDEFINED.bits())), + ); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call2(err, out.get()); } Err(msg) => { - let err = build_error_object(&msg); - let _ = JsClosure::from_raw(callback as *const RawClosureHeader) - .call2(err, f64::from_bits(JsValue::UNDEFINED.bits())); + let err = roots.root_nanbox(build_error_object(&msg)); + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call2(err.get(), f64::from_bits(JsValue::UNDEFINED.bits())); } } } @@ -1316,6 +1336,121 @@ unsafe fn build_error_object(msg: &str) -> f64 { f64::from_bits(POINTER_TAG | (obj as u64 & POINTER_MASK)) } +struct ZlibEventDispatch { + event: Option, +} + +unsafe extern "C" fn zlib_event_dispatch_thunk(data: *mut c_void) -> f64 { + let call = &mut *(data as *mut ZlibEventDispatch); + let event = call + .event + .take() + .expect("zlib event dispatch thunk must run exactly once"); + match event { + ZlibEvent::Data(id, bytes) => { + publish_bytes_written(id); + let roots = TransientRootScope::enter(); + let callbacks = roots.root_addrs(&listeners_for(id, "data")); + let destinations = pipes_for(id) + .into_iter() + .map(|bits| roots.root_nanbox(f64::from_bits(bits))) + .collect::>(); + if callbacks.is_empty() && destinations.is_empty() { + buffer_output_for_late_consumer(&mut statics().lock().unwrap(), id, &bytes); + } else { + if !callbacks.is_empty() { + if let Some(buffer) = make_buffer_f64(&bytes) { + let buffer = roots.root_nanbox(buffer); + for callback in callbacks { + if callback.get() != 0 { + let _ = + JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call1(buffer.get()); + } + } + } + } + for destination in destinations { + forward_write(destination.get().to_bits(), &bytes); + } + } + } + ZlibEvent::Finish(id) => { + let roots = TransientRootScope::enter(); + for callback in roots.root_addrs(&listeners_for(id, "finish")) { + if callback.get() != 0 { + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call0(); + } + } + } + ZlibEvent::End(id) => { + publish_bytes_written(id); + let roots = TransientRootScope::enter(); + let end_callbacks = roots.root_addrs(&listeners_for(id, "end")); + let destinations = pipes_for(id) + .into_iter() + .map(|bits| roots.root_nanbox(f64::from_bits(bits))) + .collect::>(); + let close_callbacks = roots.root_addrs(&listeners_for(id, "close")); + drop_buffered_stream(&mut statics().lock().unwrap(), id); + for callback in end_callbacks { + if callback.get() != 0 { + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call0(); + } + } + for destination in destinations { + forward_end(destination.get().to_bits()); + } + for callback in close_callbacks { + if callback.get() != 0 { + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader).call0(); + } + } + } + ZlibEvent::Error(id, message) => { + let roots = TransientRootScope::enter(); + let callbacks = roots.root_addrs(&listeners_for(id, "error")); + drop_buffered_stream(&mut statics().lock().unwrap(), id); + let error = roots.root_nanbox(build_error_object(&message)); + for callback in callbacks { + if callback.get() != 0 { + let _ = JsClosure::from_raw(callback.get() as *const RawClosureHeader) + .call1(error.get()); + } + } + } + ZlibEvent::Callback(callback) => { + if callback != 0 { + let _ = JsClosure::from_raw(callback as *const RawClosureHeader).call0(); + } + } + ZlibEvent::OneShotCallback(_, _, _) => { + unreachable!("one-shot zlib events use the two-phase provider path") + } + } + f64::from_bits(UNDEFINED) +} + +unsafe extern "C" fn zlib_empty_phase_thunk(_data: *mut c_void) -> f64 { + f64::from_bits(UNDEFINED) +} + +struct ZlibOneShotDispatch { + callback: TransientRootedAddr, + result: Option, String>>, +} + +unsafe extern "C" fn zlib_one_shot_dispatch_thunk(data: *mut c_void) -> f64 { + let call = &mut *(data as *mut ZlibOneShotDispatch); + call_one_shot_callback( + call.callback.get(), + call.result + .take() + .expect("zlib one-shot dispatch thunk must run exactly once"), + ); + f64::from_bits(UNDEFINED) +} + /// Drain queued zlib stream events on the main thread. Wired into perry-stdlib's /// `js_stdlib_process_pending` via the external-zlib-pump feature. #[no_mangle] @@ -1345,133 +1480,78 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { }; count += 1; let event_async_id = event_stream_handle(&ev).map(stream_async_id).unwrap_or(0); - let mut destroy_after_dispatch = 0; - if event_async_id != 0 { - js_async_hooks_provider_enter(event_async_id); - } - match ev { - ZlibEvent::Data(id, bytes) => { - publish_bytes_written(id); - let cbs = listeners_for(id, "data"); - let dests = pipes_for(id); - if cbs.is_empty() && dests.is_empty() { - // No consumer attached yet — buffer instead of dropping, so a - // `.on('data')`/`.pipe()` that attaches later (after `await`) - // still receives the body (flushed by `flush_buffered`), - // bounded by the per-stream + global byte caps. - buffer_output_for_late_consumer(&mut statics().lock().unwrap(), id, &bytes); - } else { - if !cbs.is_empty() { - if let Some(buf_f64) = make_buffer_f64(&bytes) { - for cb in cbs { - if cb != 0 { - let _ = JsClosure::from_raw(cb as *const RawClosureHeader) - .call1(buf_f64); - } - } - } - } - for dest in dests { - forward_write(dest, &bytes); - } - } - } - ZlibEvent::Finish(id) => { - let scope = TransientRootScope::enter(); - let callbacks = scope.root_addrs(&listeners_for(id, "finish")); - for cb in callbacks { - let cb = cb.get(); - if cb != 0 { - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); - } - } - } - ZlibEvent::End(id) => { - publish_bytes_written(id); - // Defer `'end'` (keep the stream + its buffer alive) when no - // consumer has attached yet — otherwise removing the stream here - // would strand a `.on('data')`/`.on('end')` that attaches later - // (gaxios attaches them only after `await`ing the fetch), hanging - // the body-consume. `flush_buffered` re-queues End once a - // consumer attaches and the buffer has drained. - let has_consumer = - !listeners_for(id, "data").is_empty() || !pipes_for(id).is_empty(); - if !has_consumer { - let mut g = statics().lock().unwrap(); - let deferred = match g.streams.get_mut(&id) { - Some(s) => { - s.end_buffered = true; - true - } - None => false, - }; - if deferred { - // Cap how many never-consumed ended streams we retain so - // an abandoned handle (one that never gets a `'data'` - // listener or pipe) can't pin its buffered output for the - // process lifetime; drop the oldest excess. - evict_excess_buffered_ended(&mut g); - if event_async_id != 0 { - js_async_hooks_provider_leave(event_async_id); - } - continue; + if let ZlibEvent::End(id) = &ev { + // Defer `'end'` (keep the stream + its buffer alive) when no + // consumer has attached yet. Do this before entering the provider + // so a deferred stream does not emit a lifecycle phase prematurely. + let has_consumer = !listeners_for(*id, "data").is_empty() || !pipes_for(*id).is_empty(); + if !has_consumer { + let mut g = statics().lock().unwrap(); + let deferred = match g.streams.get_mut(id) { + Some(s) => { + s.end_buffered = true; + true } - // Stream already gone — release the lock and fall through to - // the (no-op) delivery + removal below. - drop(g); - } - for cb in listeners_for(id, "end") { - if cb != 0 { - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); - } - } - for dest in pipes_for(id) { - forward_end(dest); - } - for cb in listeners_for(id, "close") { - if cb != 0 { - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); - } - } - drop_buffered_stream(&mut statics().lock().unwrap(), id); - destroy_after_dispatch = event_async_id; - } - ZlibEvent::Callback(cb) => { - if cb != 0 { - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); + None => false, + }; + if deferred { + // Cap how many never-consumed ended streams we retain so + // an abandoned handle (one that never gets a `'data'` + // listener or pipe) can't pin its buffered output for the + // process lifetime; drop the oldest excess. + evict_excess_buffered_ended(&mut g); + continue; } + // Stream already gone — release the lock and fall through to + // the (no-op) delivery + removal below. + drop(g); } - ZlibEvent::OneShotCallback(cb, result, async_id) => { + } + + let ev = match ev { + ZlibEvent::OneShotCallback(callback, result, async_id) => { let scope = TransientRootScope::enter(); - let callback = scope.root_addr(cb); + let callback = scope.root_addr(callback); // Node exposes the native codec completion and delivery of the // JavaScript callback as two phases of the same ZLIB resource. - js_async_hooks_provider_enter(async_id); - js_async_hooks_provider_leave(async_id); - js_async_hooks_provider_enter(async_id); - call_one_shot_callback(callback.get(), result); - js_async_hooks_provider_leave(async_id); - // This is queued before the callback's Promise continuation - // schedules its first user immediate, so zlib needs one more - // check turn than synchronously closed handles. - js_async_hooks_provider_defer_destroy(async_id, 4); - } - ZlibEvent::Error(id, msg) => { - let err_f64 = build_error_object(&msg); - for cb in listeners_for(id, "error") { - if cb != 0 { - let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call1(err_f64); - } - } - drop_buffered_stream(&mut statics().lock().unwrap(), id); - destroy_after_dispatch = event_async_id; + js_async_hooks_provider_run_catching_deferred_destroy_on_error( + async_id, + 4, + zlib_empty_phase_thunk, + std::ptr::null_mut(), + ); + let mut call = ZlibOneShotDispatch { + callback, + result: Some(result), + }; + js_async_hooks_provider_run_catching_deferred_destroy( + async_id, + 4, + zlib_one_shot_dispatch_thunk, + &mut call as *mut ZlibOneShotDispatch as *mut c_void, + ); + continue; } - } - if event_async_id != 0 { - js_async_hooks_provider_leave(event_async_id); - } - if destroy_after_dispatch != 0 { - js_async_hooks_provider_defer_destroy(destroy_after_dispatch, 4); + event => event, + }; + + let terminal = matches!(&ev, ZlibEvent::End(_) | ZlibEvent::Error(_, _)); + let mut call = ZlibEventDispatch { event: Some(ev) }; + if event_async_id == 0 { + zlib_event_dispatch_thunk(&mut call as *mut ZlibEventDispatch as *mut c_void); + } else if terminal { + js_async_hooks_provider_run_catching_deferred_destroy( + event_async_id, + 4, + zlib_event_dispatch_thunk, + &mut call as *mut ZlibEventDispatch as *mut c_void, + ); + } else { + js_async_hooks_provider_run_catching( + event_async_id, + zlib_event_dispatch_thunk, + &mut call as *mut ZlibEventDispatch as *mut c_void, + ); } } count diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 961190f9c1..914898c9e6 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -24,7 +24,9 @@ pub use provider_ffi::{ defer_destroy_after_check_turns, js_async_hooks_provider_defer_destroy, js_async_hooks_provider_destroy, js_async_hooks_provider_enter, js_async_hooks_provider_init, js_async_hooks_provider_init_with_trigger, js_async_hooks_provider_leave, - js_async_hooks_provider_run_catching, js_async_hooks_provider_run_catching_with_this, + js_async_hooks_provider_run_catching, js_async_hooks_provider_run_catching_deferred_destroy, + js_async_hooks_provider_run_catching_deferred_destroy_on_error, + js_async_hooks_provider_run_catching_with_this, }; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; @@ -58,6 +60,8 @@ per_test_global! { pub static HOOKS_ACTIVE: AtomicUsize = AtomicUsize::new(0); static PROMISE_HOOKS_ACTIVE: AtomicUsize = AtomicUsize::new(0); static TOP_LEVEL_RESOURCE: AtomicU64 = AtomicU64::new(0); + #[cfg(test)] + static TEST_FORCE_RESOLVE_GC: AtomicUsize = AtomicUsize::new(0); } #[derive(Clone, Copy)] @@ -211,11 +215,18 @@ pub(crate) fn resolve_async_resource_handle(receiver: i64) -> Option { if !crate::value::addr_class::is_plausible_heap_addr(raw) { return None; } + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_raw_mut_ptr(raw as *mut ObjectHeader); + #[cfg(test)] + if TEST_FORCE_RESOLVE_GC.swap(0, Ordering::Relaxed) != 0 { + let _ = crate::gc::gc_collect_minor(); + } let key = js_string_from_bytes( ASYNC_RESOURCE_SUBCLASS_KEY.as_ptr(), ASYNC_RESOURCE_SUBCLASS_KEY.len() as u32, ); - let value = js_object_get_field_by_name(raw as *const ObjectHeader, key); + let value = receiver + .with_mut_ptr::(|receiver| js_object_get_field_by_name(receiver, key)); if !value.is_pointer() { return None; } @@ -223,6 +234,28 @@ pub(crate) fn resolve_async_resource_handle(receiver: i64) -> Option { is_async_resource_handle(backing).then_some(backing) } +#[cfg(test)] +pub(crate) fn test_force_next_async_resource_resolve_gc() { + TEST_FORCE_RESOLVE_GC.store(1, Ordering::Relaxed); +} + +#[cfg(test)] +pub(crate) fn test_link_async_resource_subclass(receiver: *mut ObjectHeader, backing: i64) { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_raw_mut_ptr(receiver); + let key = js_string_from_bytes( + ASYNC_RESOURCE_SUBCLASS_KEY.as_ptr(), + ASYNC_RESOURCE_SUBCLASS_KEY.len() as u32, + ); + receiver.with_mut_ptr::(|receiver| { + crate::object::js_object_set_field_by_name( + receiver, + key, + crate::value::js_nanbox_pointer(backing), + ); + }); +} + #[inline(always)] pub fn hooks_active() -> bool { HOOKS_ACTIVE.load(Ordering::Relaxed) != 0 @@ -889,16 +922,30 @@ pub fn run_provider_completion(type_name: &'static str, completion: impl FnOnce( true, ) }); - before(ids.async_id, ids.trigger_async_id); - let result = scope.root_nanbox_f64(completion()); - after(ids.async_id); - destroy(ids.async_id); + let outcome = try_run_resource_scope(ids, completion); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + let destroy_outcome = crate::exception::js_call_catching(|| { + destroy(ids.async_id); + TAG_UNDEFINED_F64 + }); + let destroy_error = destroy_outcome + .err() + .map(|error| scope.root_nanbox_f64(error)); + if threw { + crate::exception::js_throw(result.get_nanbox_f64()); + } + if let Some(error) = destroy_error { + crate::exception::js_throw(error.get_nanbox_f64()); + } result.get_nanbox_f64() } /// Enter an existing provider's captured AsyncLocalStorage and execution-id /// scope for one native callback phase. -pub fn enter_resource_scope(ids: AsyncResourceIds) { +pub fn try_enter_resource_scope(ids: AsyncResourceIds) -> Result<(), f64> { let context = RESOURCES .lock() .unwrap() @@ -909,25 +956,102 @@ pub fn enter_resource_scope(ids: AsyncResourceIds) { crate::async_context::push_context_guard( crate::async_context::ContextGuardAction::RestoreSnapshot(previous), ); - before(ids.async_id, ids.trigger_async_id); crate::async_context::push_context_guard( crate::async_context::ContextGuardAction::RestoreExecutionIds, ); + let outcome = crate::exception::js_call_catching(|| { + before(ids.async_id, ids.trigger_async_id); + TAG_UNDEFINED_F64 + }); + if let Err(error) = outcome { + let scope = crate::gc::RuntimeHandleScope::new(); + let error = scope.root_nanbox_f64(error); + if let Some(action) = crate::async_context::pop_context_guard() { + crate::async_context::apply_context_guard(action); + } + if let Some(action) = crate::async_context::pop_context_guard() { + crate::async_context::apply_context_guard(action); + } + return Err(error.get_nanbox_f64()); + } + Ok(()) +} + +pub fn enter_resource_scope(ids: AsyncResourceIds) { + if let Err(error) = try_enter_resource_scope(ids) { + crate::exception::js_throw(error); + } } /// Leave a provider scope entered by [`enter_resource_scope`]. -pub fn leave_resource_scope(async_id: u64) { - let _ = crate::async_context::pop_context_guard(); - after(async_id); +pub fn try_leave_resource_scope(async_id: u64) -> Result<(), f64> { + let outcome = crate::exception::js_call_catching(|| { + after(async_id); + TAG_UNDEFINED_F64 + }); + let scope = crate::gc::RuntimeHandleScope::new(); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + if let Some(action) = crate::async_context::pop_context_guard() { + if threw { + crate::async_context::apply_context_guard(action); + } + } if let Some(action) = crate::async_context::pop_context_guard() { crate::async_context::apply_context_guard(action); } + if threw { + Err(result.get_nanbox_f64()) + } else { + Ok(()) + } +} + +pub fn leave_resource_scope(async_id: u64) { + if let Err(error) = try_leave_resource_scope(async_id) { + crate::exception::js_throw(error); + } } pub fn run_resource_scope(ids: AsyncResourceIds, completion: impl FnOnce()) { - enter_resource_scope(ids); - completion(); - leave_resource_scope(ids.async_id); + let _ = run_resource_scope_catching(ids, || { + completion(); + TAG_UNDEFINED_F64 + }); +} + +/// Execute user code inside an existing provider and return its exception only +/// after the provider context and execution-id stacks have been restored. +pub fn try_run_resource_scope( + ids: AsyncResourceIds, + completion: impl FnOnce() -> f64, +) -> Result { + try_enter_resource_scope(ids)?; + let scope = crate::gc::RuntimeHandleScope::new(); + let outcome = crate::exception::js_call_catching(completion); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + let leave = try_leave_resource_scope(ids.async_id); + if let Err(error) = leave { + let error = scope.root_nanbox_f64(error); + return Err(error.get_nanbox_f64()); + } + if threw { + Err(result.get_nanbox_f64()) + } else { + Ok(result.get_nanbox_f64()) + } +} + +pub fn run_resource_scope_catching(ids: AsyncResourceIds, completion: impl FnOnce() -> f64) -> f64 { + match try_run_resource_scope(ids, completion) { + Ok(value) => value, + Err(error) => crate::exception::js_throw(error), + } } pub fn enqueue_gc_destroy(async_id: u64) { @@ -1269,13 +1393,15 @@ pub extern "C" fn js_async_resource_subclass_init( options_handle.get_nanbox_f64(), Some(this_handle.get_nanbox_f64()), ); - let current_this = this_handle.get_nanbox_f64(); - let raw = crate::value::js_nanbox_get_pointer(current_this) as *mut ObjectHeader; + let raw = + crate::value::js_nanbox_get_pointer(this_handle.get_nanbox_f64()) as *mut ObjectHeader; if !raw.is_null() && crate::value::addr_class::is_plausible_heap_addr(raw as usize) { let key = js_string_from_bytes( ASYNC_RESOURCE_SUBCLASS_KEY.as_ptr(), ASYNC_RESOURCE_SUBCLASS_KEY.len() as u32, ); + let raw = + crate::value::js_nanbox_get_pointer(this_handle.get_nanbox_f64()) as *mut ObjectHeader; crate::object::js_object_set_field_by_name( raw, key, @@ -1341,23 +1467,28 @@ extern "C" fn async_resource_bind_method_trampoline( return TAG_UNDEFINED_F64; } let handle = js_closure_get_capture_ptr(closure, 0); - let args_array = crate::value::js_nanbox_get_pointer(rest) as *const ArrayHeader; - let args_len = if args_array.is_null() { + let scope = crate::gc::RuntimeHandleScope::new(); + let args_array = + scope.root_raw_const_ptr(crate::value::js_nanbox_get_pointer(rest) as *const ArrayHeader); + let args_len = if args_array.get_raw_const_ptr::().is_null() { 0 } else { - js_array_length(args_array) + js_array_length(args_array.get_raw_const_ptr()) }; let callback = if args_len == 0 { TAG_UNDEFINED_F64 } else { - crate::array::js_array_get_f64(args_array, 0) + crate::array::js_array_get_f64(args_array.get_raw_const_ptr(), 0) }; + let callback = scope.root_nanbox_f64(callback); let this_arg = if args_len < 2 { TAG_UNDEFINED_F64 } else { - crate::array::js_array_get_f64(args_array, 1) + crate::array::js_array_get_f64(args_array.get_raw_const_ptr(), 1) }; - let bound = js_async_resource_bind(handle, callback, this_arg); + let this_arg = scope.root_nanbox_f64(this_arg); + let bound = + js_async_resource_bind(handle, callback.get_nanbox_f64(), this_arg.get_nanbox_f64()); if bound == 0 { TAG_UNDEFINED_F64 } else { @@ -1443,14 +1574,20 @@ pub fn try_async_resource_method_dispatch( unsafe { std::slice::from_raw_parts(args_ptr, args_len).to_vec() } }; let arg_handles = scope.root_nanbox_f64_slice(&raw_args); - let handle = resolve_async_resource_handle(receiver)?; + let receiver = scope.root_raw_mut_ptr(receiver as *mut ObjectHeader); + let handle = resolve_async_resource_handle(receiver.get_raw_mut_ptr::() as i64)?; + let handle = scope.root_raw_const_ptr(handle as *const AsyncResourceHandle); let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); Some(match method_name { - "asyncId" => js_async_resource_async_id(handle), - "triggerAsyncId" => js_async_resource_trigger_async_id(handle), + "asyncId" => { + js_async_resource_async_id(handle.get_raw_const_ptr::() as i64) + } + "triggerAsyncId" => js_async_resource_trigger_async_id( + handle.get_raw_const_ptr::() as i64, + ), "emitDestroy" => { - js_async_resource_emit_destroy(handle); - crate::value::js_nanbox_pointer(receiver) + js_async_resource_emit_destroy(handle.get_raw_const_ptr::() as i64); + crate::value::js_nanbox_pointer(receiver.get_raw_mut_ptr::() as i64) } "runInAsyncScope" => { // runInAsyncScope(fn[, thisArg, ...args]) @@ -1458,13 +1595,22 @@ pub fn try_async_resource_method_dispatch( let this_arg = args.get(1).copied().unwrap_or(TAG_UNDEFINED_F64); let rest = if args.len() > 2 { &args[2..] } else { &[] }; let args_array = pack_rest_args_array(rest); - js_async_resource_run_in_async_scope(handle, callback, this_arg, args_array) + js_async_resource_run_in_async_scope( + handle.get_raw_const_ptr::() as i64, + callback, + this_arg, + args_array, + ) } "bind" => { // bind(fn[, thisArg]) let callback = args.first().copied().unwrap_or(TAG_UNDEFINED_F64); let this_arg = args.get(1).copied().unwrap_or(TAG_UNDEFINED_F64); - let bound = js_async_resource_bind(handle, callback, this_arg); + let bound = js_async_resource_bind( + handle.get_raw_const_ptr::() as i64, + callback, + this_arg, + ); if bound == 0 { TAG_UNDEFINED_F64 } else { @@ -1527,83 +1673,58 @@ pub extern "C" fn js_async_resource_run_in_async_scope( this_arg: f64, args_array: i64, ) -> f64 { - let Some(handle) = resolve_async_resource_handle(handle) else { - return TAG_UNDEFINED_F64; - }; - if !is_callable_value(callback_value) { - throw_apply_not_function(callback_value); - } let scope = crate::gc::RuntimeHandleScope::new(); + let receiver_handle = scope.root_raw_mut_ptr(handle as *mut ObjectHeader); let callback_handle = scope.root_nanbox_f64(callback_value); let this_arg_handle = scope.root_nanbox_f64(this_arg); + let args_array_handle = scope.root_raw_const_ptr(args_array as *const ArrayHeader); + let receiver = receiver_handle.get_raw_mut_ptr::() as i64; + let Some(handle) = resolve_async_resource_handle(receiver) else { + return TAG_UNDEFINED_F64; + }; + let handle = scope.root_raw_const_ptr(handle as *const AsyncResourceHandle); + if !is_callable_value(callback_handle.get_nanbox_f64()) { + throw_apply_not_function(callback_handle.get_nanbox_f64()); + } + let ids = unsafe { (*handle.get_raw_const_ptr::()).ids }; let rebound_bits = crate::closure::clone_closure_rebind_this( callback_handle.get_nanbox_f64().to_bits(), this_arg_handle.get_nanbox_f64(), ); let rebound_handle = scope.root_nanbox_f64(f64::from_bits(rebound_bits)); - let callback = crate::fs::extract_closure_ptr(rebound_handle.get_nanbox_f64()); - if callback.is_null() { + if crate::fs::extract_closure_ptr(rebound_handle.get_nanbox_f64()).is_null() { throw_apply_not_function(callback_handle.get_nanbox_f64()); } - let args_array_handle = scope.root_raw_const_ptr(args_array as *const ArrayHeader); - let resource = unsafe { &*(handle as *const AsyncResourceHandle) }; - let resource_context = RESOURCES - .lock() - .unwrap() - .get(&resource.ids.async_id) - .map(|meta| meta.context.clone()) - .unwrap_or_default(); - let mut resource_context = resource_context; - let resource_context_roots = crate::async_context::root_snapshot(&scope, &resource_context); - let previous = crate::async_context::enter_context(&resource_context); - // The guard owns the previous snapshot: it is GC-scanned while held, and - // if the callback throws, `js_throw` restores it during unwind (#788). - crate::async_context::push_context_guard( - crate::async_context::ContextGuardAction::RestoreSnapshot(previous), - ); - before(resource.ids.async_id, resource.ids.trigger_async_id); - crate::async_context::push_context_guard( - crate::async_context::ContextGuardAction::RestoreExecutionIds, - ); - let prev_this = crate::object::js_implicit_this_set(this_arg_handle.get_nanbox_f64()); - // Catch locally so a throwing scope still delivers `after` and restores - // the resource/context before the exception is rethrown to user code. - // The trap is installed after our guards, so throw-time unwinding leaves - // those guards for the normal cleanup below. - let outcome = crate::exception::js_call_catching(|| { - if args_array == 0 { - unsafe { js_closure_call_array(callback as i64, ptr::null(), 0) } - } else { - let arr = args_array_handle.get_raw_const_ptr::(); - let len = js_array_length(arr) as i64; - let data = if arr.is_null() { - ptr::null() + let outcome = try_run_resource_scope(ids, || { + let callback = crate::fs::extract_closure_ptr(rebound_handle.get_nanbox_f64()); + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( + this_arg_handle.get_nanbox_f64(), + )); + let callback_outcome = crate::exception::js_call_catching(|| { + if args_array_handle + .get_raw_const_ptr::() + .is_null() + { + unsafe { js_closure_call_array(callback as i64, ptr::null(), 0) } } else { - unsafe { (arr as *const u8).add(std::mem::size_of::()) as *const f64 } - }; - unsafe { js_closure_call_array(callback as i64, data, len) } + let arr = args_array_handle.get_raw_const_ptr::(); + let len = js_array_length(arr) as i64; + let data = unsafe { + (arr as *const u8).add(std::mem::size_of::()) as *const f64 + }; + unsafe { js_closure_call_array(callback as i64, data, len) } + } + }); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); + match callback_outcome { + Ok(value) => value, + Err(error) => crate::exception::js_throw(error), } }); - crate::object::js_implicit_this_set(prev_this); - let threw = outcome.is_err(); - let result_handle = scope.root_nanbox_f64(match outcome { - Ok(result) | Err(result) => result, - }); - // Normal exit: `after` fires hooks and pops the execution scope itself, - // so discard the silent-unwind guard rather than applying it. - let _ = crate::async_context::pop_context_guard(); - after(resource.ids.async_id); - crate::async_context::refresh_snapshot_from_roots( - &mut resource_context, - &resource_context_roots, - ); - if let Some(action) = crate::async_context::pop_context_guard() { - crate::async_context::apply_context_guard(action); + match outcome { + Ok(value) => value, + Err(error) => crate::exception::js_throw(error), } - if threw { - crate::exception::js_throw(result_handle.get_nanbox_f64()); - } - result_handle.get_nanbox_f64() } /// Trampoline body for `AsyncResource#bind`. Stored as the `func_ptr` of the @@ -1645,20 +1766,28 @@ fn register_bind_trampoline_once() { #[no_mangle] pub extern "C" fn js_async_resource_bind(handle: i64, callback_value: f64, this_arg: f64) -> i64 { - validate_bind_callback(callback_value); - let Some(handle) = resolve_async_resource_handle(handle) else { - return 0; - }; - register_bind_trampoline_once(); let scope = crate::gc::RuntimeHandleScope::new(); + let receiver_handle = scope.root_raw_mut_ptr(handle as *mut ObjectHeader); let callback_handle = scope.root_nanbox_f64(callback_value); let this_arg_handle = scope.root_nanbox_f64(this_arg); + validate_bind_callback(callback_handle.get_nanbox_f64()); + let Some(handle) = + resolve_async_resource_handle(receiver_handle.get_raw_mut_ptr::() as i64) + else { + return 0; + }; + let handle = scope.root_raw_const_ptr(handle as *const AsyncResourceHandle); + register_bind_trampoline_once(); let closure = js_closure_alloc(async_resource_bind_trampoline as *const u8, 3); if closure.is_null() { return 0; } let closure_handle = scope.root_raw_mut_ptr(closure); - js_closure_set_capture_ptr(closure_handle.get_raw_mut_ptr(), 0, handle); + js_closure_set_capture_ptr( + closure_handle.get_raw_mut_ptr(), + 0, + handle.get_raw_const_ptr::() as i64, + ); js_closure_set_capture_f64( closure_handle.get_raw_mut_ptr(), 1, @@ -1669,9 +1798,9 @@ pub extern "C" fn js_async_resource_bind(handle: i64, callback_value: f64, this_ 2, this_arg_handle.get_nanbox_f64(), ); - if let Some(length) = - crate::closure::closure_length(crate::fs::extract_closure_ptr(callback_value)) - { + if let Some(length) = crate::closure::closure_length(crate::fs::extract_closure_ptr( + callback_handle.get_nanbox_f64(), + )) { crate::object::set_builtin_closure_length( closure_handle.get_raw_mut_ptr::() as usize, length, diff --git a/crates/perry-runtime/src/async_hooks/provider_ffi.rs b/crates/perry-runtime/src/async_hooks/provider_ffi.rs index b50e7f3657..7570dc316a 100644 --- a/crates/perry-runtime/src/async_hooks/provider_ffi.rs +++ b/crates/perry-runtime/src/async_hooks/provider_ffi.rs @@ -1,8 +1,8 @@ //! Exception-safe callback bridges for separately linked async providers. use super::{ - destroy, enter_resource_scope, init_resource, init_resource_with_trigger, leave_resource_scope, - AsyncResourceIds, RESOURCES, + destroy, init_resource, init_resource_with_trigger, try_enter_resource_scope, + try_leave_resource_scope, AsyncResourceIds, RESOURCES, }; extern "C" fn deferred_destroy_step(closure: *const crate::closure::ClosureHeader) -> f64 { @@ -45,6 +45,19 @@ pub fn defer_destroy_after_check_turns(async_id: u64, check_turns: u32) { } } +fn provider_ids(async_id: u64) -> AsyncResourceIds { + let trigger_async_id = RESOURCES + .lock() + .unwrap() + .get(&async_id) + .map(|meta| meta.trigger_async_id) + .unwrap_or(0); + AsyncResourceIds { + async_id, + trigger_async_id, + } +} + /// C ABI used by separately-linked native providers such as perry-ext-zlib. #[no_mangle] pub unsafe extern "C" fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64 { @@ -83,21 +96,16 @@ pub unsafe extern "C" fn js_async_hooks_provider_init_with_trigger( #[no_mangle] pub extern "C" fn js_async_hooks_provider_enter(async_id: u64) { - let trigger_async_id = RESOURCES - .lock() - .unwrap() - .get(&async_id) - .map(|meta| meta.trigger_async_id) - .unwrap_or(0); - enter_resource_scope(AsyncResourceIds { - async_id, - trigger_async_id, - }); + if let Err(error) = try_enter_resource_scope(provider_ids(async_id)) { + crate::exception::js_throw(error); + } } #[no_mangle] pub extern "C" fn js_async_hooks_provider_leave(async_id: u64) { - leave_resource_scope(async_id); + if let Err(error) = try_leave_resource_scope(async_id) { + crate::exception::js_throw(error); + } } #[no_mangle] @@ -120,17 +128,87 @@ pub unsafe extern "C" fn js_async_hooks_provider_run_catching( callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, data: *mut std::ffi::c_void, ) -> f64 { - js_async_hooks_provider_enter(async_id); - let outcome = crate::exception::js_call_catching(|| callback(data)); + provider_run_catching(async_id, DestroyPolicy::Never, callback, data) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum DestroyPolicy { + Never, + Always(u32), + OnError(u32), +} + +/// Variant used by terminal external-provider events. Teardown is scheduled +/// after scope restoration and before a caught JavaScript exception is +/// rethrown, so a throwing listener cannot strand the resource. +#[no_mangle] +pub unsafe extern "C" fn js_async_hooks_provider_run_catching_deferred_destroy( + async_id: u64, + check_turns: u32, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, +) -> f64 { + provider_run_catching(async_id, DestroyPolicy::Always(check_turns), callback, data) +} + +/// Schedule terminal teardown only when scope entry, the callback, or scope +/// exit throws. This lets a multi-phase provider protect an early phase while +/// leaving its normal destroy timing to the final phase. +#[no_mangle] +pub unsafe extern "C" fn js_async_hooks_provider_run_catching_deferred_destroy_on_error( + async_id: u64, + check_turns: u32, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, +) -> f64 { + provider_run_catching( + async_id, + DestroyPolicy::OnError(check_turns), + callback, + data, + ) +} + +unsafe fn provider_run_catching( + async_id: u64, + destroy_policy: DestroyPolicy, + callback: unsafe extern "C" fn(*mut std::ffi::c_void) -> f64, + data: *mut std::ffi::c_void, +) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); + if let Err(error) = try_enter_resource_scope(provider_ids(async_id)) { + let error = scope.root_nanbox_f64(error); + if let DestroyPolicy::Always(turns) | DestroyPolicy::OnError(turns) = destroy_policy { + defer_destroy_after_check_turns(async_id, turns); + } + crate::exception::js_throw(error.get_nanbox_f64()); + } + let outcome = crate::exception::js_call_catching(|| callback(data)); let (threw, result) = match outcome { Ok(value) => (false, scope.root_nanbox_f64(value)), Err(error) => (true, scope.root_nanbox_f64(error)), }; - js_async_hooks_provider_leave(async_id); + let leave = try_leave_resource_scope(async_id); + let (leave_threw, leave_result) = match leave { + Ok(()) => ( + false, + scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_UNDEFINED)), + ), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + if let DestroyPolicy::Always(turns) = destroy_policy { + defer_destroy_after_check_turns(async_id, turns); + } else if let DestroyPolicy::OnError(turns) = destroy_policy { + if threw || leave_threw { + defer_destroy_after_check_turns(async_id, turns); + } + } if threw { crate::exception::js_throw(result.get_nanbox_f64()); } + if leave_threw { + crate::exception::js_throw(leave_result.get_nanbox_f64()); + } result.get_nanbox_f64() } @@ -147,7 +225,16 @@ pub unsafe extern "C" fn js_async_hooks_provider_run_catching_with_this( ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); - js_async_hooks_provider_enter(async_id); + if let Err(error) = try_enter_resource_scope(provider_ids(async_id)) { + let error = scope.root_nanbox_f64(error); + if destroy_after != 0 { + let _ = crate::exception::js_call_catching(|| { + destroy(async_id); + f64::from_bits(crate::value::TAG_UNDEFINED) + }); + } + crate::exception::js_throw(error.get_nanbox_f64()); + } let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( this_value.get_nanbox_f64(), )); @@ -157,12 +244,35 @@ pub unsafe extern "C" fn js_async_hooks_provider_run_catching_with_this( Err(error) => (true, scope.root_nanbox_f64(error)), }; crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); - js_async_hooks_provider_leave(async_id); - if destroy_after != 0 { - js_async_hooks_provider_destroy(async_id); - } + let leave = try_leave_resource_scope(async_id); + let (leave_threw, leave_result) = match leave { + Ok(()) => ( + false, + scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_UNDEFINED)), + ), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + let destroy_outcome = (destroy_after != 0).then(|| { + crate::exception::js_call_catching(|| { + destroy(async_id); + f64::from_bits(crate::value::TAG_UNDEFINED) + }) + }); + let (destroy_threw, destroy_result) = match destroy_outcome { + Some(Err(error)) => (true, scope.root_nanbox_f64(error)), + _ => ( + false, + scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_UNDEFINED)), + ), + }; if threw { crate::exception::js_throw(result.get_nanbox_f64()); } + if leave_threw { + crate::exception::js_throw(leave_result.get_nanbox_f64()); + } + if destroy_threw { + crate::exception::js_throw(destroy_result.get_nanbox_f64()); + } result.get_nanbox_f64() } diff --git a/crates/perry-runtime/src/async_hooks/test_support.rs b/crates/perry-runtime/src/async_hooks/test_support.rs index be53907020..ce0ec4a56f 100644 --- a/crates/perry-runtime/src/async_hooks/test_support.rs +++ b/crates/perry-runtime/src/async_hooks/test_support.rs @@ -63,6 +63,26 @@ pub(crate) fn test_async_hooks_scanner_snapshot() -> (usize, u64) { mod tests { use super::*; + extern "C" fn throwing_lifecycle_hook(_closure: *const ClosureHeader, _async_id: f64) -> f64 { + crate::exception::js_throw(73.0) + } + + fn enable_throwing_lifecycle_hook(before_phase: bool) { + let callback = js_closure_alloc(throwing_lifecycle_hook as *const u8, 0); + let mut callbacks = HookCallbacks::empty(); + if before_phase { + callbacks.before = callback; + } else { + callbacks.after = callback; + } + HOOKS.lock().unwrap().push(HookRecord { + callbacks, + enabled: true, + track_promises: false, + }); + HOOKS_ACTIVE.store(1, Ordering::Relaxed); + } + // #7680: no lock needed here anymore. The per-test globals isolate this // thread's reset and resource-id sequence from concurrent tests. #[test] @@ -84,6 +104,33 @@ mod tests { assert_eq!(execution_async_id_u64(), 0); } + #[test] + fn resource_scope_restores_context_when_lifecycle_hooks_throw() { + const STORE: i64 = -9_401; + for before_phase in [true, false] { + reset_for_tests(); + crate::async_context::clear_store(STORE); + crate::async_context::enter_with(STORE, 11.0); + let ids = init_resource("throwing-scope", TAG_UNDEFINED_F64, true); + crate::async_context::enter_with(STORE, 22.0); + enable_throwing_lifecycle_hook(before_phase); + + let mut completion_ran = false; + let outcome = try_run_resource_scope(ids, || { + completion_ran = true; + TAG_UNDEFINED_F64 + }); + + assert_eq!(outcome.unwrap_err().to_bits(), 73.0f64.to_bits()); + assert_eq!(completion_ran, !before_phase); + assert_eq!(execution_async_id_u64(), 0); + assert!(EXECUTION_STACK.with(|stack| stack.borrow().is_empty())); + assert_eq!(crate::async_context::get_store(STORE), Some(22.0)); + crate::async_context::clear_store(STORE); + } + reset_for_tests(); + } + #[test] fn track_promises_filters_hooks_and_activity() { reset_for_tests(); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs index b8a1194344..29217da550 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs @@ -1,5 +1,43 @@ use super::*; +extern "C" fn test_current_async_id(_closure: *const crate::closure::ClosureHeader) -> f64 { + crate::async_hooks::execution_async_id_u64() as f64 +} + +#[test] +fn test_async_resource_subclass_run_in_scope_roots_inputs_during_key_alloc_gc() { + let _async_hook_guard = AsyncHookRuntimeTestGuard::new(); + let _guard = CopyingNurseryTestGuard::new(0); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); + let _verify_evacuation = crate::gc::knob_overrides::VerifyEvacuationTestGuard::on(); + register_runtime_handle_root_scanner_for_tests(); + + let resource_type = test_string_value(b"SubclassResource"); + let backing = crate::async_hooks::js_async_resource_new( + resource_type, + f64::from_bits(crate::value::TAG_UNDEFINED), + ); + let expected_async_id = crate::async_hooks::js_async_resource_async_id(backing); + let receiver = crate::object::js_object_alloc(0, 1); + crate::async_hooks::test_link_async_resource_subclass(receiver, backing); + let callback = crate::closure::js_closure_alloc(test_current_async_id as *const u8, 0); + + crate::async_hooks::test_force_next_async_resource_resolve_gc(); + let before = crate::gc::copying_minor_cycles(); + let result = crate::async_hooks::js_async_resource_run_in_async_scope( + receiver as i64, + f64::from_bits(ptr_bits(callback as usize)), + f64::from_bits(crate::value::TAG_UNDEFINED), + 0, + ); + let after = crate::gc::copying_minor_cycles(); + + assert!(after > before, "the resolver must complete a copying minor"); + assert_eq!(result, expected_async_id); + assert_eq!(crate::async_hooks::execution_async_id_u64(), 0); +} + #[test] fn test_async_hook_option_lookup_roots_callbacks_across_copied_minor_gc() { let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); diff --git a/crates/perry-stdlib/src/webcrypto/digest.rs b/crates/perry-stdlib/src/webcrypto/digest.rs index e56f1396de..d464638195 100644 --- a/crates/perry-stdlib/src/webcrypto/digest.rs +++ b/crates/perry-stdlib/src/webcrypto/digest.rs @@ -55,11 +55,11 @@ pub unsafe extern "C" fn js_webcrypto_digest(algo_bits: f64, data_bits: f64) -> let scope = perry_runtime::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(f64::from_bits(JSValue::pointer(buf as *const u8).bits())); let promise = scope.root_raw_mut_ptr(perry_runtime::promise::js_promise_new()); + let cl = perry_runtime::closure::js_closure_alloc(webcrypto_digest_settle as *const u8, 3); + let cl = scope.root_raw_mut_ptr(cl); let promise_val = promise.with_mut_ptr(|promise: *mut Promise| { f64::from_bits(JSValue::pointer(promise as *const u8).bits()) }); - let cl = perry_runtime::closure::js_closure_alloc(webcrypto_digest_settle as *const u8, 3); - let cl = scope.root_raw_mut_ptr(cl); perry_runtime::closure::js_closure_set_capture_ptr( cl.get_raw_mut_ptr(), 0, diff --git a/crates/perry-stdlib/src/worker_threads/worker_pump.rs b/crates/perry-stdlib/src/worker_threads/worker_pump.rs index 6e05f1d72e..2fe789e91f 100644 --- a/crates/perry-stdlib/src/worker_threads/worker_pump.rs +++ b/crates/perry-stdlib/src/worker_threads/worker_pump.rs @@ -261,51 +261,53 @@ fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { "message" | "messageerror" => async_resources[2], _ => async_resources[0], }; - perry_runtime::async_hooks::enter_resource_scope(resource); - let property_name = match event { - "message" => Some("onmessage"), - "error" => Some("onerror"), - "messageerror" => Some("onmessageerror"), - _ => None, - }; - let property_handler = property_name - .and_then(|name| object_event_handler(object_h.get_nanbox_f64().to_bits(), name)) - .map(|bits| scope.root_nanbox_f64(f64::from_bits(bits))); - let needs_event = property_handler.is_some() || callbacks.iter().any(|(_, web)| *web); - let event_handle = if needs_event { - let data = (event == "message") - .then(|| arg_handle.as_ref().map(|h| h.get_nanbox_f64())) - .flatten(); - let ev = event_object(event, object_h.get_nanbox_f64().to_bits(), data); - Some(scope.root_nanbox_f64(ev)) - } else { - None - }; - - if let (Some(callback_h), Some(event_h)) = (property_handler, event_handle.as_ref()) { - call_callback1( - callback_h.get_nanbox_f64().to_bits(), - object_h.get_nanbox_f64().to_bits(), - event_h.get_nanbox_f64(), - ); - } - - for (callback_h, web_event) in callbacks { - let closure_ptr = perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); - if closure_ptr == 0 { - continue; - } - let closure = closure_ptr as *const ClosureHeader; - let call_arg = if web_event { - event_handle.as_ref().map(|h| h.get_nanbox_f64()) - } else { - arg_handle.as_ref().map(|h| h.get_nanbox_f64()) + perry_runtime::async_hooks::run_resource_scope_catching(resource, || { + let property_name = match event { + "message" => Some("onmessage"), + "error" => Some("onerror"), + "messageerror" => Some("onmessageerror"), + _ => None, }; - if let Some(arg) = call_arg { - perry_runtime::closure::js_closure_call1(closure, arg); + let property_handler = property_name + .and_then(|name| object_event_handler(object_h.get_nanbox_f64().to_bits(), name)) + .map(|bits| scope.root_nanbox_f64(f64::from_bits(bits))); + let needs_event = property_handler.is_some() || callbacks.iter().any(|(_, web)| *web); + let event_handle = if needs_event { + let data = (event == "message") + .then(|| arg_handle.as_ref().map(|h| h.get_nanbox_f64())) + .flatten(); + let ev = event_object(event, object_h.get_nanbox_f64().to_bits(), data); + Some(scope.root_nanbox_f64(ev)) } else { - perry_runtime::closure::js_closure_call0(closure); + None + }; + + if let (Some(callback_h), Some(event_h)) = (property_handler, event_handle.as_ref()) { + call_callback1( + callback_h.get_nanbox_f64().to_bits(), + object_h.get_nanbox_f64().to_bits(), + event_h.get_nanbox_f64(), + ); } - } - perry_runtime::async_hooks::leave_resource_scope(resource.async_id); + + for (callback_h, web_event) in callbacks { + let closure_ptr = + perry_runtime::value::js_nanbox_get_pointer(callback_h.get_nanbox_f64()); + if closure_ptr == 0 { + continue; + } + let closure = closure_ptr as *const ClosureHeader; + let call_arg = if web_event { + event_handle.as_ref().map(|h| h.get_nanbox_f64()) + } else { + arg_handle.as_ref().map(|h| h.get_nanbox_f64()) + }; + if let Some(arg) = call_arg { + perry_runtime::closure::js_closure_call1(closure, arg); + } else { + perry_runtime::closure::js_closure_call0(closure); + } + } + js_undefined() + }); } diff --git a/crates/perry-stdlib/src/zlib.rs b/crates/perry-stdlib/src/zlib.rs index 0b4bd1902e..14e4aac6e6 100644 --- a/crates/perry-stdlib/src/zlib.rs +++ b/crates/perry-stdlib/src/zlib.rs @@ -1351,113 +1351,182 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { .and_then(|streams| streams.get(id).map(|stream| stream.async_ids)), _ => None, }; - if let Some(ids) = event_ids { - perry_runtime::async_hooks::enter_resource_scope(ids); - } - let mut destroy_after_dispatch = None; - match ev { - ZlibEvent::Data(id, bytes) => { - publish_zlib_bytes_written(id); - let cbs = listeners_for(id, "data"); - if !cbs.is_empty() { - if let Some(buf_f64) = make_buffer(&bytes) { - for cb in cbs { - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, buf_f64); + let destroy_after_dispatch = match (&ev, event_ids) { + (ZlibEvent::End(_) | ZlibEvent::Error(_, _), Some(ids)) => Some(ids.async_id), + _ => None, + }; + let dispatch = || { + match ev { + ZlibEvent::Data(id, bytes) => { + publish_zlib_bytes_written(id); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks = listeners_for(id, "data") + .into_iter() + .map(|callback| scope.root_raw_const_ptr(callback as *const ClosureHeader)) + .collect::>(); + let destinations = pipes_for(id) + .into_iter() + .map(|destination| scope.root_nanbox_f64(f64::from_bits(destination))) + .collect::>(); + if !callbacks.is_empty() { + if let Some(buf_f64) = make_buffer(&bytes) { + let buffer = scope.root_nanbox_f64(buf_f64); + for callback in callbacks { + let callback = callback.get_raw_const_ptr::(); + if !callback.is_null() { + js_closure_call1(callback, buffer.get_nanbox_f64()); + } } } } - } - // Fresh Buffer per pipe dest (the chunk lives in the owned - // `bytes`, so this is safe even after listener callbacks GC'd). - for dest in pipes_for(id) { - forward_write(dest, &bytes); - } - } - ZlibEvent::End(id) => { - publish_zlib_bytes_written(id); - for cb in listeners_for(id, "end") { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + // Fresh Buffer per pipe dest (the chunk lives in the owned + // `bytes`, so this is safe even after listener callbacks GC'd). + for destination in destinations { + forward_write(destination.get_nanbox_f64().to_bits(), &bytes); } } - for cb in listeners_for(id, "finish") { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + ZlibEvent::End(id) => { + publish_zlib_bytes_written(id); + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let end_callbacks = listeners_for(id, "end") + .into_iter() + .map(|callback| scope.root_raw_const_ptr(callback as *const ClosureHeader)) + .collect::>(); + let finish_callbacks = listeners_for(id, "finish") + .into_iter() + .map(|callback| scope.root_raw_const_ptr(callback as *const ClosureHeader)) + .collect::>(); + let destinations = pipes_for(id) + .into_iter() + .map(|destination| scope.root_nanbox_f64(f64::from_bits(destination))) + .collect::>(); + let close_callbacks = listeners_for(id, "close") + .into_iter() + .map(|callback| scope.root_raw_const_ptr(callback as *const ClosureHeader)) + .collect::>(); + ZLIB_LISTENERS.lock().unwrap().remove(&id); + ZLIB_STREAMS.lock().unwrap().remove(&id); + for callback in end_callbacks { + let callback = callback.get_raw_const_ptr::(); + if !callback.is_null() { + js_closure_call0(callback); + } } - } - for dest in pipes_for(id) { - forward_end(dest); - } - for cb in listeners_for(id, "close") { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + for callback in finish_callbacks { + let callback = callback.get_raw_const_ptr::(); + if !callback.is_null() { + js_closure_call0(callback); + } + } + for destination in destinations { + forward_end(destination.get_nanbox_f64().to_bits()); + } + for callback in close_callbacks { + let callback = callback.get_raw_const_ptr::(); + if !callback.is_null() { + js_closure_call0(callback); + } } } - ZLIB_LISTENERS.lock().unwrap().remove(&id); - ZLIB_STREAMS.lock().unwrap().remove(&id); - destroy_after_dispatch = event_ids.map(|ids| ids.async_id); - } - ZlibEvent::Callback(cb) => { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); + ZlibEvent::Callback(cb) => { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callback = scope.root_raw_const_ptr(cb as *const ClosureHeader); + let callback = callback.get_raw_const_ptr::(); + if !callback.is_null() { + js_closure_call0(callback); + } } - } - ZlibEvent::OneShotCallback(cb, result, ids) => { - // Node exposes two ZLIB provider phases for one-shot helpers: - // native compression completion, followed by delivery of the - // JavaScript callback. They intentionally share one resource. - perry_runtime::async_hooks::run_resource_scope(ids, || {}); - perry_runtime::async_hooks::enter_resource_scope(ids); - if cb != 0 { - match result { - Ok(bytes) => { - if let Some(buf_f64) = make_buffer(&bytes) { - js_closure_call2( - cb as *const ClosureHeader, - f64::from_bits(JSValue::null().bits()), - buf_f64, - ); - } else { - let err_f64 = build_zlib_error("Buffer allocation failed"); - js_closure_call2( - cb as *const ClosureHeader, - err_f64, - f64::from_bits(JSValue::undefined().bits()), - ); + ZlibEvent::OneShotCallback(cb, result, ids) => { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callback = scope.root_raw_const_ptr(cb as *const ClosureHeader); + // Node exposes two ZLIB provider phases for one-shot helpers: + // native compression completion, followed by delivery of the + // JavaScript callback. They intentionally share one resource. + let first_phase = + perry_runtime::async_hooks::try_run_resource_scope(ids, || { + f64::from_bits(JSValue::undefined().bits()) + }); + if let Err(error) = first_phase { + let error = scope.root_nanbox_f64(error); + perry_runtime::async_hooks::defer_destroy_after_check_turns( + ids.async_id, + 4, + ); + perry_runtime::exception::js_throw(error.get_nanbox_f64()); + } + let outcome = perry_runtime::async_hooks::try_run_resource_scope(ids, || { + if !callback.get_raw_const_ptr::().is_null() { + match result { + Ok(bytes) => { + if let Some(buf_f64) = make_buffer(&bytes) { + let buffer = scope.root_nanbox_f64(buf_f64); + js_closure_call2( + callback.get_raw_const_ptr::(), + f64::from_bits(JSValue::null().bits()), + buffer.get_nanbox_f64(), + ); + } else { + let error = scope.root_nanbox_f64(build_zlib_error( + "Buffer allocation failed", + )); + js_closure_call2( + callback.get_raw_const_ptr::(), + error.get_nanbox_f64(), + f64::from_bits(JSValue::undefined().bits()), + ); + } + } + Err(msg) => { + let error = scope.root_nanbox_f64(build_zlib_error(&msg)); + js_closure_call2( + callback.get_raw_const_ptr::(), + error.get_nanbox_f64(), + f64::from_bits(JSValue::undefined().bits()), + ); + } } } - Err(msg) => { - let err_f64 = build_zlib_error(&msg); - js_closure_call2( - cb as *const ClosureHeader, - err_f64, - f64::from_bits(JSValue::undefined().bits()), - ); - } + f64::from_bits(JSValue::undefined().bits()) + }); + let error = outcome.err().map(|error| scope.root_nanbox_f64(error)); + perry_runtime::async_hooks::defer_destroy_after_check_turns(ids.async_id, 4); + if let Some(error) = error { + perry_runtime::exception::js_throw(error.get_nanbox_f64()); } } - perry_runtime::async_hooks::leave_resource_scope(ids.async_id); - perry_runtime::async_hooks::defer_destroy_after_check_turns(ids.async_id, 4); - } - ZlibEvent::Error(id, msg) => { - let err_f64 = build_zlib_error(&msg); - for cb in listeners_for(id, "error") { - if cb != 0 { - js_closure_call1(cb as *const ClosureHeader, err_f64); + ZlibEvent::Error(id, msg) => { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let callbacks = listeners_for(id, "error") + .into_iter() + .map(|callback| scope.root_raw_const_ptr(callback as *const ClosureHeader)) + .collect::>(); + ZLIB_LISTENERS.lock().unwrap().remove(&id); + ZLIB_STREAMS.lock().unwrap().remove(&id); + let error = scope.root_nanbox_f64(build_zlib_error(&msg)); + for callback in callbacks { + let callback = callback.get_raw_const_ptr::(); + if !callback.is_null() { + js_closure_call1(callback, error.get_nanbox_f64()); + } } } - ZLIB_LISTENERS.lock().unwrap().remove(&id); - ZLIB_STREAMS.lock().unwrap().remove(&id); - destroy_after_dispatch = event_ids.map(|ids| ids.async_id); } - } - if let Some(ids) = event_ids { - perry_runtime::async_hooks::leave_resource_scope(ids.async_id); - } + f64::from_bits(JSValue::undefined().bits()) + }; + let outcome = match event_ids { + Some(ids) => perry_runtime::async_hooks::try_run_resource_scope(ids, dispatch), + None => Ok(dispatch()), + }; + let error_scope = perry_runtime::gc::RuntimeHandleScope::new(); + let error = outcome + .err() + .map(|error| error_scope.root_nanbox_f64(error)); if let Some(async_id) = destroy_after_dispatch { perry_runtime::async_hooks::defer_destroy_after_check_turns(async_id, 4); } + if let Some(error) = error { + perry_runtime::exception::js_throw(error.get_nanbox_f64()); + } } count } diff --git a/scripts/thread_local_cold_allowlist.json b/scripts/thread_local_cold_allowlist.json index db158dedeb..89f27d4433 100644 --- a/scripts/thread_local_cold_allowlist.json +++ b/scripts/thread_local_cold_allowlist.json @@ -1,6 +1,6 @@ { "_comment": "Files still declaring raw `thread_local!`. Every entry is a declaration that pays `_tlv_get_addr` on Darwin; the count is a ratchet, so adding one to an already-listed file fails too. New code should use `crate::perry_thread_local!` \u2014 see crates/perry-runtime/src/tls_hot.rs. Regenerate with scripts/check_thread_locals.py --update.", - "_hot_declarations": 263, + "_hot_declarations": 261, "files": { "crates/perry-runtime/src/agent.rs": 1, "crates/perry-runtime/src/arena/block.rs": 2, diff --git a/test-parity/node-suite/async_hooks/integrations/events-emitter.ts b/test-parity/node-suite/async_hooks/integrations/events-emitter.ts index 5f6409530d..ff84b7681e 100644 --- a/test-parity/node-suite/async_hooks/integrations/events-emitter.ts +++ b/test-parity/node-suite/async_hooks/integrations/events-emitter.ts @@ -30,3 +30,18 @@ await storage.run( ); console.log("events outside:", String(storage.getStore())); + +let eventNameConversions = 0; +const convertedName = { + toString() { + eventNameConversions += 1; + return "converted"; + }, +}; +const conversionEmitter = new EventEmitter(); +let convertedValue = "missing"; +conversionEmitter.on("converted", (value) => { + convertedValue = value; +}); +conversionEmitter.emit(convertedName as unknown as string, "value"); +console.log("event name conversion:", eventNameConversions, convertedValue); diff --git a/test-parity/node-suite/async_hooks/resource/shadowed-spread-parent.ts b/test-parity/node-suite/async_hooks/resource/shadowed-spread-parent.ts new file mode 100644 index 0000000000..f7d24fcc16 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/shadowed-spread-parent.ts @@ -0,0 +1,19 @@ +// A lexical class with a builtin-looking name must still run its own spread +// constructor path instead of being lowered as a native AsyncResource parent. +class AsyncResource { + readonly marker: string; + constructor(...values: string[]) { + this.marker = `user:${values.join(",")}`; + } +} + +class ShadowedResource extends AsyncResource { + constructor(...values: string[]) { + super(...values); + } +} + +console.log( + "shadowed spread parent:", + new ShadowedResource("first", "second").marker, +); From 56e18519ddbd4b4be9b7542f57aa134fc2f6c41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 25 Aug 2026 14:06:54 +0200 Subject: [PATCH 3/4] refactor(async_hooks): split resource scopes --- crates/perry-ext-net/src/lib.rs | 6 +- crates/perry-runtime/src/async_hooks.rs | 154 +----------------- .../perry-runtime/src/async_hooks/scopes.rs | 149 +++++++++++++++++ 3 files changed, 157 insertions(+), 152 deletions(-) create mode 100644 crates/perry-runtime/src/async_hooks/scopes.rs diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 748ca0248f..548c844021 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -352,11 +352,9 @@ enum PendingNetEvent { Data(i64, Bytes), /// Peer half-closed (FIN received); public readable-side `end` event. End(i64), - /// A queued `socket.write` finished. `.1` is the completion token and - /// `.2` is the write error message when the write failed. + /// A queued `socket.write` finished with a completion token and optional error. WriteComplete(i64, u64, Option), - /// Writable-side shutdown requested by `socket.end()`, distinct from FIN. - /// `.1` is the completion token and `.2` is the shutdown error message. + /// `socket.end()` writable shutdown with a completion token and optional error. ShutdownComplete(i64, u64, Option), Close(i64), Error(i64, String), diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 914898c9e6..c3375b09b6 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -28,6 +28,12 @@ pub use provider_ffi::{ js_async_hooks_provider_run_catching_deferred_destroy_on_error, js_async_hooks_provider_run_catching_with_this, }; +mod scopes; +pub use scopes::{ + enter_resource_scope, leave_resource_scope, run_provider_completion, run_resource_scope, + run_resource_scope_catching, try_enter_resource_scope, try_leave_resource_scope, + try_run_resource_scope, +}; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; @@ -906,154 +912,6 @@ pub fn destroy_promise(async_id: u64) { destroy_with_kind(async_id, true); } -/// Run a synchronous native completion as an observable async-hooks provider. -/// The operation may already have done its blocking work eagerly, but its -/// Promise settlement still needs the same provider execution/resource scope -/// Node gives a libuv completion. The returned JS value is rooted across hook -/// callbacks, which are arbitrary allocating user code. -pub fn run_provider_completion(type_name: &'static str, completion: impl FnOnce() -> f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let resource = crate::object::js_object_alloc_null_proto(0, 0); - let resource_handle = scope.root_raw_mut_ptr(resource); - let ids = resource_handle.with_mut_ptr::(|resource| { - init_resource( - type_name, - crate::value::js_nanbox_pointer(resource as i64), - true, - ) - }); - let outcome = try_run_resource_scope(ids, completion); - let (threw, result) = match outcome { - Ok(value) => (false, scope.root_nanbox_f64(value)), - Err(error) => (true, scope.root_nanbox_f64(error)), - }; - let destroy_outcome = crate::exception::js_call_catching(|| { - destroy(ids.async_id); - TAG_UNDEFINED_F64 - }); - let destroy_error = destroy_outcome - .err() - .map(|error| scope.root_nanbox_f64(error)); - if threw { - crate::exception::js_throw(result.get_nanbox_f64()); - } - if let Some(error) = destroy_error { - crate::exception::js_throw(error.get_nanbox_f64()); - } - result.get_nanbox_f64() -} - -/// Enter an existing provider's captured AsyncLocalStorage and execution-id -/// scope for one native callback phase. -pub fn try_enter_resource_scope(ids: AsyncResourceIds) -> Result<(), f64> { - let context = RESOURCES - .lock() - .unwrap() - .get(&ids.async_id) - .map(|meta| meta.context.clone()) - .unwrap_or_default(); - let previous = crate::async_context::enter_context(&context); - crate::async_context::push_context_guard( - crate::async_context::ContextGuardAction::RestoreSnapshot(previous), - ); - crate::async_context::push_context_guard( - crate::async_context::ContextGuardAction::RestoreExecutionIds, - ); - let outcome = crate::exception::js_call_catching(|| { - before(ids.async_id, ids.trigger_async_id); - TAG_UNDEFINED_F64 - }); - if let Err(error) = outcome { - let scope = crate::gc::RuntimeHandleScope::new(); - let error = scope.root_nanbox_f64(error); - if let Some(action) = crate::async_context::pop_context_guard() { - crate::async_context::apply_context_guard(action); - } - if let Some(action) = crate::async_context::pop_context_guard() { - crate::async_context::apply_context_guard(action); - } - return Err(error.get_nanbox_f64()); - } - Ok(()) -} - -pub fn enter_resource_scope(ids: AsyncResourceIds) { - if let Err(error) = try_enter_resource_scope(ids) { - crate::exception::js_throw(error); - } -} - -/// Leave a provider scope entered by [`enter_resource_scope`]. -pub fn try_leave_resource_scope(async_id: u64) -> Result<(), f64> { - let outcome = crate::exception::js_call_catching(|| { - after(async_id); - TAG_UNDEFINED_F64 - }); - let scope = crate::gc::RuntimeHandleScope::new(); - let (threw, result) = match outcome { - Ok(value) => (false, scope.root_nanbox_f64(value)), - Err(error) => (true, scope.root_nanbox_f64(error)), - }; - if let Some(action) = crate::async_context::pop_context_guard() { - if threw { - crate::async_context::apply_context_guard(action); - } - } - if let Some(action) = crate::async_context::pop_context_guard() { - crate::async_context::apply_context_guard(action); - } - if threw { - Err(result.get_nanbox_f64()) - } else { - Ok(()) - } -} - -pub fn leave_resource_scope(async_id: u64) { - if let Err(error) = try_leave_resource_scope(async_id) { - crate::exception::js_throw(error); - } -} - -pub fn run_resource_scope(ids: AsyncResourceIds, completion: impl FnOnce()) { - let _ = run_resource_scope_catching(ids, || { - completion(); - TAG_UNDEFINED_F64 - }); -} - -/// Execute user code inside an existing provider and return its exception only -/// after the provider context and execution-id stacks have been restored. -pub fn try_run_resource_scope( - ids: AsyncResourceIds, - completion: impl FnOnce() -> f64, -) -> Result { - try_enter_resource_scope(ids)?; - let scope = crate::gc::RuntimeHandleScope::new(); - let outcome = crate::exception::js_call_catching(completion); - let (threw, result) = match outcome { - Ok(value) => (false, scope.root_nanbox_f64(value)), - Err(error) => (true, scope.root_nanbox_f64(error)), - }; - let leave = try_leave_resource_scope(ids.async_id); - if let Err(error) = leave { - let error = scope.root_nanbox_f64(error); - return Err(error.get_nanbox_f64()); - } - if threw { - Err(result.get_nanbox_f64()) - } else { - Ok(result.get_nanbox_f64()) - } -} - -pub fn run_resource_scope_catching(ids: AsyncResourceIds, completion: impl FnOnce() -> f64) -> f64 { - match try_run_resource_scope(ids, completion) { - Ok(value) => value, - Err(error) => crate::exception::js_throw(error), - } -} - pub fn enqueue_gc_destroy(async_id: u64) { if async_id != 0 { GC_DESTROY_QUEUE.lock().unwrap().push_back(async_id); diff --git a/crates/perry-runtime/src/async_hooks/scopes.rs b/crates/perry-runtime/src/async_hooks/scopes.rs new file mode 100644 index 0000000000..a2144bfd9f --- /dev/null +++ b/crates/perry-runtime/src/async_hooks/scopes.rs @@ -0,0 +1,149 @@ +//! Exception-safe entry and cleanup for async-resource execution scopes. + +use super::{after, before, destroy, init_resource, AsyncResourceIds, RESOURCES}; + +const TAG_UNDEFINED_F64: f64 = f64::from_bits(crate::value::TAG_UNDEFINED); + +/// Run a synchronous native completion as an observable async-hooks provider. +/// The returned value stays rooted while arbitrary JavaScript hooks run. +pub fn run_provider_completion(type_name: &'static str, completion: impl FnOnce() -> f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let resource = crate::object::js_object_alloc_null_proto(0, 0); + let resource_handle = scope.root_raw_mut_ptr(resource); + let ids = resource_handle.with_mut_ptr::(|resource| { + init_resource( + type_name, + crate::value::js_nanbox_pointer(resource as i64), + true, + ) + }); + let outcome = try_run_resource_scope(ids, completion); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + let destroy_outcome = crate::exception::js_call_catching(|| { + destroy(ids.async_id); + TAG_UNDEFINED_F64 + }); + let destroy_error = destroy_outcome + .err() + .map(|error| scope.root_nanbox_f64(error)); + if threw { + crate::exception::js_throw(result.get_nanbox_f64()); + } + if let Some(error) = destroy_error { + crate::exception::js_throw(error.get_nanbox_f64()); + } + result.get_nanbox_f64() +} + +/// Enter an existing provider's captured AsyncLocalStorage and execution-id +/// scope for one native callback phase. +pub fn try_enter_resource_scope(ids: AsyncResourceIds) -> Result<(), f64> { + let context = RESOURCES + .lock() + .unwrap() + .get(&ids.async_id) + .map(|meta| meta.context.clone()) + .unwrap_or_default(); + let previous = crate::async_context::enter_context(&context); + crate::async_context::push_context_guard( + crate::async_context::ContextGuardAction::RestoreSnapshot(previous), + ); + crate::async_context::push_context_guard( + crate::async_context::ContextGuardAction::RestoreExecutionIds, + ); + let outcome = crate::exception::js_call_catching(|| { + before(ids.async_id, ids.trigger_async_id); + TAG_UNDEFINED_F64 + }); + if let Err(error) = outcome { + let scope = crate::gc::RuntimeHandleScope::new(); + let error = scope.root_nanbox_f64(error); + if let Some(action) = crate::async_context::pop_context_guard() { + crate::async_context::apply_context_guard(action); + } + if let Some(action) = crate::async_context::pop_context_guard() { + crate::async_context::apply_context_guard(action); + } + return Err(error.get_nanbox_f64()); + } + Ok(()) +} + +pub fn enter_resource_scope(ids: AsyncResourceIds) { + if let Err(error) = try_enter_resource_scope(ids) { + crate::exception::js_throw(error); + } +} + +/// Leave a provider scope entered by [`enter_resource_scope`]. +pub fn try_leave_resource_scope(async_id: u64) -> Result<(), f64> { + let outcome = crate::exception::js_call_catching(|| { + after(async_id); + TAG_UNDEFINED_F64 + }); + let scope = crate::gc::RuntimeHandleScope::new(); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + if let Some(action) = crate::async_context::pop_context_guard() { + if threw { + crate::async_context::apply_context_guard(action); + } + } + if let Some(action) = crate::async_context::pop_context_guard() { + crate::async_context::apply_context_guard(action); + } + if threw { + Err(result.get_nanbox_f64()) + } else { + Ok(()) + } +} + +pub fn leave_resource_scope(async_id: u64) { + if let Err(error) = try_leave_resource_scope(async_id) { + crate::exception::js_throw(error); + } +} + +pub fn run_resource_scope(ids: AsyncResourceIds, completion: impl FnOnce()) { + let _ = run_resource_scope_catching(ids, || { + completion(); + TAG_UNDEFINED_F64 + }); +} + +/// Execute user code inside an existing provider and return its exception only +/// after the provider context and execution-id stacks have been restored. +pub fn try_run_resource_scope( + ids: AsyncResourceIds, + completion: impl FnOnce() -> f64, +) -> Result { + try_enter_resource_scope(ids)?; + let scope = crate::gc::RuntimeHandleScope::new(); + let outcome = crate::exception::js_call_catching(completion); + let (threw, result) = match outcome { + Ok(value) => (false, scope.root_nanbox_f64(value)), + Err(error) => (true, scope.root_nanbox_f64(error)), + }; + if let Err(error) = try_leave_resource_scope(ids.async_id) { + let error = scope.root_nanbox_f64(error); + return Err(error.get_nanbox_f64()); + } + if threw { + Err(result.get_nanbox_f64()) + } else { + Ok(result.get_nanbox_f64()) + } +} + +pub fn run_resource_scope_catching(ids: AsyncResourceIds, completion: impl FnOnce() -> f64) -> f64 { + match try_run_resource_scope(ids, completion) { + Ok(value) => value, + Err(error) => crate::exception::js_throw(error), + } +} From 404310cfb1a92e64237552b1f4f716176410afdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 25 Aug 2026 14:26:35 +0200 Subject: [PATCH 4/4] fix(net): account only accepted socket writes --- crates/perry-ext-net/src/adopt.rs | 1 + crates/perry-ext-net/src/ipc.rs | 2 + crates/perry-ext-net/src/lib.rs | 30 +++--- crates/perry-ext-net/src/lifecycle.rs | 93 +++++++++++++++++-- crates/perry-ext-net/src/server_state.rs | 1 + .../providers/net-write-callbacks.ts | 1 + 6 files changed, 105 insertions(+), 23 deletions(-) diff --git a/crates/perry-ext-net/src/adopt.rs b/crates/perry-ext-net/src/adopt.rs index d3d8f331ad..e972f07c91 100644 --- a/crates/perry-ext-net/src/adopt.rs +++ b/crates/perry-ext-net/src/adopt.rs @@ -53,6 +53,7 @@ pub fn adopt_upgraded_tcp_stream(stream: tokio::net::TcpStream) -> i64 { destroyed: false, bytes_read: 0, bytes_written: 0, + bytes_queued: 0, timeout: None, type_of_service: 0, server_id: None, diff --git a/crates/perry-ext-net/src/ipc.rs b/crates/perry-ext-net/src/ipc.rs index b501471f3a..afd088afdf 100644 --- a/crates/perry-ext-net/src/ipc.rs +++ b/crates/perry-ext-net/src/ipc.rs @@ -44,6 +44,7 @@ fn allocate_socket() -> (i64, mpsc::UnboundedReceiver) { destroyed: false, bytes_read: 0, bytes_written: 0, + bytes_queued: 0, timeout: None, type_of_service: 0, server_id: None, @@ -114,6 +115,7 @@ pub(crate) fn register_accepted_transport( destroyed: false, bytes_read: 0, bytes_written: 0, + bytes_queued: 0, timeout: None, type_of_service: 0, server_id: Some(server_id), diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 548c844021..068c5c1527 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -12,20 +12,10 @@ //! //! # Differences from the perry-stdlib version //! -//! - Uses `perry_ffi::spawn_async` to drive each socket reader / server accept -//! loop cooperatively on Perry's shared multi-thread runtime (the same -//! reactor `crate::common::async_bridge` drives), rather than spinning a -//! throwaway current-thread runtime on a blocking-pool thread per socket. -//! Keepalive comes from `js_ext_net_has_active_handles` (the socket/server is -//! registered synchronously before the spawn), not the blocking-pool -//! active-handle counter. -//! - Uses `perry_ffi::JsClosure` instead of raw `js_closure_call*` extern fns. -//! - Uses `perry_ffi::alloc_buffer` / `BufferHeader` instead of -//! `perry-runtime::buffer::*` directly. -//! - GC root scanner registered via `perry_ffi::gc_register_mutable_root_scanner`. -//! Listeners stored inside the `NET_LISTENERS` map need this — issue #35 -//! pattern — and the mutable visitor lets copied-minor GC rewrite moved -//! closure pointers in place. +//! - Uses `perry_ffi::spawn_async` on Perry's shared runtime, with keepalive +//! provided by `js_ext_net_has_active_handles`. +//! - Uses perry-ffi closures, buffers, and mutable GC root scanning; the latter +//! rewrites listener pointers after a copying minor collection. //! //! TLS is unconditionally compiled in (no `#[cfg(feature = "tls")]` gates //! like perry-stdlib has) — keeping the wrapper crate simple, the deps are @@ -276,6 +266,7 @@ pub(crate) struct SocketState { pub(crate) destroyed: bool, pub(crate) bytes_read: u64, pub(crate) bytes_written: u64, + pub(crate) bytes_queued: u64, pub(crate) timeout: Option, pub(crate) type_of_service: u8, pub(crate) server_id: Option, @@ -301,6 +292,7 @@ impl SocketState { destroyed: false, bytes_read: 0, bytes_written: 0, + bytes_queued: 0, timeout: None, type_of_service: 0, server_id: None, @@ -554,6 +546,7 @@ pub unsafe extern "C" fn js_net_socket_alloc() -> i64 { destroyed: false, bytes_read: 0, bytes_written: 0, + bytes_queued: 0, timeout: None, type_of_service: 0, server_id: None, @@ -1081,6 +1074,7 @@ where destroyed: false, bytes_read: 0, bytes_written: 0, + bytes_queued: 0, timeout: None, type_of_service: 0, server_id: None, @@ -1238,7 +1232,9 @@ pub(crate) async fn run_socket_task( }; match command { Some(SocketCommand::Write(bytes, completion)) => { - if let Err(e) = t.write_all(&bytes).await { + if let Err(e) = + lifecycle::write_socket_bytes(t, id, &bytes).await + { let msg = format!("{}", e); if completion != 0 { push_event(PendingNetEvent::WriteComplete( @@ -1341,7 +1337,9 @@ pub(crate) async fn run_socket_task( buffer_pool::checkin(buf); match cmd { Some(SocketCommand::Write(bytes, completion)) => { - if let Err(e) = t.write_all(&bytes).await { + if let Err(e) = + lifecycle::write_socket_bytes(t, id, &bytes).await + { let msg = format!("{}", e); if completion != 0 { push_event(PendingNetEvent::WriteComplete( diff --git a/crates/perry-ext-net/src/lifecycle.rs b/crates/perry-ext-net/src/lifecycle.rs index 5b1ea5f512..98f16e1590 100644 --- a/crates/perry-ext-net/src/lifecycle.rs +++ b/crates/perry-ext-net/src/lifecycle.rs @@ -22,8 +22,10 @@ use perry_ffi::{alloc_string, nanbox_string_bits, ArrayHeader, JsValue, StringHeader}; use std::collections::HashSet; +use std::io; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; +use tokio::io::AsyncWriteExt; use crate::statics; use crate::string_from_header_i64; @@ -174,14 +176,16 @@ pub unsafe extern "C" fn js_net_socket_get_bytes_read(handle: i64) -> f64 { with_socket(handle, 0u64, |s| s.bytes_read) as f64 } -/// `socket.bytesWritten` — total bytes queued for the socket. +/// `socket.bytesWritten` — bytes dispatched to the transport or still queued. /// /// # Safety /// /// See [`js_net_socket_get_pending`]. #[no_mangle] pub unsafe extern "C" fn js_net_socket_get_bytes_written(handle: i64) -> f64 { - with_socket(handle, 0u64, |s| s.bytes_written) as f64 + with_socket(handle, 0u64, |s| { + s.bytes_written.saturating_add(s.bytes_queued) + }) as f64 } /// `socket.timeout` — the value set via `setTimeout(ms)`, or `undefined`. @@ -344,13 +348,14 @@ pub unsafe extern "C" fn js_ext_net_socket_write(handle: i64, chunk_bits: i64) { fn enqueue_socket_write(handle: i64, bytes: Vec, completion: u64) { let mut sockets = statics::sockets().lock().unwrap(); let failure = if let Some(s) = sockets.get_mut(&handle) { - s.bytes_written = s.bytes_written.saturating_add(bytes.len() as u64); + let byte_len = bytes.len() as u64; if s.cmd_tx .send(crate::SocketCommand::Write(bytes, completion)) .is_err() { Some("Socket write failed") } else { + s.bytes_queued = s.bytes_queued.saturating_add(byte_len); None } } else { @@ -366,6 +371,37 @@ fn enqueue_socket_write(handle: i64, bytes: Vec, completion: u64) { } } +fn record_socket_write_progress(handle: i64, written: usize) { + if written == 0 { + return; + } + if let Some(socket) = statics::sockets().lock().unwrap().get_mut(&handle) { + let written = written as u64; + socket.bytes_queued = socket.bytes_queued.saturating_sub(written); + socket.bytes_written = socket.bytes_written.saturating_add(written); + } +} + +pub(crate) async fn write_socket_bytes( + transport: &mut crate::Transport, + handle: i64, + bytes: &[u8], +) -> io::Result<()> { + let mut written = 0; + while written < bytes.len() { + let count = transport.write(&bytes[written..]).await?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write socket bytes", + )); + } + written += count; + record_socket_write_progress(handle, count); + } + Ok(()) +} + /// `socket.write(chunk)` under the name the static NATIVE_MODULE_TABLE path /// emits. Delegates to the collision-proof [`js_ext_net_socket_write`] via a /// crate-local call, so even when the bundled stdlib's same-named twin wins the @@ -464,8 +500,10 @@ pub unsafe extern "C" fn js_ext_net_socket_end(handle: i64, chunk_bits: i64) { if let Some(s) = sockets.get_mut(&handle) { if let Some(bytes) = final_bytes { if !bytes.is_empty() { - s.bytes_written = s.bytes_written.saturating_add(bytes.len() as u64); - let _ = s.cmd_tx.send(crate::SocketCommand::Write(bytes, 0)); + let byte_len = bytes.len() as u64; + if s.cmd_tx.send(crate::SocketCommand::Write(bytes, 0)).is_ok() { + s.bytes_queued = s.bytes_queued.saturating_add(byte_len); + } } } let _ = s.cmd_tx.send(crate::SocketCommand::End(0)); @@ -516,8 +554,14 @@ pub unsafe extern "C" fn js_ext_net_socket_end3( let mut sockets = statics::sockets().lock().unwrap(); if let Some(socket) = sockets.get_mut(&handle) { if let Some(bytes) = final_bytes.filter(|bytes| !bytes.is_empty()) { - socket.bytes_written = socket.bytes_written.saturating_add(bytes.len() as u64); - let _ = socket.cmd_tx.send(crate::SocketCommand::Write(bytes, 0)); + let byte_len = bytes.len() as u64; + if socket + .cmd_tx + .send(crate::SocketCommand::Write(bytes, 0)) + .is_ok() + { + socket.bytes_queued = socket.bytes_queued.saturating_add(byte_len); + } } if socket .cmd_tx @@ -1198,4 +1242,39 @@ mod tests { reset_handle(handle); } + + #[test] + fn rejected_write_does_not_increase_bytes_written() { + let handle = -91_238; + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + drop(rx); + statics::sockets() + .lock() + .unwrap() + .insert(handle, crate::SocketState::for_test(tx)); + + enqueue_socket_write(handle, vec![1, 2, 3], 0); + assert_eq!(unsafe { js_net_socket_get_bytes_written(handle) }, 0.0); + + statics::sockets().lock().unwrap().remove(&handle); + } + + #[test] + fn bytes_written_includes_queue_then_keeps_only_dispatched_progress_on_close() { + let handle = -91_239; + let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); + statics::sockets() + .lock() + .unwrap() + .insert(handle, crate::SocketState::for_test(tx)); + + enqueue_socket_write(handle, vec![1, 2, 3, 4], 0); + assert_eq!(unsafe { js_net_socket_get_bytes_written(handle) }, 4.0); + record_socket_write_progress(handle, 2); + assert_eq!(unsafe { js_net_socket_get_bytes_written(handle) }, 4.0); + crate::server_state::mark_socket_closed(handle); + assert_eq!(unsafe { js_net_socket_get_bytes_written(handle) }, 2.0); + + statics::sockets().lock().unwrap().remove(&handle); + } } diff --git a/crates/perry-ext-net/src/server_state.rs b/crates/perry-ext-net/src/server_state.rs index e16cebf739..276b88befc 100644 --- a/crates/perry-ext-net/src/server_state.rs +++ b/crates/perry-ext-net/src/server_state.rs @@ -350,6 +350,7 @@ pub(crate) fn mark_socket_closed(socket_id: i64) { return; }; socket.is_open = false; + socket.bytes_queued = 0; let Some(server_id) = socket.server_id.take() else { return; }; diff --git a/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts b/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts index 4b1dbcfac8..04b5efdecb 100644 --- a/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts +++ b/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts @@ -20,6 +20,7 @@ try { client!.write("payload", () => { console.log("net write callback store:", storage.getStore()); }); + console.log("net write queued bytes:", client!.bytesWritten); client!.end(() => { console.log("net end callback store:", storage.getStore()); });