diff --git a/changelog.d/8671-async-hooks-parity.md b/changelog.d/8671-async-hooks-parity.md deleted file mode 100644 index 380a36f6da..0000000000 --- a/changelog.d/8671-async-hooks-parity.md +++ /dev/null @@ -1,8 +0,0 @@ -### Fixed - -- Complete the `node:async_hooks` parity tracker across all 194 fixtures: - hook mutation and lifecycle ordering, Promise/resource identity and trigger - chains, `AsyncResource` and `EventEmitterAsyncResource` subclasses, - `AsyncLocalStorage` propagation, and provider lifecycles for timers, files, - DNS, crypto, zlib, processes, signals, workers, streams, net, HTTP(S), TLS, - readline, event iterators, ESM, fetch, and UDP now match Node. diff --git a/crates/perry-api-manifest/src/entries/part_4.rs b/crates/perry-api-manifest/src/entries/part_4.rs index 3894bbfa36..e54672ee18 100644 --- a/crates/perry-api-manifest/src/entries/part_4.rs +++ b/crates/perry-api-manifest/src/entries/part_4.rs @@ -838,7 +838,6 @@ pub(crate) const API_MANIFEST_PART_4: &[ApiEntry] = &[ method("http", "ref", true, Some("HttpServer")), method("http", "unref", true, Some("HttpServer")), method("http", "on", true, Some("IncomingMessage")), - method("http", "once", true, Some("IncomingMessage")), method("http", "addListener", true, Some("IncomingMessage")), method("http", "pause", true, Some("IncomingMessage")), method("http", "resume", true, Some("IncomingMessage")), @@ -857,7 +856,6 @@ pub(crate) const API_MANIFEST_PART_4: &[ApiEntry] = &[ // method, so the manifest-consistency drift guard requires a row // here even though the test collapses class_filter variants. method("http", "setTimeout", true, Some("ClientRequest")), - method("http", "once", true, Some("ClientRequest")), method("http", "listenerCount", true, Some("ClientRequest")), method("http", "setHeader", true, Some("ClientRequest")), method("http", "getHeader", true, Some("ClientRequest")), diff --git a/crates/perry-codegen/src/expr/calls/crypto_misc.rs b/crates/perry-codegen/src/expr/calls/crypto_misc.rs index adcb45142b..649cca8666 100644 --- a/crates/perry-codegen/src/expr/calls/crypto_misc.rs +++ b/crates/perry-codegen/src/expr/calls/crypto_misc.rs @@ -271,18 +271,13 @@ pub(crate) fn arm_crypto_prime( unreachable!() }; let first_box = lower_expr(ctx, &args[0])?; - let is_async = matches!(property, "generatePrime" | "checkPrime"); - // The callback forms are `(value, callback)` or - // `(value, options, callback)`. Treating the second argument as options - // unconditionally accidentally routed the common two-argument form to - // the synchronous implementation and returned the generated value. - let options_box = if args.len() >= 2 && (!is_async || args.len() >= 3) { + let options_box = if args.len() >= 2 { lower_expr(ctx, &args[1])? } else { double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)) }; - let callback_box = if is_async && args.len() >= 2 { - Some(lower_expr(ctx, &args[args.len().min(3) - 1])?) + let callback_box = if matches!(property, "generatePrime" | "checkPrime") && args.len() >= 3 { + Some(lower_expr(ctx, &args[2])?) } else { None }; diff --git a/crates/perry-codegen/src/expr/env_clones.rs b/crates/perry-codegen/src/expr/env_clones.rs index bde625da12..80dbfba215 100644 --- a/crates/perry-codegen/src/expr/env_clones.rs +++ b/crates/perry-codegen/src/expr/env_clones.rs @@ -193,7 +193,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let blk = ctx.block(); let raw = blk.call( I64, - "js_ext_net_create_server", + "js_net_create_server", &[(I64, &options_i64), (I64, &listener_i64)], ); Ok(nanbox_pointer_inline(blk, &raw)) diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index cea7608f8e..74962321e7 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -483,8 +483,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Promise values are raw promise allocations, not ObjectHeader // instances with a class_id field. "Promise" => 0xFFFF0027u32, - "AsyncLocalStorage" => 0xFFFF0078u32, - "AsyncResource" => 0xFFFF0079u32, // WHATWG fetch types. Like Blob/streams these are pointer-tagged // small-int handles; the runtime resolves them via the stdlib // fetch kind-probe (`res instanceof Response`, etc.). diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index af87f6da97..54eb58b38b 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -132,8 +132,7 @@ pub(crate) use write_barrier::{ emit_root_heap_word_store_on_block, emit_root_nanbox_store_on_block, emit_write_barrier, emit_write_barrier_slot_generation_tested, emit_write_barrier_slot_on_block, emit_write_barrier_slot_value_and_generation_tested, lower_array_super_init, - lower_event_emitter_async_resource_subclass_init, lower_event_emitter_subclass_init, - lower_node_stream_super_init, lower_stream_super_init, + lower_event_emitter_subclass_init, lower_node_stream_super_init, lower_stream_super_init, }; // Issue #1098 phase 3: the `FnCtx` definition stays in this trunk, but its diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 8229d2f0c4..15a0daaf3a 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -1376,15 +1376,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if receiver_class_is_proven && is_net_native_method_value(&class_name, property) { return lower_class_method_bind(ctx, object, property); } - if receiver_class_is_proven - && class_name == "AsyncResource" - && matches!( - property.as_str(), - "asyncId" | "triggerAsyncId" | "emitDestroy" | "runInAsyncScope" | "bind" - ) - { - return lower_runtime_property_get_by_name(ctx, object, property); - } if class_has_computed_runtime_members(ctx, &class_name) { return lower_runtime_property_get_by_name(ctx, object, property); } diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 42f62ef709..4eb5c09e51 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -12,9 +12,8 @@ use crate::nanbox::{double_literal, POINTER_MASK_I64}; use crate::types::{DOUBLE, I1, I32, I64, PTR}; use super::{ - lower_array_super_init, lower_event_emitter_async_resource_subclass_init, - lower_event_emitter_subclass_init, lower_expr, lower_node_stream_super_init, - lower_stream_super_init, nanbox_pointer_inline, FnCtx, + lower_array_super_init, lower_event_emitter_subclass_init, lower_expr, + lower_node_stream_super_init, lower_stream_super_init, nanbox_pointer_inline, FnCtx, }; /// Enter one derived constructor's `super()` binding scope. @@ -827,83 +826,6 @@ 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 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 { - let _ = lower_expr(ctx, arg)?; - } - 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)), - }; - ctx.block().call( - DOUBLE, - "js_async_local_storage_subclass_init", - &[(DOUBLE, &this_box)], - ); - 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() == "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))); - } // `class X extends Request` / `extends Response`: // `super(input, init)` allocates the underlying native // Web-Fetch handle and stashes its id on `this` under diff --git a/crates/perry-codegen/src/expr/write_barrier.rs b/crates/perry-codegen/src/expr/write_barrier.rs index b84c06b582..13b4c4091a 100644 --- a/crates/perry-codegen/src/expr/write_barrier.rs +++ b/crates/perry-codegen/src/expr/write_barrier.rs @@ -1081,15 +1081,3 @@ pub(crate) fn lower_event_emitter_subclass_init(ctx: &mut FnCtx<'_>, this_box: & &[(DOUBLE, this_box)], ); } - -pub(crate) fn lower_event_emitter_async_resource_subclass_init( - ctx: &mut FnCtx<'_>, - this_box: &str, - options_box: &str, -) { - ctx.block().call( - DOUBLE, - "js_event_emitter_async_resource_subclass_init", - &[(DOUBLE, this_box), (DOUBLE, options_box)], - ); -} diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index cd1a27c466..da1291ba8c 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -391,8 +391,6 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ // import flip might not fire. Tagging here so the linker pulls // libperry_ext_net.a in regardless. ("js_net_create_server", OwnerKind::WellKnown("net")), - ("js_ext_net_create_server", OwnerKind::WellKnown("net")), - ("js_ext_net_socket_connect", OwnerKind::WellKnown("net")), ("js_net_server_listen", OwnerKind::WellKnown("net")), ("js_net_server_close", OwnerKind::WellKnown("net")), ("js_net_server_address", OwnerKind::WellKnown("net")), @@ -441,9 +439,6 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ // program that doesn't otherwise import socket-side surface. ("js_net_socket_address", OwnerKind::WellKnown("net")), ("js_net_socket_once", OwnerKind::WellKnown("net")), - ("js_ext_net_socket_once", OwnerKind::WellKnown("net")), - ("js_ext_net_socket_on", OwnerKind::WellKnown("net")), - ("js_ext_tls_connect", OwnerKind::WellKnown("net")), ("js_net_socket_remove_listener", OwnerKind::WellKnown("net")), ("js_net_socket_remove_all_listeners", OwnerKind::WellKnown("net")), ("js_net_socket_listener_count", OwnerKind::WellKnown("net")), @@ -544,15 +539,11 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ // `external-events-construct` feature (see optimized_libs.rs), which the // default-import dynamic-`new` path relies on (#4995). // - // EventEmitterAsyncResource lives alongside the external EventEmitter so - // optimized node:events builds retain one coherent handle registry. + // Only the core surface defined by perry-ext-events is listed; the + // `js_event_emitter_async_resource_*` helpers live in perry-stdlib and + // are out of scope here (`EventEmitterAsyncResource` is node:events-only). ("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_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")), - ("js_event_emitter_async_resource_emit_destroy", OwnerKind::WellKnown("events")), ("js_event_emitter_on", OwnerKind::WellKnown("events")), ("js_event_emitter_once", OwnerKind::WellKnown("events")), ("js_event_emitter_prepend_listener", OwnerKind::WellKnown("events")), @@ -568,8 +559,6 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_event_emitter_set_max_listeners", OwnerKind::WellKnown("events")), ("js_event_emitter_get_max_listeners", OwnerKind::WellKnown("events")), ("js_event_emitter_domain_value", OwnerKind::WellKnown("events")), - ("js_ext_net_socket_write3", OwnerKind::WellKnown("net")), - ("js_ext_net_socket_end3", OwnerKind::WellKnown("net")), // ── mysql2 (perry-ext-mysql2) ──────────────────────────────────── // Normally `import "mysql2"` flips the `[bindings.mysql2]` well-known diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index 637ec3b3a2..5dca17baa9 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -133,32 +133,6 @@ pub(super) fn lower_builtin_new<'a>( } } match class_name { - "Resolver" - if import_src.is_some_and(|source| { - matches!( - source.strip_prefix("node:").unwrap_or(source), - "dns" | "dns/promises" - ) - }) => - { - // `new Resolver()` is a constructor expression, so it bypasses - // 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 _ = lower_expr(ctx, arg)?; - } - let runtime = if import_src.is_some_and(|source| { - source.strip_prefix("node:").unwrap_or(source) == "dns/promises" - }) { - "js_dns_promises_resolver_new" - } else { - "js_dns_resolver_new" - }; - ctx.pending_declares - .push((runtime.to_string(), DOUBLE, vec![I64])); - Ok(Some(ctx.block().call(DOUBLE, runtime, &[(I64, "0")]))) - } "Utf8Stream" if import_src .map(|source| source.strip_prefix("node:").unwrap_or(source) == "fs") 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..43dba9e93d 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 @@ -136,15 +136,6 @@ pub(super) const HTTP_CLIENT_ROWS: &[NativeModSig] = &[ args: &[NA_STR, NA_PTR], ret: NR_PTR, }, - NativeModSig { - module: "http", - has_receiver: true, - method: "once", - class_filter: Some("ClientRequest"), - runtime: "js_http_on", - args: &[NA_STR, NA_PTR], - ret: NR_PTR, - }, NativeModSig { module: "http", has_receiver: true, 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..11b8c59f49 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 @@ -382,19 +382,6 @@ pub(super) const HTTP_SERVER_ROWS: &[NativeModSig] = &[ args: &[NA_STR, NA_PTR], ret: NR_F64, }, - // IncomingMessage events in the async-hooks provider probes are terminal - // (`end`/`error`), so the shared registration path also provides the - // observable one-shot behaviour while ensuring client responses are - // cross-routed into perry-ext-http's listener registry. - NativeModSig { - module: "http", - has_receiver: true, - method: "once", - class_filter: Some("IncomingMessage"), - runtime: "js_node_http_im_on", - args: &[NA_STR, NA_PTR], - ret: NR_F64, - }, NativeModSig { module: "http", has_receiver: true, diff --git a/crates/perry-codegen/src/lower_call/native_table/net_events.rs b/crates/perry-codegen/src/lower_call/native_table/net_events.rs index f6d1f58575..0918b10364 100644 --- a/crates/perry-codegen/src/lower_call/native_table/net_events.rs +++ b/crates/perry-codegen/src/lower_call/native_table/net_events.rs @@ -148,7 +148,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: false, method: "createConnection", class_filter: None, - runtime: "js_ext_net_socket_connect", + runtime: "js_net_socket_connect", args: &[NA_F64, NA_F64, NA_F64], ret: NR_PTR, }, @@ -163,7 +163,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: false, method: "connect", class_filter: None, - runtime: "js_ext_net_socket_connect", + runtime: "js_net_socket_connect", args: &[NA_F64, NA_F64, NA_F64], ret: NR_PTR, }, @@ -176,7 +176,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: false, method: "createServer", class_filter: None, - runtime: "js_ext_net_create_server", + runtime: "js_net_create_server", args: &[NA_PTR, NA_PTR], ret: NR_PTR, }, @@ -185,7 +185,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: false, method: "Server", class_filter: None, - runtime: "js_ext_net_create_server", + runtime: "js_net_create_server", args: &[NA_PTR, NA_PTR], ret: NR_PTR, }, @@ -309,14 +309,14 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "write", class_filter: Some("Socket"), - runtime: "js_ext_net_socket_write3", + runtime: "js_net_socket_write", // Issue #1131 — pass the full NaN-boxed JS value (NA_JSV) so // the runtime can probe Buffer-vs-string-vs-number and read // through the correct header layout. NA_PTR pre-stripped the // tag, so `sock.write("ping")` handed the runtime a bare // StringHeader pointer that it reinterpreted as a // BufferHeader → garbage on the wire. - args: &[NA_JSV, NA_JSV, NA_JSV], + args: &[NA_JSV], ret: NR_VOID, }, NativeModSig { @@ -324,12 +324,12 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "end", class_filter: Some("Socket"), - runtime: "js_ext_net_socket_end3", + runtime: "js_net_socket_end", // Issue #1852 — `socket.end([data])` writes the optional final // chunk before half-closing. NA_JSV carries the full NaN-boxed // value so the runtime can probe Buffer/string/number; the // no-arg `socket.end()` form pads this slot with `undefined`. - args: &[NA_JSV, NA_JSV, NA_JSV], + args: &[NA_JSV], ret: NR_VOID, }, NativeModSig { @@ -346,7 +346,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "on", class_filter: Some("Socket"), - runtime: "js_ext_net_socket_on", + runtime: "js_net_socket_on", args: &[NA_STR, NA_PTR], ret: NR_VOID, }, @@ -625,11 +625,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "once", class_filter: Some("Socket"), - // Use ext-net's collision-proof symbol. The bundled stdlib exports a - // same-named `js_net_socket_once`; in an auto-optimized link the - // shared name can resolve to that empty registry and silently drop - // listeners on sockets owned by perry-ext-net. - runtime: "js_ext_net_socket_once", + runtime: "js_net_socket_once", args: &[NA_STR, NA_PTR], ret: NR_PTR, }, @@ -638,7 +634,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: true, method: "addListener", class_filter: Some("Socket"), - runtime: "js_ext_net_socket_on", + runtime: "js_net_socket_on", args: &[NA_STR, NA_PTR], ret: NR_VOID, }, @@ -754,7 +750,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ has_receiver: false, method: "connect", class_filter: None, - runtime: "js_ext_tls_connect", + runtime: "js_tls_connect", args: &[NA_F64, NA_F64, NA_F64, NA_F64], ret: NR_PTR, }, diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs index 666d2a0cd3..d13d721c42 100644 --- a/crates/perry-codegen/src/lower_call/new_helpers.rs +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -35,9 +35,6 @@ use crate::types::{DOUBLE, I32}; pub(crate) enum NativeInstanceBase { EventEmitter, Array, - EventEmitterAsyncResource, - AsyncLocalStorage, - AsyncResource, Map, Set, WeakMap, @@ -60,9 +57,6 @@ pub(crate) fn native_instance_base(name: &str) -> Option { match name { "EventEmitter" => Some(NativeInstanceBase::EventEmitter), "Array" | "ReadonlyArray" => Some(NativeInstanceBase::Array), - "EventEmitterAsyncResource" => Some(NativeInstanceBase::EventEmitterAsyncResource), - "AsyncLocalStorage" => Some(NativeInstanceBase::AsyncLocalStorage), - "AsyncResource" => Some(NativeInstanceBase::AsyncResource), "Map" => Some(NativeInstanceBase::Map), "Set" => Some(NativeInstanceBase::Set), "WeakMap" => Some(NativeInstanceBase::WeakMap), @@ -164,36 +158,6 @@ pub(crate) fn emit_native_instance_base_init( ], ); } - NativeInstanceBase::EventEmitterAsyncResource => { - let options = lowered_args.first().cloned().unwrap_or(undef); - crate::expr::lower_event_emitter_async_resource_subclass_init(ctx, this_box, &options); - } - NativeInstanceBase::AsyncLocalStorage => { - ctx.block().call( - DOUBLE, - "js_async_local_storage_subclass_init", - &[(DOUBLE, this_box)], - ); - } - NativeInstanceBase::AsyncResource => { - let type_value = lowered_args - .first() - .cloned() - .unwrap_or_else(|| undef.clone()); - let options = lowered_args - .get(1) - .cloned() - .unwrap_or_else(|| undef.clone()); - ctx.block().call( - DOUBLE, - "js_async_resource_subclass_init", - &[ - (DOUBLE, this_box), - (DOUBLE, &type_value), - (DOUBLE, &options), - ], - ); - } NativeInstanceBase::Map | NativeInstanceBase::Set => { let kind: i32 = if base == NativeInstanceBase::Map { 0 diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs index f6b2464854..a89983e334 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/language_core.rs @@ -166,8 +166,6 @@ pub(crate) fn declare_core(module: &mut LlModule) { // `expr.rs::Expr::NetCreateServer`, matching the // `js_node_http_create_server` (`I64, &[I64]`) convention. module.declare_function("js_net_create_server", I64, &[I64, I64]); - module.declare_function("js_ext_net_create_server", I64, &[I64, I64]); - module.declare_function("js_ext_net_socket_connect", I64, &[DOUBLE, DOUBLE, DOUBLE]); module.declare_function("js_net_normalize_args", DOUBLE, &[DOUBLE]); module.declare_function( "js_net_create_server_handle_stub", @@ -228,9 +226,6 @@ pub(crate) fn declare_core(module: &mut LlModule) { // pointers consumed by the NR_OBJ_FROM_JSON_STR pipeline. module.declare_function("js_net_socket_address", I64, &[I64]); module.declare_function("js_net_socket_once", I64, &[I64, I64, I64]); - module.declare_function("js_ext_net_socket_once", I64, &[I64, I64, I64]); - module.declare_function("js_ext_net_socket_on", VOID, &[I64, I64, I64]); - module.declare_function("js_ext_tls_connect", I64, &[DOUBLE, DOUBLE, DOUBLE, DOUBLE]); module.declare_function("js_net_socket_remove_listener", I64, &[I64, I64, I64]); module.declare_function("js_net_socket_remove_all_listeners", I64, &[I64, I64]); module.declare_function("js_net_socket_listener_count", DOUBLE, &[I64, I64]); diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs index 3decf0f8cc..a35162b2b7 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs @@ -8,11 +8,6 @@ use crate::types::{DOUBLE, I32, I64, PTR, VOID}; pub(crate) fn declare_streams_events(module: &mut LlModule) { // ========== node:stream stubs (issue #631) ========== module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE]); // #5137 EE subclass init - module.declare_function( - "js_event_emitter_async_resource_subclass_init", - DOUBLE, - &[DOUBLE, DOUBLE], - ); module.declare_function("js_array_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Array module.declare_function("js_array_subclass_init_args", DOUBLE, &[DOUBLE, PTR, I64]); module.declare_function("js_map_set_subclass_init", DOUBLE, &[DOUBLE, I32, DOUBLE]); // class extends Map/Set diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs index 4609ade92a..0b69a94701 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs @@ -159,11 +159,6 @@ pub(crate) fn declare_third_party(module: &mut LlModule) { module.declare_function("js_async_hook_enable", I64, &[I64]); module.declare_function("js_async_hook_disable", I64, &[I64]); module.declare_function("js_async_resource_new", I64, &[DOUBLE, DOUBLE]); - module.declare_function( - "js_async_resource_subclass_init", - DOUBLE, - &[DOUBLE, DOUBLE, DOUBLE], - ); module.declare_function("js_async_resource_async_id", DOUBLE, &[I64]); module.declare_function("js_async_resource_trigger_async_id", DOUBLE, &[I64]); module.declare_function("js_async_resource_emit_destroy", I64, &[I64]); @@ -181,7 +176,6 @@ pub(crate) fn declare_third_party(module: &mut LlModule) { module.declare_function("js_async_local_storage_exit", DOUBLE, &[I64, DOUBLE, I64]); module.declare_function("js_async_local_storage_get_store", DOUBLE, &[I64]); module.declare_function("js_async_local_storage_new", I64, &[]); - module.declare_function("js_async_local_storage_subclass_init", DOUBLE, &[DOUBLE]); module.declare_function( "js_async_local_storage_run", DOUBLE, diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index 3863c15c99..5a67004744 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -20,10 +20,9 @@ //! `events.getMaxListeners` / `events.setMaxListeners` helpers. 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, + error_value_with_code, js_array_alloc, js_array_get, js_array_push, js_array_set, + nanbox_string_bits, read_string, throw_with_code, ArrayHeader, ErrorKind, Handle, JsPromise, + JsString, JsValue, ObjectHeader, Promise, RawClosureHeader, StringHeader, }; use std::collections::{HashMap, HashSet}; use std::ffi::c_void; @@ -34,8 +33,6 @@ use error_monitor::dispatch_error_monitor; mod max_listeners; mod messages; mod module_helpers; -mod module_on; -pub use module_on::{js_events_add_abort_listener, js_events_on}; mod target_helpers; use module_helpers::{call_net_socket_method, js_events_native_dispatch}; @@ -58,10 +55,9 @@ use target_helpers::{ }; mod module_iterators; use module_iterators::{ - events_on_abort_listener, events_on_close_listener, events_on_install_async_iterator, - 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_abort_listener, events_on_queue_listener, events_once_abort_listener, + events_once_event_target_listener, events_once_stream_reject_listener, + events_once_stream_resolve_listener, }; const MIN_HEAP_POINTER: u64 = 0x1000; @@ -115,11 +111,6 @@ extern "C" { fn js_closure_set_capture_ptr(closure: *mut RawClosureHeader, slot: u32, ptr: i64); fn js_closure_get_capture_ptr(closure: *const RawClosureHeader, slot: u32) -> i64; fn js_array_push_f64(arr: *mut ArrayHeader, value: f64) -> *mut ArrayHeader; - fn js_array_shift_f64(arr: *mut ArrayHeader) -> f64; - fn js_promise_new() -> *mut Promise; - fn js_register_closure_arity(func_ptr: *const u8, arity: u32); - fn js_closure_get_capture_f64(closure: *const RawClosureHeader, slot: u32) -> f64; - fn js_closure_set_capture_f64(closure: *mut RawClosureHeader, slot: u32, value: f64); // #1557: AbortSignal listener attachment for events.addAbortListener. fn js_string_from_bytes(data: *const u8, len: u32) -> *mut StringHeader; fn js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader; @@ -128,7 +119,6 @@ extern "C" { fn js_object_set_field_by_name(obj: *mut ObjectHeader, key: *const StringHeader, value: f64); fn js_symbol_for(key_f64: f64) -> f64; fn js_object_set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64; - fn js_symbol_well_known_async_iterator() -> f64; fn js_get_global_this() -> f64; fn js_array_is_array(value: f64) -> f64; fn js_abort_signal_add_listener(signal: *mut u8, event: f64, listener: f64); @@ -173,8 +163,6 @@ extern "C" { fn js_native_call_value(func_value: f64, args_ptr: *const f64, args_len: usize) -> f64; fn js_value_is_promise(value: f64) -> i32; fn js_register_event_emitter_handle_probe(f: unsafe extern "C" fn(i64) -> bool); - fn js_register_event_emitter_async_resource_handle_probe(f: unsafe extern "C" fn(i64) -> bool); - fn js_register_event_emitter_async_resource_dispatch(f: unsafe extern "C" fn(i64, u32) -> f64); fn js_register_event_emitter_get_domain(f: unsafe extern "C" fn(i64) -> i64); fn js_register_event_emitter_set_domain(f: unsafe extern "C" fn(i64, i64) -> i32); fn js_register_event_emitter_on(f: unsafe extern "C" fn(i64, i64, i64) -> i64); @@ -195,14 +183,6 @@ extern "C" { // stdlib and ext-events EventEmitter implementations stay byte-identical. fn js_validate_event_listener(listener_bits: i64, name_ptr: *const u8, name_len: u32) -> i64; fn js_register_closure_rest(fn_ptr: *const u8, fixed_arity: u32); - fn js_async_resource_new(type_value: f64, options: f64) -> i64; - fn js_async_resource_async_id(handle: i64) -> f64; - fn js_async_resource_trigger_async_id(handle: i64) -> f64; - 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); } /// #3072: validate an EventEmitter listener argument, returning the closure @@ -291,7 +271,6 @@ pub struct EventEmitterHandle { max_listeners: f64, capture_rejections: bool, domain_handle: Option, - async_resource_handle: i64, } // SAFETY: `*mut Promise` is not Send/Sync by default, but the registry's @@ -318,7 +297,6 @@ impl EventEmitterHandle { max_listeners: 10.0, capture_rejections: false, domain_handle: None, - async_resource_handle: 0, } } @@ -485,20 +463,6 @@ unsafe extern "C" fn event_emitter_handle_probe(handle: i64) -> bool { is_local_event_emitter_handle(handle) } -unsafe extern "C" fn event_emitter_async_resource_handle_probe(handle: i64) -> bool { - get_event_emitter_mut(handle).is_some_and(|emitter| emitter.async_resource_handle != 0) -} - -unsafe extern "C" fn event_emitter_async_resource_dispatch(handle: i64, operation: u32) -> f64 { - match operation { - 0 => js_event_emitter_async_resource_async_id(handle), - 1 => js_event_emitter_async_resource_trigger_async_id(handle), - 2 => js_event_emitter_async_resource_async_resource(handle), - 3 => js_event_emitter_async_resource_emit_destroy(handle), - _ => undefined_value(), - } -} - unsafe extern "C" fn event_emitter_on_hook( handle: i64, event_bits: i64, @@ -518,27 +482,20 @@ unsafe extern "C" fn events_native_construct( args_len: usize, ) -> f64 { let class_name = std::slice::from_raw_parts(class_name_ptr, class_name_len); + if class_name != b"EventEmitter" { + return f64::from_bits(TAG_UNDEFINED_F64_BITS); + } let options = if !args_ptr.is_null() && args_len > 0 { *args_ptr } else { f64::from_bits(TAG_UNDEFINED_F64_BITS) }; - match class_name { - b"EventEmitter" => nanbox_pointer_bits(js_event_emitter_new_with_options(options)), - b"EventEmitterAsyncResource" => { - nanbox_pointer_bits(js_event_emitter_async_resource_new(options)) - } - _ => f64::from_bits(TAG_UNDEFINED_F64_BITS), - } + nanbox_pointer_bits(js_event_emitter_new_with_options(options)) } fn ensure_runtime_hooks_registered() { EVENTS_RUNTIME_HOOKS_REGISTERED.call_once(|| unsafe { js_register_event_emitter_handle_probe(event_emitter_handle_probe); - js_register_event_emitter_async_resource_handle_probe( - event_emitter_async_resource_handle_probe, - ); - js_register_event_emitter_async_resource_dispatch(event_emitter_async_resource_dispatch); js_register_event_emitter_get_domain(js_event_emitter_get_domain); js_register_event_emitter_set_domain(js_event_emitter_set_domain); js_register_event_emitter_on(event_emitter_on_hook); @@ -974,81 +931,6 @@ pub unsafe extern "C" fn js_event_emitter_new_with_options(_options: f64) -> Han register_event_emitter_handle(emitter) } -unsafe fn event_emitter_async_resource_name(options: f64) -> f64 { - if JsValue::from_bits(options.to_bits()).is_any_string() { - options - } else { - get_object_property(options, b"name").unwrap_or_else(undefined_value) - } -} - -/// `new EventEmitterAsyncResource(nameOrOptions)` — an EventEmitter whose -/// listener dispatch runs in one backing AsyncResource scope. -#[no_mangle] -pub unsafe extern "C" fn js_event_emitter_async_resource_new(options: f64) -> Handle { - ensure_runtime_hooks_registered(); - ensure_gc_scanner_registered(); - let name = event_emitter_async_resource_name(options); - let async_options = if JsValue::from_bits(options.to_bits()).is_any_string() { - undefined_value() - } else { - options - }; - let async_resource_handle = js_async_resource_new(name, async_options); - let mut emitter = EventEmitterHandle::new(); - emitter.capture_rejections = options_capture_rejections(options); - emitter.async_resource_handle = async_resource_handle; - let emitter_handle = register_event_emitter_handle(emitter); - js_async_resource_set_event_emitter(async_resource_handle, emitter_handle); - emitter_handle -} - -fn async_resource_handle_for_receiver(handle: Handle) -> Option { - get_event_emitter_mut(handle) - .filter(|emitter| emitter.async_resource_handle != 0) - .map(|emitter| emitter.async_resource_handle) - .or_else(|| { - let resource = unsafe { js_event_emitter_async_resource_subclass_backing(handle) }; - (resource != 0).then_some(resource) - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_event_emitter_async_resource_async_id(handle: Handle) -> f64 { - async_resource_handle_for_receiver(handle) - .map(|resource| js_async_resource_async_id(resource)) - .unwrap_or(0.0) -} - -#[no_mangle] -pub unsafe extern "C" fn js_event_emitter_async_resource_trigger_async_id(handle: Handle) -> f64 { - async_resource_handle_for_receiver(handle) - .map(|resource| js_async_resource_trigger_async_id(resource)) - .unwrap_or(0.0) -} - -#[no_mangle] -pub unsafe extern "C" fn js_event_emitter_async_resource_async_resource(handle: Handle) -> f64 { - async_resource_handle_for_receiver(handle) - .map(nanbox_pointer_bits) - .unwrap_or_else(undefined_value) -} - -#[no_mangle] -pub unsafe extern "C" fn js_event_emitter_async_resource_emit_destroy(handle: Handle) -> f64 { - if let Some(resource) = async_resource_handle_for_receiver(handle) { - js_async_resource_emit_destroy(resource); - } - undefined_value() -} - -fn event_emitter_async_id(handle: Handle) -> u64 { - get_event_emitter_mut(handle) - .filter(|emitter| emitter.async_resource_handle != 0) - .map(|emitter| unsafe { js_async_resource_async_id(emitter.async_resource_handle) as u64 }) - .unwrap_or(0) -} - /// `emitter.on(eventName, listener)` — register a listener. /// Also serves as `addListener` (wired at the codegen layer). /// @@ -1297,10 +1179,6 @@ pub unsafe extern "C" fn js_event_emitter_emit( 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 +1232,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. @@ -1389,10 +1260,6 @@ pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) 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 +1312,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 @@ -1949,5 +1809,146 @@ pub unsafe extern "C" fn js_events_once( raw } +/// `events.on(emitter, eventName)` — returns an async-iterable queue of +/// argument arrays. Perry's `for await` lowering already accepts plain arrays +/// as async-iterable inputs, so the implementation backs the iterator with an +/// Array and appends one `[arg]` entry per emitted event. Ported from +/// `perry-stdlib/src/events.rs` (#1557). +/// +/// # Safety +/// +/// `event_name_ptr` must be null or a Perry-runtime `StringHeader`. +#[no_mangle] +pub unsafe extern "C" fn js_events_on( + target_value: f64, + event_name_ptr: *const StringHeader, + options: f64, +) -> *mut ArrayHeader { + ensure_gc_scanner_registered(); + let target = + event_helper_target(target_value).unwrap_or_else(|| throw_invalid_emitter(target_value)); + let queue = js_array_alloc(0); + let Some(event_name) = event_name_from_bits(event_name_ptr as i64) else { + return queue; + }; + let event_name_ptr = string_header_ptr_from_arg(event_name_ptr); + let signal = options_signal_or_throw(options); + if signal.is_some_and(signal_is_aborted) { + js_throw(js_abort_error_value()); + } + let abort_promise = if signal.is_some() { + JsPromise::new().as_raw() + } else { + std::ptr::null_mut() + }; + + let listener = js_closure_alloc(events_on_queue_listener as *const u8, 2); + js_closure_set_capture_ptr(listener, 0, queue as i64); + js_closure_set_capture_ptr(listener, 1, abort_promise as i64); + if !abort_promise.is_null() { + let _ = js_array_push_f64(queue, nanbox_pointer_bits(abort_promise as i64)); + } + + let handle = match target { + EventHelperTarget::EventEmitter(handle) => { + if let Some(emitter) = get_event_emitter_mut(handle) { + emitter.add_listener(handle, &event_name, listener as i64, false, false); + } + handle + } + EventHelperTarget::EventTarget(target) => { + if !event_name_ptr.is_null() { + js_event_target_add_event_listener(target, event_name_ptr, listener as i64); + } + target as Handle + } + 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 _ = call_net_socket_method(handle, "on", &[event, listener_value]); + } + handle + } + EventHelperTarget::Stream(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 _ = js_node_stream_method_on(handle, event, listener_value); + } + handle + } + }; + + if let Some(signal) = signal { + if let Some(signal_ptr) = object_ptr_from_value(signal) { + let abort_listener = js_closure_alloc(events_on_abort_listener as *const u8, 5); + js_closure_set_capture_ptr(abort_listener, 0, handle); + js_closure_set_capture_ptr(abort_listener, 1, listener as i64); + js_closure_set_capture_ptr(abort_listener, 2, signal_ptr as i64); + js_closure_set_capture_ptr(abort_listener, 3, abort_promise as i64); + js_closure_set_capture_ptr(abort_listener, 4, event_name_ptr as i64); + js_abort_signal_add_listener( + signal_ptr as *mut u8, + abort_event_value(), + nanbox_pointer_bits(abort_listener as i64), + ); + } + } + queue +} + +extern "C" fn events_abort_listener_dispose(closure: *const RawClosureHeader) -> f64 { + unsafe { + let signal_ptr = js_closure_get_capture_ptr(closure, 0); + let callback_ptr = js_closure_get_capture_ptr(closure, 1); + if signal_ptr != 0 && callback_ptr != 0 { + js_abort_signal_remove_listener( + signal_ptr as *mut u8, + abort_event_value(), + nanbox_pointer_bits(callback_ptr), + ); + } + } + undefined_value() +} + +/// `events.addAbortListener(signal, listener)` — attach `listener` to the +/// AbortSignal's "abort" event and return a `Disposable`-shaped plain object. +/// +/// # Safety +/// +/// `signal` and `listener` are NaN-boxed JS values, matching codegen's +/// module-helper ABI. +#[no_mangle] +pub unsafe extern "C" fn js_events_add_abort_listener(signal: f64, listener: f64) -> i64 { + let signal = validate_abort_signal_arg(signal, "signal"); + let signal_ptr = object_ptr_from_value(signal).unwrap_or_else(|| { + throw_invalid_arg_type(&invalid_instance_arg_message( + "signal", + "AbortSignal", + signal, + )) + }); + let callback_ptr = validate_event_listener(listener.to_bits() as i64); + + let listener_val = nanbox_pointer_bits(callback_ptr); + js_abort_signal_add_listener(signal_ptr as *mut u8, abort_event_value(), listener_val); + + let dispose_closure = js_closure_alloc(events_abort_listener_dispose as *const u8, 2); + js_closure_set_capture_ptr(dispose_closure, 0, signal_ptr as i64); + js_closure_set_capture_ptr(dispose_closure, 1, callback_ptr); + let dispose_val = nanbox_pointer_bits(dispose_closure as i64); + + let disposable = js_object_alloc(0, 0); + let disposable_val = nanbox_pointer_bits(disposable as i64); + let dispose_key = b"@@__perry_wk_dispose"; + let dispose_key_ptr = js_string_from_bytes(dispose_key.as_ptr(), dispose_key.len() as u32); + let dispose_key_val = f64::from_bits(nanbox_string_bits(dispose_key_ptr)); + let dispose_sym_val = js_symbol_for(dispose_key_val); + js_object_set_symbol_property(disposable_val, dispose_sym_val, dispose_val); + disposable as i64 +} + #[cfg(test)] mod tests; diff --git a/crates/perry-ext-events/src/module_iterators.rs b/crates/perry-ext-events/src/module_iterators.rs index 24e38bb39d..d00b8595b5 100644 --- a/crates/perry-ext-events/src/module_iterators.rs +++ b/crates/perry-ext-events/src/module_iterators.rs @@ -132,138 +132,32 @@ pub(super) unsafe fn first_rest_arg_or_undefined(rest: f64) -> f64 { } } -// `events.on()` state lives in a GC-traced Array captured by the listener and -// iterator closures. Keeping all JS pointers inside that array means the -// extension's existing listener scanner is sufficient across moving GC. -const EVENTS_ON_BUFFER: u32 = 0; -const EVENTS_ON_PENDING: u32 = 1; -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_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_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)); - let pending = js_array_alloc(0); - let pending_root = scope.root_nanbox(nanbox_pointer_bits(pending as i64)); - let state_ptr = || (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader; - let _ = js_array_push_f64(state_ptr(), buffer_root.get()); - let _ = js_array_push_f64(state_ptr(), pending_root.get()); - let _ = js_array_push_f64(state_ptr(), f64::from_bits(0x7FFC_0000_0000_0003)); - 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() -} - -unsafe fn events_on_state_array(state: *mut ArrayHeader, index: u32) -> *mut ArrayHeader { - let value = f64::from_bits(js_array_get(state, index).bits()); - (value.to_bits() & POINTER_MASK) as *mut ArrayHeader -} - -unsafe fn events_on_state_set(state: *mut ArrayHeader, index: u32, value: f64) { - js_array_set(state, index, JsValue::from_bits(value.to_bits())); -} - -pub(super) unsafe fn events_on_state_set_target( - state: *mut ArrayHeader, - handle: Handle, - listener: *mut RawClosureHeader, -) { - events_on_state_set(state, EVENTS_ON_HANDLE, handle as f64); - events_on_state_set( - state, - EVENTS_ON_LISTENER, - nanbox_pointer_bits(listener as i64), - ); -} - -fn events_on_iter_result(value: f64, done: bool) -> f64 { - let scope = TransientRootScope::enter(); - let value_root = scope.root_nanbox(value); - let packed = b"value\0done\0"; - let object = unsafe { - js_object_alloc_with_shape( - EVENTS_ON_ITER_SHAPE_ID, - 2, - packed.as_ptr(), - packed.len() as u32, - ) - }; - let object_root = scope.root_nanbox(nanbox_pointer_bits(object as i64)); - unsafe { - let current = (object_root.get().to_bits() & POINTER_MASK) as *mut ObjectHeader; - js_object_set_field(current, 0, JsValue::from_bits(value_root.get().to_bits())); - js_object_set_field(current, 1, JsValue::from_bool(done)); - } - object_root.get() -} - -fn events_on_resolved(value: f64, done: bool) -> f64 { - unsafe { - let scope = TransientRootScope::enter(); - let result = scope.root_nanbox(events_on_iter_result(value, done)); - let promise = js_promise_new(); - let promise_root = scope.root_addr(promise as i64); - js_promise_resolve(promise_root.get() as *mut Promise, result.get()); - nanbox_pointer_bits(promise_root.get()) - } -} - -fn events_on_finish_pending(state: *mut ArrayHeader, reason: Option) { - unsafe { - let pending = events_on_state_array(state, EVENTS_ON_PENDING); - if pending.is_null() { - return; - } - while js_array_length(pending) > 0 { - let promise = (js_array_shift_f64(pending).to_bits() & POINTER_MASK) as *mut Promise; - if promise.is_null() { - continue; - } - if let Some(reason) = reason { - js_promise_reject(promise, reason); - } else { - js_promise_resolve(promise, events_on_iter_result(undefined_value(), true)); - } - } - } -} - -/// Queue listener for `events.on(...)`. Resolve an already-blocked `next()` -/// immediately, otherwise retain the argument tuple in FIFO order. +/// Queue listener for `events.on(...)` — captures the queue array in +/// slot 0 and pushes `[arg]` onto it for each emitted event. The +/// `for await (... of iter)` loop pulls items off the array as the +/// stream produces them. pub(super) extern "C" fn events_on_queue_listener( closure: *const RawClosureHeader, arg0: f64, ) -> f64 { unsafe { - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if !state.is_null() { - let scope = TransientRootScope::enter(); - let state_root = scope.root_nanbox(nanbox_pointer_bits(state as i64)); + let queue = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; + let abort_promise = js_closure_get_capture_ptr(closure, 1) as *mut Promise; + if !queue.is_null() { 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)); - let current_state = (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader; - let pending = events_on_state_array(current_state, EVENTS_ON_PENDING); - if !pending.is_null() && js_array_length(pending) > 0 { - let promise = - (js_array_shift_f64(pending).to_bits() & POINTER_MASK) as *mut Promise; - if !promise.is_null() { - let promise_root = scope.root_addr(promise as i64); - let result = scope.root_nanbox(events_on_iter_result(args_root.get(), false)); - js_promise_resolve(promise_root.get() as *mut Promise, result.get()); - } + let args_val = nanbox_pointer_bits(args as i64); + if abort_promise.is_null() { + let _ = js_array_push_f64(queue, args_val); } else { - let current_state = (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader; - let buffer = events_on_state_array(current_state, EVENTS_ON_BUFFER); - if !buffer.is_null() { - let _ = js_array_push_f64(buffer, args_root.get()); + let abort_val = nanbox_pointer_bits(abort_promise as i64); + let len = (*queue).length; + if len == 0 { + let _ = js_array_push_f64(queue, args_val); + let _ = js_array_push_f64(queue, abort_val); + } else { + js_array_set(queue, len - 1, JsValue::from_bits(args_val.to_bits())); + let _ = js_array_push_f64(queue, abort_val); } } } @@ -271,148 +165,12 @@ pub(super) extern "C" fn events_on_queue_listener( f64::from_bits(TAG_UNDEFINED_F64_BITS) } -extern "C" fn events_on_next(closure: *const RawClosureHeader) -> f64 { - unsafe { - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if state.is_null() { - return events_on_resolved(undefined_value(), true); - } - let buffer = events_on_state_array(state, EVENTS_ON_BUFFER); - if !buffer.is_null() && js_array_length(buffer) > 0 { - return events_on_resolved(js_array_shift_f64(buffer), false); - } - let abort = f64::from_bits(js_array_get(state, EVENTS_ON_ABORT).bits()); - if abort.to_bits() != TAG_UNDEFINED_F64_BITS { - let promise = js_promise_new(); - js_promise_reject(promise, abort); - return nanbox_pointer_bits(promise as i64); - } - let done = f64::from_bits(js_array_get(state, EVENTS_ON_DONE).bits()); - if done.to_bits() == TAG_TRUE_F64_BITS { - return events_on_resolved(undefined_value(), true); - } - let promise = js_promise_new(); - let pending = events_on_state_array(state, EVENTS_ON_PENDING); - if !pending.is_null() { - let _ = js_array_push_f64(pending, nanbox_pointer_bits(promise as i64)); - } - nanbox_pointer_bits(promise as i64) - } -} - -extern "C" fn events_on_return(closure: *const RawClosureHeader) -> f64 { - unsafe { - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if state.is_null() { - 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 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); - } - } - events_on_finish_pending(state, None); - events_on_resolved(undefined_value(), true) - } -} - -extern "C" fn events_on_iterator_self(closure: *const RawClosureHeader) -> f64 { - unsafe { js_closure_get_capture_f64(closure, 0) } -} - -extern "C" fn events_on_async_iterator(closure: *const RawClosureHeader) -> f64 { - unsafe { - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - let scope = TransientRootScope::enter(); - let state_root = scope.root_nanbox(nanbox_pointer_bits(state as i64)); - let packed = b"next\0return\0"; - let object = js_object_alloc_with_shape( - EVENTS_ON_ITER_SHAPE_ID + 1, - 2, - packed.as_ptr(), - packed.len() as u32, - ); - let object_root = scope.root_nanbox(nanbox_pointer_bits(object as i64)); - let next = js_closure_alloc(events_on_next as *const u8, 1); - js_closure_set_capture_ptr(next, 0, (state_root.get().to_bits() & POINTER_MASK) as i64); - let next_root = scope.root_addr(next as i64); - js_object_set_field( - (object_root.get().to_bits() & POINTER_MASK) as *mut ObjectHeader, - 0, - JsValue::from_object_ptr(next_root.get() as *mut u8), - ); - let return_fn = js_closure_alloc(events_on_return as *const u8, 1); - js_closure_set_capture_ptr( - return_fn, - 0, - (state_root.get().to_bits() & POINTER_MASK) as i64, - ); - let return_root = scope.root_addr(return_fn as i64); - js_object_set_field( - (object_root.get().to_bits() & POINTER_MASK) as *mut ObjectHeader, - 1, - JsValue::from_object_ptr(return_root.get() as *mut u8), - ); - - let iterator = object_root.get(); - let iterator_root = scope.root_nanbox(iterator); - let symbol = js_symbol_well_known_async_iterator(); - let self_fn = js_closure_alloc(events_on_iterator_self as *const u8, 1); - js_closure_set_capture_f64(self_fn, 0, iterator_root.get()); - js_object_set_symbol_property( - iterator_root.get(), - symbol, - nanbox_pointer_bits(self_fn as i64), - ); - iterator_root.get() - } -} - -pub(super) unsafe fn events_on_install_async_iterator( - queue: *mut ArrayHeader, - state: *mut ArrayHeader, -) { - let scope = TransientRootScope::enter(); - let queue_root = scope.root_nanbox(nanbox_pointer_bits(queue as i64)); - let state_root = scope.root_nanbox(nanbox_pointer_bits(state as i64)); - js_register_closure_arity(events_on_next as *const u8, 0); - js_register_closure_arity(events_on_return as *const u8, 0); - js_register_closure_arity(events_on_iterator_self as *const u8, 0); - js_register_closure_arity(events_on_async_iterator as *const u8, 0); - let closure = js_closure_alloc(events_on_async_iterator as *const u8, 1); - js_closure_set_capture_ptr( - closure, - 0, - (state_root.get().to_bits() & POINTER_MASK) as i64, - ); - js_object_set_symbol_property( - queue_root.get(), - js_symbol_well_known_async_iterator(), - nanbox_pointer_bits(closure as i64), - ); -} - -/// A configured close event ends the iterator after already-buffered events. -pub(super) extern "C" fn events_on_close_listener(closure: *const RawClosureHeader) -> f64 { - unsafe { - let state = js_closure_get_capture_ptr(closure, 0) as *mut ArrayHeader; - if !state.is_null() { - events_on_state_set(state, EVENTS_ON_DONE, f64::from_bits(TAG_TRUE_F64_BITS)); - events_on_finish_pending(state, None); - } - } - undefined_value() -} - pub(super) extern "C" fn events_on_abort_listener(closure: *const RawClosureHeader) -> f64 { unsafe { let handle = js_closure_get_capture_ptr(closure, 0) as Handle; let data_listener = js_closure_get_capture_ptr(closure, 1); let signal_ptr = js_closure_get_capture_ptr(closure, 2) as *mut u8; - let state = js_closure_get_capture_ptr(closure, 3) as *mut ArrayHeader; + let abort_promise = js_closure_get_capture_ptr(closure, 3) as *mut Promise; let event_name_ptr = js_closure_get_capture_ptr(closure, 4) as *const StringHeader; if let Some(emitter) = get_event_emitter_mut(handle) { @@ -434,11 +192,8 @@ pub(super) extern "C" fn events_on_abort_listener(closure: *const RawClosureHead nanbox_pointer_bits(closure as i64), ); } - if !state.is_null() { - let reason = js_abort_error_value(); - events_on_state_set(state, EVENTS_ON_ABORT, reason); - events_on_state_set(state, EVENTS_ON_DONE, f64::from_bits(TAG_TRUE_F64_BITS)); - events_on_finish_pending(state, Some(reason)); + if !abort_promise.is_null() { + js_promise_reject(abort_promise, js_abort_error_value()); } } undefined_value() diff --git a/crates/perry-ext-events/src/module_on.rs b/crates/perry-ext-events/src/module_on.rs deleted file mode 100644 index c6b7ff789e..0000000000 --- a/crates/perry-ext-events/src/module_on.rs +++ /dev/null @@ -1,207 +0,0 @@ -use super::*; - -#[no_mangle] -pub unsafe extern "C" fn js_events_on( - target_value: f64, - event_name_ptr: *const StringHeader, - options: f64, -) -> *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 queue = js_array_alloc(0); - let queue_root = root_scope.root_nanbox(nanbox_pointer_bits(queue as i64)); - let state = events_on_state_new(); - let state_root = root_scope.root_nanbox(nanbox_pointer_bits(state as i64)); - events_on_install_async_iterator( - (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 { - return (queue_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader; - }; - let event_name_ptr = string_header_ptr_from_arg(event_name_ptr); - let signal = options_signal_or_throw(options); - if signal.is_some_and(signal_is_aborted) { - js_throw(js_abort_error_value()); - } - let listener = js_closure_alloc(events_on_queue_listener as *const u8, 1); - js_closure_set_capture_ptr( - listener, - 0, - (state_root.get().to_bits() & POINTER_MASK) as i64, - ); - let listener_root = root_scope.root_addr(listener as i64); - let handle = 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 - } - 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 - } - 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 _ = call_net_socket_method(handle, "on", &[event, listener_value]); - } - handle - } - EventHelperTarget::Stream(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_root.get()); - let _ = js_node_stream_method_on(handle, event, listener_value); - } - handle - } - }; - events_on_state_set_target( - (state_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader, - handle, - listener_root.get() as *mut RawClosureHeader, - ); - if let Some(close) = get_object_property(options, b"close") { - if js_array_is_array(close).to_bits() == TAG_TRUE_F64_BITS { - let close_array = (close.to_bits() & POINTER_MASK) as *mut ArrayHeader; - if !close_array.is_null() { - for index in 0..js_array_length(close_array) { - let close_value = f64::from_bits(js_array_get(close_array, index).bits()); - let Some(close_name) = event_name_from_bits(close_value.to_bits() as i64) - else { - continue; - }; - let close_ptr = - js_string_from_bytes(close_name.as_ptr(), close_name.len() as u32); - let close_listener = js_closure_alloc(events_on_close_listener as *const u8, 1); - js_closure_set_capture_ptr( - close_listener, - 0, - (state_root.get().to_bits() & POINTER_MASK) as i64, - ); - let close_listener_root = root_scope.root_addr(close_listener as i64); - match target { - EventHelperTarget::EventEmitter(emitter_handle) => { - if let Some(emitter) = get_event_emitter_mut(emitter_handle) { - emitter.add_listener( - emitter_handle, - &close_name, - close_listener_root.get(), - true, - false, - ); - } - } - EventHelperTarget::EventTarget(event_target) => { - js_event_target_add_event_listener( - event_target, - close_ptr, - close_listener_root.get(), - ); - } - EventHelperTarget::NetSocket(handle) - | EventHelperTarget::NativeHandle(handle) => { - let event = f64::from_bits(nanbox_string_bits(close_ptr)); - let callback = nanbox_pointer_bits(close_listener_root.get()); - let _ = call_net_socket_method(handle, "once", &[event, callback]); - } - EventHelperTarget::Stream(handle) => { - let event = f64::from_bits(nanbox_string_bits(close_ptr)); - let callback = nanbox_pointer_bits(close_listener_root.get()); - let _ = js_node_stream_method_once(handle, event, callback); - } - } - } - } - } - } - if let Some(signal) = signal { - if let Some(signal_ptr) = object_ptr_from_value(signal) { - let abort_listener = js_closure_alloc(events_on_abort_listener as *const u8, 5); - js_closure_set_capture_ptr(abort_listener, 0, handle); - js_closure_set_capture_ptr(abort_listener, 1, listener_root.get()); - js_closure_set_capture_ptr(abort_listener, 2, signal_ptr as i64); - js_closure_set_capture_ptr( - abort_listener, - 3, - (state_root.get().to_bits() & POINTER_MASK) as i64, - ); - js_closure_set_capture_ptr(abort_listener, 4, event_name_ptr as i64); - js_abort_signal_add_listener( - signal_ptr as *mut u8, - abort_event_value(), - nanbox_pointer_bits(abort_listener as i64), - ); - } - } - (queue_root.get().to_bits() & POINTER_MASK) as *mut ArrayHeader -} - -extern "C" fn events_abort_listener_dispose(closure: *const RawClosureHeader) -> f64 { - unsafe { - let signal_ptr = js_closure_get_capture_ptr(closure, 0); - let callback_ptr = js_closure_get_capture_ptr(closure, 1); - if signal_ptr != 0 && callback_ptr != 0 { - js_abort_signal_remove_listener( - signal_ptr as *mut u8, - abort_event_value(), - nanbox_pointer_bits(callback_ptr), - ); - } - } - undefined_value() -} - -#[no_mangle] -pub unsafe extern "C" fn js_events_add_abort_listener(signal: f64, listener: f64) -> i64 { - let root_scope = TransientRootScope::enter(); - let signal_root = root_scope.root_nanbox(signal); - let listener_root = root_scope.root_nanbox(listener); - let signal = validate_abort_signal_arg(signal_root.get(), "signal"); - let signal_ptr = object_ptr_from_value(signal_root.get()).unwrap_or_else(|| { - throw_invalid_arg_type(&invalid_instance_arg_message( - "signal", - "AbortSignal", - signal, - )) - }); - let callback_ptr = validate_event_listener(listener_root.get().to_bits() as i64); - js_abort_signal_add_listener( - signal_ptr as *mut u8, - abort_event_value(), - nanbox_pointer_bits(callback_ptr), - ); - let dispose_closure = js_closure_alloc(events_abort_listener_dispose as *const u8, 2); - let dispose_closure_root = root_scope.root_addr(dispose_closure as i64); - let signal_ptr = object_ptr_from_value(signal_root.get()) - .expect("validated AbortSignal remained a rooted object"); - let callback_ptr = validate_event_listener(listener_root.get().to_bits() as i64); - js_closure_set_capture_ptr( - dispose_closure_root.get() as *mut RawClosureHeader, - 0, - signal_ptr as i64, - ); - js_closure_set_capture_ptr( - dispose_closure_root.get() as *mut RawClosureHeader, - 1, - callback_ptr, - ); - let disposable = js_object_alloc(0, 0); - let disposable_root = root_scope.root_addr(disposable as i64); - let dispose_key = b"@@__perry_wk_dispose"; - let dispose_key_ptr = js_string_from_bytes(dispose_key.as_ptr(), dispose_key.len() as u32); - let dispose_sym_val = js_symbol_for(f64::from_bits(nanbox_string_bits(dispose_key_ptr))); - js_object_set_symbol_property( - nanbox_pointer_bits(disposable_root.get()), - dispose_sym_val, - nanbox_pointer_bits(dispose_closure_root.get()), - ); - disposable_root.get() -} diff --git a/crates/perry-ext-events/src/target_helpers.rs b/crates/perry-ext-events/src/target_helpers.rs index f8fbcfe2a3..329e5ca885 100644 --- a/crates/perry-ext-events/src/target_helpers.rs +++ b/crates/perry-ext-events/src/target_helpers.rs @@ -1,6 +1,5 @@ use super::*; -#[derive(Clone, Copy)] pub(super) enum EventHelperTarget { EventEmitter(Handle), EventTarget(*mut u8), diff --git a/crates/perry-ext-events/src/tests.rs b/crates/perry-ext-events/src/tests.rs index 44f1767e9a..74c0a33c9b 100644 --- a/crates/perry-ext-events/src/tests.rs +++ b/crates/perry-ext-events/src/tests.rs @@ -15,13 +15,6 @@ impl GcTestGuard { let lock = GC_TEST_LOCK .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") }; perry_runtime::gc::js_gc_write_barriers_emitted(1); let frame = perry_runtime::gc::js_shadow_frame_push(0); Self { frame, _lock: lock } @@ -32,8 +25,6 @@ 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") }; } } @@ -190,7 +181,6 @@ fn gc_mutable_scanner_rewrites_listener_and_pending_promise_roots() { max_listeners: 10.0, capture_rejections: false, domain_handle: None, - async_resource_handle: 0, }); let _ = perry_runtime::gc::gc_collect_minor(); diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index c020357abb..a17307bd91 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -100,9 +100,7 @@ use response_headers::build_response_headers_object; mod request_headers; use request_headers::headers_from_options; -mod pending_dispatch; mod root_scanner; -pub use pending_dispatch::js_http_process_pending; use root_scanner::scan_http_roots; use bytes::Bytes; @@ -425,7 +423,6 @@ fn map_to_js_object(map: &HashMap) -> f64 { // ------------------------------------------------------------------ pub struct ClientRequestHandle { - async_id: u64, method: String, url: String, headers: HashMap, @@ -670,11 +667,7 @@ fn make_request_handle( agent_handle: Handle, agent_key: String, ) -> Handle { - let async_id = unsafe { - js_async_hooks_provider_init(b"HTTPCLIENTREQUEST".as_ptr(), b"HTTPCLIENTREQUEST".len()) - }; let handle = register_handle(ClientRequestHandle { - async_id, method, url, headers, @@ -751,44 +744,6 @@ fn make_request_handle( handle } -extern "C" { - fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64; - 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 pending_request_handle(event: &PendingHttpEvent) -> Handle { - match event { - PendingHttpEvent::Socket { request_handle } - | PendingHttpEvent::SignalAbort { request_handle } - | PendingHttpEvent::Response { request_handle, .. } - | PendingHttpEvent::ResponseHead { request_handle, .. } - | PendingHttpEvent::ResponseChunk { request_handle, .. } - | PendingHttpEvent::ResponseEnd { request_handle } - | PendingHttpEvent::Error { request_handle, .. } - | PendingHttpEvent::TransportError { request_handle, .. } - | PendingHttpEvent::Timeout { request_handle } - | PendingHttpEvent::Abort { request_handle } - | PendingHttpEvent::Flushed { request_handle } - | PendingHttpEvent::Continue { request_handle } - | PendingHttpEvent::DeferredArmContinue { request_handle } => *request_handle, - PendingHttpEvent::AgentIdleExpire { .. } => 0, - } -} - -fn terminal_http_event(event: &PendingHttpEvent) -> bool { - matches!( - event, - PendingHttpEvent::SignalAbort { .. } - | PendingHttpEvent::Response { .. } - | PendingHttpEvent::ResponseEnd { .. } - | PendingHttpEvent::Error { .. } - | PendingHttpEvent::TransportError { .. } - | PendingHttpEvent::Abort { .. } - ) -} - /// Parse the client-side TLS options (#4906) off a request options value /// and store them on the freshly-built request handle. A no-op for /// string-URL requests / plain http (parse yields the default). @@ -1878,6 +1833,142 @@ pub extern "C" fn js_http_has_pending() -> i32 { .unwrap_or(0) } +/// Drain the pending HTTP-event queue and fire user callbacks. Called +/// from codegen's event-loop tick. Returns count of events drained. +#[no_mangle] +pub unsafe extern "C" fn js_http_process_pending() -> i32 { + // Process events ONE AT A TIME, re-reading the shared queue each iteration + // rather than draining the whole batch into a local Vec up front. + // + // Why (#5783 follow-up): a response handler may RE-ENTER the event loop — + // e.g. a `ResponseHead` invokes an async response callback that drives a + // `for await` / `.toArray()` over a `res.pipe(PassThrough())` body. That + // consumer's `await` block-waits, pumping the loop (which re-enters this + // function), and its resolution depends on the body arriving via the LATER + // `ResponseChunk`/`ResponseEnd` events of this same batch. If those were + // already drained into a local Vec, the re-entrant pump would find an empty + // `HTTP_PENDING_EVENTS` and the consumer would deadlock (empty body / hang). + // Keeping unprocessed events in the shared queue lets the re-entrant drain + // deliver them. FIFO `remove(0)` preserves event order; each event is taken + // by exactly one (outer or re-entrant) frame, so there is no double-dispatch. + let mut count = 0i32; + loop { + let ev = match HTTP_PENDING_EVENTS.lock() { + Ok(mut q) => { + if q.is_empty() { + None + } else { + Some(q.remove(0)) + } + } + Err(_) => return count, + }; + let Some(ev) = ev else { + break; + }; + count += 1; + match ev { + PendingHttpEvent::Socket { request_handle } => { + client_events::fire_request_socket_event(request_handle); + } + PendingHttpEvent::SignalAbort { request_handle } => { + client_abort::handle_request_signal_abort(request_handle); + } + PendingHttpEvent::AgentIdleExpire { + agent_handle, + key, + socket, + generation, + } => { + agent::expire_free_socket(agent_handle, &key, socket, generation); + } + PendingHttpEvent::Response { + request_handle, + status, + status_message, + headers, + trailers, + body, + } => { + client_events::handle_response_event( + request_handle, + status, + status_message, + headers, + trailers, + body, + ); + } + PendingHttpEvent::ResponseHead { + request_handle, + status, + status_message, + headers, + } => { + client_events::handle_response_head_event( + request_handle, + status, + status_message, + headers, + ); + } + PendingHttpEvent::ResponseChunk { + request_handle, + chunk, + } => { + client_events::handle_response_chunk_event(request_handle, chunk); + } + PendingHttpEvent::ResponseEnd { request_handle } => { + client_events::handle_response_end_event(request_handle); + } + PendingHttpEvent::Error { + request_handle, + error_message, + } => { + client_events::handle_error_event(request_handle, &error_message); + } + PendingHttpEvent::TransportError { + request_handle, + message, + code, + syscall, + errno, + } => { + client_events::handle_transport_error_event( + request_handle, + &message, + &code, + &syscall, + errno, + ); + } + PendingHttpEvent::Timeout { request_handle } => { + client_events::handle_timeout_event(request_handle); + } + PendingHttpEvent::Abort { request_handle } => { + client_events::fire_request_event_listeners(request_handle, "abort"); + client_events::fire_request_close_once(request_handle); + finish_agent_request(request_handle, false); + } + PendingHttpEvent::Flushed { request_handle } => { + client_events::handle_flushed_event(request_handle); + } + PendingHttpEvent::Continue { request_handle } => { + // #5080 — the server sent an interim `100 Continue`; fire the + // request's `'continue'` listeners (the canonical handler then + // sends the withheld body via `req.end(...)`). + client_events::fire_request_event_listeners(request_handle, "continue"); + } + PendingHttpEvent::DeferredArmContinue { request_handle } => { + // #5080 — next-tick arming (see the enum variant docs). + continue_client::arm_expect_continue(request_handle); + } + } + } + + count +} + // ------------------------------------------------------------------ // Tests // ------------------------------------------------------------------ diff --git a/crates/perry-ext-http/src/pending_dispatch.rs b/crates/perry-ext-http/src/pending_dispatch.rs deleted file mode 100644 index c2a1bb24cd..0000000000 --- a/crates/perry-ext-http/src/pending_dispatch.rs +++ /dev/null @@ -1,120 +0,0 @@ -use super::*; - -/// Drain the pending HTTP-event queue and fire user callbacks. Events remain -/// in the shared queue until selected so re-entrant event-loop pumps can make -/// progress on later response chunks (#5783). -#[no_mangle] -pub unsafe extern "C" fn js_http_process_pending() -> i32 { - let mut count = 0i32; - loop { - let ev = match HTTP_PENDING_EVENTS.lock() { - Ok(mut q) => { - if q.is_empty() { - None - } else { - Some(q.remove(0)) - } - } - Err(_) => return count, - }; - let Some(ev) = ev else { break }; - count += 1; - let request_handle = pending_request_handle(&ev); - let terminal = terminal_http_event(&ev); - let async_id = with_handle_mut::(request_handle, |request| { - request.async_id - }) - .unwrap_or(0); - if async_id != 0 { - js_async_hooks_provider_enter(async_id); - } - match ev { - PendingHttpEvent::Socket { request_handle } => { - client_events::fire_request_socket_event(request_handle); - } - PendingHttpEvent::SignalAbort { request_handle } => { - client_abort::handle_request_signal_abort(request_handle); - } - PendingHttpEvent::AgentIdleExpire { - agent_handle, - key, - socket, - generation, - } => agent::expire_free_socket(agent_handle, &key, socket, generation), - PendingHttpEvent::Response { - request_handle, - status, - status_message, - headers, - trailers, - body, - } => client_events::handle_response_event( - request_handle, - status, - status_message, - headers, - trailers, - body, - ), - PendingHttpEvent::ResponseHead { - request_handle, - status, - status_message, - headers, - } => client_events::handle_response_head_event( - request_handle, - status, - status_message, - headers, - ), - PendingHttpEvent::ResponseChunk { - request_handle, - chunk, - } => client_events::handle_response_chunk_event(request_handle, chunk), - PendingHttpEvent::ResponseEnd { request_handle } => { - client_events::handle_response_end_event(request_handle); - } - PendingHttpEvent::Error { - request_handle, - error_message, - } => client_events::handle_error_event(request_handle, &error_message), - PendingHttpEvent::TransportError { - request_handle, - message, - code, - syscall, - errno, - } => client_events::handle_transport_error_event( - request_handle, - &message, - &code, - &syscall, - errno, - ), - PendingHttpEvent::Timeout { request_handle } => { - client_events::handle_timeout_event(request_handle); - } - PendingHttpEvent::Abort { request_handle } => { - client_events::fire_request_event_listeners(request_handle, "abort"); - client_events::fire_request_close_once(request_handle); - finish_agent_request(request_handle, false); - } - PendingHttpEvent::Flushed { request_handle } => { - client_events::handle_flushed_event(request_handle); - } - PendingHttpEvent::Continue { request_handle } => { - client_events::fire_request_event_listeners(request_handle, "continue"); - } - PendingHttpEvent::DeferredArmContinue { request_handle } => { - continue_client::arm_expect_continue(request_handle); - } - } - if async_id != 0 { - js_async_hooks_provider_leave(async_id); - if terminal { - js_async_hooks_provider_destroy(async_id); - } - } - } - count -} diff --git a/crates/perry-ext-http/src/server/server.rs b/crates/perry-ext-http/src/server/server.rs index 16af91d477..0aa207bd5f 100644 --- a/crates/perry-ext-http/src/server/server.rs +++ b/crates/perry-ext-http/src/server/server.rs @@ -72,7 +72,6 @@ pub(crate) fn apply_accept_no_delay(stream: &tokio::net::TcpStream, no_delay: bo /// Backing struct for an `http.Server` JS-side handle. pub struct HttpServer { - pub async_id: u64, /// User's `(req, res) => ...` handler. Stored as raw `i64`; the /// GC root scanner pins it across malloc-triggered sweeps. pub handler: i64, @@ -169,7 +168,6 @@ impl HttpServer { /// (https / http2 / test fixtures). pub fn with_handler(handler: i64) -> Self { Self { - async_id: 0, handler, listeners: HashMap::new(), once_listeners: HashMap::new(), @@ -776,8 +774,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); @@ -942,7 +938,6 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr // "Cannot read properties of undefined". The pump fires both with // `this` bound to the server (#2132), via `drain_deferred_listen_events`. if let Some(s) = get_handle_mut::(server_handle) { - s.async_id = server_async_id; queue_deferred_listening_emit(s, callback); } @@ -1484,6 +1479,7 @@ pub extern "C" fn js_node_http_server_has_active() -> i32 { /// `(req, res) => res.end(...)` shape that the load-bearing #604 /// fixture uses works without this — the response oneshot fires /// synchronously from inside `js_node_http_res_end`. + #[no_mangle] pub extern "C" fn js_node_http_server_process_pending() -> i32 { let mut count = 0i32; @@ -1671,15 +1667,6 @@ pub(crate) fn try_recv_pending_nonblocking(server_handle: i64) -> Option &mut HttpServer, { - let (cbs, async_id): (Vec, u64) = match get_handle_mut::(server_handle) { + let cbs: Vec = match get_handle_mut::(server_handle) { Some(t) => { let s = base_of(t); if !std::mem::take(&mut s.pending_listening_emit) { @@ -64,13 +64,10 @@ where } } } - (snapshot, s.async_id) + snapshot } 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. @@ -90,9 +87,6 @@ where fired += 1; } } - if async_id != 0 { - unsafe { crate::js_async_hooks_provider_leave(async_id) }; - } fired } diff --git a/crates/perry-ext-http/src/tests.rs b/crates/perry-ext-http/src/tests.rs index 34b9a42948..797f796db2 100644 --- a/crates/perry-ext-http/src/tests.rs +++ b/crates/perry-ext-http/src/tests.rs @@ -88,7 +88,6 @@ fn gc_mutable_scanner_rewrites_request_response_listener_roots() { }], ); let request_handle = register_handle(ClientRequestHandle { - async_id: 0, method: "GET".to_string(), url: "http://localhost/".to_string(), headers: HashMap::new(), @@ -176,7 +175,6 @@ fn gc_mutable_scanner_rewrites_request_response_listener_roots() { /// no live codegen — only the handle registry the other tests already use. fn drain_streamed_body(chunks: &[&[u8]]) -> Vec { let request_handle = register_handle(ClientRequestHandle { - async_id: 0, method: "GET".to_string(), url: "http://localhost/".to_string(), headers: HashMap::new(), @@ -348,7 +346,6 @@ fn dispatch_request_stays_visible_to_exit_gate_until_response_queued() { }); let request_handle = register_handle(ClientRequestHandle { - async_id: 0, method: "GET".to_string(), url: format!("http://127.0.0.1:{port}/"), headers: HashMap::new(), diff --git a/crates/perry-ext-net/src/adopt.rs b/crates/perry-ext-net/src/adopt.rs index d3d8f331ad..e2f5a39952 100644 --- a/crates/perry-ext-net/src/adopt.rs +++ b/crates/perry-ext-net/src/adopt.rs @@ -41,9 +41,6 @@ pub fn adopt_upgraded_tcp_stream(stream: tokio::net::TcpStream) -> i64 { statics::sockets().lock().unwrap().insert( id, SocketState { - tcp_async_id: 0, - connect_async_id: 0, - shutdown_async_id: 0, cmd_tx: tx, pending_rx: None, is_open: true, diff --git a/crates/perry-ext-net/src/dispatch.rs b/crates/perry-ext-net/src/dispatch.rs index 8ecfd7b829..76afc8fbe8 100644 --- a/crates/perry-ext-net/src/dispatch.rs +++ b/crates/perry-ext-net/src/dispatch.rs @@ -207,33 +207,11 @@ unsafe fn socket_method(handle: i64, method: &str, args: &[f64]) -> Option // #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(); - } - } 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(); - } - } 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 deleted file mode 100644 index df109d4016..0000000000 --- a/crates/perry-ext-net/src/gc_roots.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! GC-root registration and scanning for native net handles. - -use super::*; - -static NET_GC_REGISTERED: std::sync::Once = std::sync::Once::new(); - -extern "C" { - fn js_register_net_socket_handle_probe(f: unsafe extern "C" fn(i64) -> bool); -} - -unsafe extern "C" fn ext_net_socket_handle_probe(handle: i64) -> bool { - is_net_socket_handle(handle) -} - -/// Register the net GC root scanner exactly once. Safe to call from any -/// `js_net_*` entry point on the main thread. -pub(crate) fn ensure_gc_scanner_registered() { - NET_GC_REGISTERED.call_once(|| { - gc_register_mutable_root_scanner_named("perry-ext-net", scan_net_roots); - unsafe { - js_register_net_socket_handle_probe(ext_net_socket_handle_probe); - } - // #2154 — publish the raw-consumer vtable for perry-ext-http (runs on - // the first net FFI entry, before http could reference a socket). - raw_bridge::register(); - }); -} - -/// GC root scanner for net.Socket event listener closures. -/// -/// Without this, any GC cycle between `.on()` and the next dispatch would -/// sweep the closure; the next `closure.call*()` would dereference freed -/// memory. Same pattern as perry-stdlib's net mod and perry-ext-events. -pub(crate) fn scan_net_roots(visitor: &mut GcRootVisitor<'_>) { - if let Ok(mut listeners) = statics::listeners().lock() { - for per_socket in listeners.values_mut() { - for cb_vec in per_socket.values_mut() { - for cb in cb_vec.iter_mut() { - visitor.visit_i64_slot(cb); - } - } - } - } - // `once_flags()` keys membership by the closure's ADDRESS BITS. The - // canonical copy in `listeners()` above keeps the closure alive and is - // rewritten when the copying GC moves it — but a `HashSet` element - // cannot be rewritten in place, so without this rebuild the set still - // holds the OLD address after evacuation: the once-membership test in - // `lifecycle.rs` then misses, the listener is never auto-removed, and a - // "once" callback fires on every subsequent event. Drain, forward each - // element through the visitor, and reinsert under the new identity. - if let Ok(mut once) = statics::once_flags().lock() { - for per_handle in once.values_mut() { - for set in per_handle.values_mut() { - let old: Vec = set.drain().collect(); - for mut cb in old { - visitor.visit_i64_slot(&mut cb); - set.insert(cb); - } - } - } - } - // #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/handle_exports.rs b/crates/perry-ext-net/src/handle_exports.rs deleted file mode 100644 index 4c8612da70..0000000000 --- a/crates/perry-ext-net/src/handle_exports.rs +++ /dev/null @@ -1,87 +0,0 @@ -use super::*; - -pub(super) fn listeners_for(id: i64, event: &str) -> Vec { - statics::listeners() - .lock() - .unwrap() - .get(&id) - .and_then(|m| m.get(event).cloned()) - .unwrap_or_default() -} - -#[no_mangle] -pub extern "C" fn js_net_has_pending() -> i32 { - server_state::has_active_handles() as i32 -} - -pub fn is_net_socket_handle(handle: i64) -> bool { - statics::sockets().lock().unwrap().contains_key(&handle) -} - -pub fn is_net_server_handle(handle: i64) -> bool { - statics::servers().lock().unwrap().contains_key(&handle) -} - -#[no_mangle] -pub extern "C" fn js_net_server_listening(handle: i64) -> i32 { - statics::servers() - .lock() - .ok() - .and_then(|servers| { - servers - .get(&handle) - .map(|server| i32::from(server.listening)) - }) - .unwrap_or(0) -} - -/// `server.on(event, cb)` — register a server-level listener for -/// `'connection'`, `'listening'`, `'close'`, or `'error'`. -/// -/// # Safety -/// -/// `event_ptr` must be null or a Perry-runtime `StringHeader`. `cb` -/// is a raw `*const ClosureHeader` cast to `i64`. -#[no_mangle] -pub unsafe extern "C" fn js_net_server_on(handle: i64, event_ptr: i64, cb: i64) { - ensure_gc_scanner_registered(); - let event = match string_from_header_i64(event_ptr) { - Some(e) => e, - None => return, - }; - let mut listeners = statics::listeners().lock().unwrap(); - let entry = listeners.entry(handle).or_default(); - entry.entry(event).or_default().push(cb); -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_socket_on(handle: i64, event_ptr: i64, cb: i64) { - js_net_socket_on(handle, event_ptr, cb) -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_socket_once(handle: i64, event_ptr: i64, cb: i64) -> i64 { - js_net_socket_once(handle, event_ptr, cb) -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_socket_remove_listener( - handle: i64, - event_ptr: i64, - cb: i64, -) -> i64 { - js_net_socket_remove_listener(handle, event_ptr, cb) -} - -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_socket_remove_all_listeners( - handle: i64, - event_ptr: i64, -) -> i64 { - js_net_socket_remove_all_listeners(handle, event_ptr) -} - -#[no_mangle] -pub extern "C" fn js_ext_net_is_server_handle(handle: i64) -> i32 { - i32::from(is_net_server_handle(handle)) -} diff --git a/crates/perry-ext-net/src/ipc.rs b/crates/perry-ext-net/src/ipc.rs index b501471f3a..a13eb49a53 100644 --- a/crates/perry-ext-net/src/ipc.rs +++ b/crates/perry-ext-net/src/ipc.rs @@ -32,9 +32,6 @@ fn allocate_socket() -> (i64, mpsc::UnboundedReceiver) { statics::sockets().lock().unwrap().insert( id, SocketState { - tcp_async_id: 0, - connect_async_id: 0, - shutdown_async_id: 0, cmd_tx: tx, pending_rx: None, is_open: false, @@ -102,9 +99,6 @@ pub(crate) fn register_accepted_transport( statics::sockets().lock().unwrap().insert( socket_id, SocketState { - tcp_async_id: 0, - connect_async_id: 0, - shutdown_async_id: 0, cmd_tx: tx, pending_rx: None, is_open: true, diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index 4594c3459b..54ae80a332 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -40,6 +40,7 @@ use perry_ffi::{ }; use std::collections::HashMap; use std::net::SocketAddr; +use std::pin::Pin; use std::sync::{Arc, Mutex}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -55,7 +56,7 @@ mod ip; // `BytesMut` per read. See `buffer_pool.rs` for the rationale. mod buffer_pool; mod tls; -pub use tls::{js_ext_tls_connect, js_tls_connect}; +pub use tls::js_tls_connect; // #2131 — lifecycle / EventEmitter surface for `net.Socket` + `net.Server` // (once / off / removeAllListeners / listenerCount / eventNames / // resetAndDestroy, plus `socket.address()`). Re-exports keep the @@ -70,24 +71,15 @@ mod handle_ids; pub(crate) use handle_ids::{next_id, next_id_or_throw}; mod dispatch; mod dispatch_custody; -mod gc_roots; mod ipc; -pub(crate) use gc_roots::ensure_gc_scanner_registered; mod socket_emit; pub use socket_emit::{ js_ext_net_register_http_agent_socket_event_hook, js_ext_net_set_http_agent_phase, js_ext_net_socket_emit, js_ext_net_socket_emit_abort_error, }; -mod task_spawn; -use task_spawn::spawn_socket_runner; // #2154 — raw-consumer bridge so perry-ext-http can drive an HTTP exchange // over a socket produced by `agent.createConnection` (split out for the gate). -mod provider_lifecycle; mod raw_bridge; -use provider_lifecycle::{ - event_provider_id, init_provider, init_provider_with_trigger, prepare_event_provider, - ProviderScope, -}; use raw_bridge::RawReadState; // #2013 — chainable option-setter no-ops + Node arg-validation bridge to // perry-runtime (split out to keep lib.rs under the 2000-line gate). The @@ -230,7 +222,6 @@ pub(crate) mod statics { /// reusing the socket listener map keeps the GC scanner walk single- /// pass instead of needing a second per-server scanner. pub(crate) struct ServerState { - pub async_id: u64, /// Set by `.listen()`, dropped by `.close()`. Send on this channel /// to break the accept loop's `tokio::select!`. pub shutdown_tx: Option>, @@ -247,10 +238,70 @@ pub(crate) struct ServerState { pub drop_max_connection: Option, } +static NET_GC_REGISTERED: std::sync::Once = std::sync::Once::new(); + +extern "C" { + fn js_register_net_socket_handle_probe(f: unsafe extern "C" fn(i64) -> bool); +} + +unsafe extern "C" fn ext_net_socket_handle_probe(handle: i64) -> bool { + is_net_socket_handle(handle) +} + +/// Register the net GC root scanner exactly once. Safe to call from any +/// `js_net_*` entry point on the main thread. +pub(crate) fn ensure_gc_scanner_registered() { + NET_GC_REGISTERED.call_once(|| { + gc_register_mutable_root_scanner_named("perry-ext-net", scan_net_roots); + unsafe { + js_register_net_socket_handle_probe(ext_net_socket_handle_probe); + } + // #2154 — publish the raw-consumer vtable for perry-ext-http (runs on + // the first net FFI entry, before http could reference a socket). + raw_bridge::register(); + }); +} + +/// GC root scanner for net.Socket event listener closures. +/// +/// Without this, any GC cycle between `.on()` and the next dispatch would +/// sweep the closure; the next `closure.call*()` would dereference freed +/// memory. Same pattern as perry-stdlib's net mod and perry-ext-events. +fn scan_net_roots(visitor: &mut GcRootVisitor<'_>) { + if let Ok(mut listeners) = statics::listeners().lock() { + for per_socket in listeners.values_mut() { + for cb_vec in per_socket.values_mut() { + for cb in cb_vec.iter_mut() { + visitor.visit_i64_slot(cb); + } + } + } + } + // `once_flags()` keys membership by the closure's ADDRESS BITS. The + // canonical copy in `listeners()` above keeps the closure alive and is + // rewritten when the copying GC moves it — but a `HashSet` element + // cannot be rewritten in place, so without this rebuild the set still + // holds the OLD address after evacuation: the once-membership test in + // `lifecycle.rs` then misses, the listener is never auto-removed, and a + // "once" callback fires on every subsequent event. Drain, forward each + // element through the visitor, and reinsert under the new identity. + if let Ok(mut once) = statics::once_flags().lock() { + for per_handle in once.values_mut() { + for set in per_handle.values_mut() { + let old: Vec = set.drain().collect(); + for mut cb in old { + visitor.visit_i64_slot(&mut cb); + set.insert(cb); + } + } + } + } + // #8259 — the pump's in-flight dispatch frames (snapshotted callbacks + + // parked payloads), which the table walks above cannot see. + dispatch_custody::scan(visitor); +} + pub(crate) struct SocketState { - pub(crate) tcp_async_id: u64, - pub(crate) connect_async_id: u64, - pub(crate) shutdown_async_id: u64, pub(crate) cmd_tx: mpsc::UnboundedSender, /// `Some` only between `js_net_socket_alloc` and the first /// `js_net_socket_method_connect`. Held here so the deferred-connect @@ -289,9 +340,6 @@ impl SocketState { /// command-path test, which only needs `cmd_tx` to reach `run_socket_task`. pub(crate) fn for_test(cmd_tx: mpsc::UnboundedSender) -> Self { SocketState { - tcp_async_id: 0, - connect_async_id: 0, - shutdown_async_id: 0, cmd_tx, pending_rx: None, is_open: true, @@ -322,10 +370,6 @@ pub(crate) enum SocketCommand { /// the task starts — after the connect site has set the Node default ON, /// so an explicit opt-out wins. SetNoDelay(bool), - /// The main thread finished dispatching the accepted socket's - /// `connection` callback. Commands queued by that callback precede this - /// marker, so a peer FIN may now auto-close without dropping its response. - ServerConnectionReady, /// Test-only: report the live socket's `TCP_NODELAY` state back over a /// oneshot, so the command-path test can observe `setNoDelay` taking /// effect on the stream the task owns. @@ -356,10 +400,6 @@ enum PendingNetEvent { /// so without this the connection lifecycle never completes and the /// test hangs. 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 - /// fires the public `end` event. - ShutdownComplete(i64), Close(i64), Error(i64, String), AbortError(i64), @@ -395,15 +435,6 @@ enum PendingNetEvent { extern "C" { fn js_net_callback_ptr(value: f64) -> i64; fn js_get_string_pointer_unified(value: f64) -> i64; - fn js_async_hooks_provider_init(type_ptr: *const u8, type_len: usize) -> u64; - fn js_async_hooks_provider_init_with_trigger( - type_ptr: *const u8, - type_len: usize, - trigger_async_id: u64, - ) -> u64; - 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 perry_cluster_worker_listening( addr_ptr: *const u8, addr_len: u32, @@ -430,6 +461,34 @@ fn mark_closed(id: i64) { server_state::mark_socket_closed(id); } +// ─── Spawning helper ───────────────────────────────────────────────────────── +// +// perry-ffi v0.5.x's only async-runtime entry point is `spawn_blocking`, +// which boxes a `FnOnce()` to run on tokio's blocking pool. We bridge to +// async-Rust by calling `tokio::runtime::Handle::current().block_on(...)` +// inside the closure — same pattern axios / better-sqlite3 / iroh use. +// One thread per socket for its lifetime; the perry-stdlib version uses +// the same shared tokio runtime via `crate::common::async_bridge::spawn`, +// which is a regular `tokio::spawn` (cooperative). Neither approach is +// "wrong" — the cooperative version is denser, the blocking-pool version +// is simpler. Wrapper-side simplicity wins for a v0 port. +fn spawn_socket_runner(fut_factory: F) +where + F: FnOnce() -> Pin + Send>> + Send + 'static, +{ + // Run each socket future cooperatively on Perry's shared multi-thread + // runtime via `spawn_async`, instead of tying up one blocking-pool thread + // plus a throwaway current-thread runtime for the socket's whole lifetime. + // The shared runtime carries the I/O reactor, so `TcpStream` / TLS work + // without relying on the FFI callback's ambient `Handle` (the brittleness + // under release/LTO that the per-socket runtime worked around). The socket + // is registered in `sockets()` synchronously before this spawn, so + // `js_ext_net_has_active_handles` keeps the event loop alive for its life. + perry_ffi::spawn_async(async move { + fut_factory().await; + }); +} + // ─── FFI: net.createConnection / net.connect ───────────────────────────────── /// `net.createConnection(...)` / `net.connect(...)` — returns a handle @@ -544,13 +603,9 @@ pub unsafe extern "C" fn js_net_socket_alloc() -> i64 { dispatch::ensure_runtime_dispatch_registered(); let id = next_id_or_throw(); let (tx, rx) = mpsc::unbounded_channel::(); - let tcp_async_id = init_provider(b"TCPWRAP"); statics::sockets().lock().unwrap().insert( id, SocketState { - tcp_async_id, - connect_async_id: 0, - shutdown_async_id: 0, cmd_tx: tx, pending_rx: Some(rx), is_open: false, @@ -597,7 +652,6 @@ pub unsafe extern "C" fn js_net_create_server( statics::servers().lock().unwrap().insert( id, ServerState { - async_id: 0, shutdown_tx: None, bound_port: 0, bound_host: String::new(), @@ -623,17 +677,6 @@ pub unsafe extern "C" fn js_net_create_server( id } -/// Collision-proof server factory for generated code. Pulling this distinct -/// symbol from perry-ext-net also ensures the server/socket symbols in this -/// archive win over bundled-stdlib twins with separate handle registries. -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_create_server( - options_i64: i64, - connection_listener_i64: i64, -) -> i64 { - js_net_create_server(options_i64, connection_listener_i64) -} - // ─── FFI: net.Server.listen / .close / .address / .on ──────────────────────── /// `server.listen(port | path, callback?)` — bind TCP, a Windows named pipe, @@ -680,7 +723,6 @@ pub unsafe extern "C" fn js_net_server_listen(handle: i64, port: f64, arg2: f64, .unwrap_or_else(|| "0.0.0.0".to_string()); (port as u16, host) }; - let server_async_id = init_provider(b"TCPSERVERWRAP"); let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); @@ -695,7 +737,6 @@ pub unsafe extern "C" fn js_net_server_listen(handle: i64, port: f64, arg2: f64, Some(s) => s, None => return, }; - s.async_id = server_async_id; s.shutdown_tx = Some(shutdown_tx); s.bound_port = port_u16; s.bound_host = host.clone(); @@ -901,6 +942,28 @@ pub unsafe extern "C" fn js_net_server_address(handle: i64) -> *mut StringHeader alloc_string(&json).as_raw() } +/// `server.on(event, cb)` — register a server-level listener for +/// `'connection'`, `'listening'`, `'close'`, or `'error'`. Reuses +/// the shared listener map keyed on the server id (server ids and +/// socket ids are drawn from the same monotonic counter so they +/// never collide). +/// +/// # Safety +/// +/// `event_ptr` must be null or a Perry-runtime `StringHeader`. `cb` +/// is a raw `*const ClosureHeader` cast to `i64`. +#[no_mangle] +pub unsafe extern "C" fn js_net_server_on(handle: i64, event_ptr: i64, cb: i64) { + ensure_gc_scanner_registered(); + let event = match string_from_header_i64(event_ptr) { + Some(e) => e, + None => return, + }; + let mut listeners = statics::listeners().lock().unwrap(); + let entry = listeners.entry(handle).or_default(); + entry.entry(event).or_default().push(cb); +} + // ─── FFI: socket.connect(port, host) (instance method on existing handle) ───── /// `socket.connect(port, host)` / `socket.connect(path)` — initiates a TCP or @@ -971,19 +1034,10 @@ pub unsafe extern "C" fn js_net_socket_method_connect( let port = port as u16; ipc::register_connect_cb(handle, callback); - let (rx, tcp_async_id) = { + let rx = { let mut guard = statics::sockets().lock().unwrap(); - match guard.get_mut(&handle) { - Some(socket) => match socket.pending_rx.take() { - Some(rx) => (rx, socket.tcp_async_id), - None => { - push_event(PendingNetEvent::Error( - handle, - "socket already connected (or unknown handle)".to_string(), - )); - return; - } - }, + match guard.get_mut(&handle).and_then(|s| s.pending_rx.take()) { + Some(rx) => rx, None => { push_event(PendingNetEvent::Error( handle, @@ -993,10 +1047,6 @@ pub unsafe extern "C" fn js_net_socket_method_connect( } } }; - let connect_async_id = init_provider_with_trigger(b"TCPCONNECTWRAP", tcp_async_id); - if let Some(socket) = statics::sockets().lock().unwrap().get_mut(&handle) { - socket.connect_async_id = connect_async_id; - } let local_server = server_state::begin_local_connect(&host, port); spawn_socket_runner(move || { @@ -1066,15 +1116,10 @@ where .is_none() .then(|| server_state::begin_local_connect(&host, port)) .flatten(); - let tcp_async_id = unsafe { init_provider(b"TCPWRAP") }; - let connect_async_id = unsafe { init_provider_with_trigger(b"TCPCONNECTWRAP", tcp_async_id) }; statics::sockets().lock().unwrap().insert( id, SocketState { - tcp_async_id, - connect_async_id, - shutdown_async_id: 0, cmd_tx: tx, pending_rx: None, is_open: false, @@ -1160,13 +1205,6 @@ pub(crate) async fn run_socket_task( rx: &mut mpsc::UnboundedReceiver, ) { let mut transport: Option = Some(initial_transport); - let mut writable_ended = false; - let accepted_socket = statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(&id).map(|socket| socket.server_id.is_some())) - .unwrap_or(false); - let mut server_connection_ready = !accepted_socket; loop { let t = match transport.as_mut() { @@ -1208,72 +1246,18 @@ pub(crate) async fn run_socket_task( // breaking, so a peer FIN doesn't leak its pooled // capacity (the success path checks in below). buffer_pool::checkin(buf); - // #2154 raw mode owns its own terminal state. Accepted - // sockets may receive a request plus FIN before their - // delayed `connection` callback runs. Wait for the - // callback-complete marker so its queued writes/end - // commands are honored. Outgoing sockets start ready - // and therefore retain Node's default auto-close after - // readable EOF (#6764). - if raw_bridge::mark_terminal(id, None) { - // Complete the writable half before dropping a TLS - // transport so the peer observes close_notify rather - // than an unclean EOF (#8688). - let _ = t.shutdown().await; - mark_closed(id); - break; - } - push_event(PendingNetEvent::End(id)); - - while accepted_socket && !writable_ended { - let command = if server_connection_ready { - match tokio::time::timeout( - std::time::Duration::from_millis(25), - rx.recv(), - ) - .await - { - Ok(command) => command, - Err(_) => break, - } - } else { - rx.recv().await - }; - match command { - Some(SocketCommand::Write(bytes)) => { - if let Err(e) = t.write_all(&bytes).await { - push_event(PendingNetEvent::Error(id, format!("{}", e))); - break; - } - } - Some(SocketCommand::End) => { - let _ = t.shutdown().await; - writable_ended = true; - push_event(PendingNetEvent::ShutdownComplete(id)); - } - Some(SocketCommand::SetNoDelay(enable)) => { - let _ = t.set_nodelay(enable); - } - Some(SocketCommand::ServerConnectionReady) => { - server_connection_ready = true; - } - #[cfg(test)] - Some(SocketCommand::QueryNoDelay(reply)) => { - let _ = reply.send(t.nodelay().unwrap_or(false)); - } - Some(SocketCommand::UpgradeTls { reply, .. }) => { - let _ = reply.send(Err( - "cannot upgrade a half-closed socket".to_string(), - )); - } - Some(SocketCommand::Destroy) | None => break, - } - } - if !writable_ended { - let _ = t.shutdown().await; - push_event(PendingNetEvent::ShutdownComplete(id)); + // #2154 raw mode: signal EOF on the buffer, suppress + // JS events. Else (#1852) fire 'end' then 'close' per + // Node's default `allowHalfOpen: false` teardown order. + // Complete the writable half before dropping the + // transport. For TLS this sends close_notify; without + // it the peer observes an unclean EOF and emits only + // 'close', skipping its 'end' event. + let _ = t.shutdown().await; + if !raw_bridge::mark_terminal(id, None) { + push_event(PendingNetEvent::End(id)); + push_event(PendingNetEvent::Close(id)); } - push_event(PendingNetEvent::Close(id)); mark_closed(id); break; } @@ -1300,16 +1284,7 @@ pub(crate) async fn run_socket_task( buffer_pool::checkin(buf); let msg = format!("{}", e); if !raw_bridge::mark_terminal(id, Some(msg.clone())) { - // rustls reports a peer that closes TCP without a - // close_notify alert as UnexpectedEof. Node's TLS - // socket treats that terminal read as the readable - // side ending, so preserve the normal end→close - // event order instead of silently losing `end`. - if msg.contains("close_notify") || msg.contains("unexpected end of file") { - push_event(PendingNetEvent::End(id)); - } else { - push_event(PendingNetEvent::Error(id, msg)); - } + push_event(PendingNetEvent::Error(id, msg)); push_event(PendingNetEvent::Close(id)); } mark_closed(id); @@ -1337,17 +1312,12 @@ pub(crate) async fn run_socket_task( } Some(SocketCommand::End) => { let _ = t.shutdown().await; - writable_ended = true; - push_event(PendingNetEvent::ShutdownComplete(id)); } Some(SocketCommand::SetNoDelay(enable)) => { // Best-effort, matching Node: a failed setsockopt (e.g. // the peer already closed) does not error the socket. let _ = t.set_nodelay(enable); } - Some(SocketCommand::ServerConnectionReady) => { - server_connection_ready = true; - } #[cfg(test)] Some(SocketCommand::QueryNoDelay(reply)) => { let _ = reply.send(t.nodelay().unwrap_or(false)); @@ -1610,40 +1580,6 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { let count = events.len() as i32; for ev in events.drain(..) { - prepare_event_provider(&ev); - let provider_id = event_provider_id(&ev); - let destroy_ids: Vec = match &ev { - PendingNetEvent::Connect(id, _) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(id).map(|socket| vec![socket.connect_async_id])) - .unwrap_or_default(), - PendingNetEvent::ShutdownComplete(id) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(id).map(|socket| vec![socket.shutdown_async_id])) - .unwrap_or_default(), - PendingNetEvent::Close(id) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| { - sockets.get(id).map(|socket| { - vec![ - socket.connect_async_id, - socket.shutdown_async_id, - socket.tcp_async_id, - ] - }) - }) - .unwrap_or_default(), - PendingNetEvent::ServerClose(id) => statics::servers() - .lock() - .ok() - .and_then(|servers| servers.get(id).map(|server| vec![server.async_id])) - .unwrap_or_default(), - _ => Vec::new(), - }; - let provider_scope = ProviderScope::enter(provider_id); match ev { PendingNetEvent::Connect(id, local_server) => { server_state::finish_local_connect(local_server); @@ -1765,7 +1701,6 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { drop(frame); lifecycle::drain_once_listeners(id, "end"); } - PendingNetEvent::ShutdownComplete(_) => {} PendingNetEvent::Close(id) => { extern "C" { fn js_tls_client_record_closed(handle: i64); @@ -1804,7 +1739,6 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { // once-set may still be holding stale entries. lifecycle::drain_once_listeners(server_id, "connection"); server_state::release_pending_server_data(socket_id); - server_state::release_connection_callback(socket_id); continue; } // Sockets returned by the codegen's `net.connect` @@ -1833,7 +1767,6 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { drop(frame); lifecycle::drain_once_listeners(server_id, "connection"); server_state::release_pending_server_data(socket_id); - server_state::release_connection_callback(socket_id); } PendingNetEvent::ServerListening(server_id) => { // Take + drain the 'listening' listeners so the @@ -1936,12 +1869,6 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { lifecycle::drain_once_listeners(server_id, "drop"); } } - drop(provider_scope); - for async_id in destroy_ids { - if async_id != 0 { - js_async_hooks_provider_destroy(async_id); - } - } } // Restore the (capacity-retaining) buffer to the thread-local so the @@ -1957,14 +1884,106 @@ pub unsafe extern "C" fn js_ext_net_drain_pending() -> i32 { count } -mod handle_exports; -use handle_exports::listeners_for; -pub use handle_exports::{ - is_net_server_handle, is_net_socket_handle, js_ext_net_is_server_handle, js_ext_net_socket_on, - js_ext_net_socket_once, js_ext_net_socket_remove_all_listeners, - js_ext_net_socket_remove_listener, js_net_has_pending, js_net_server_listening, - js_net_server_on, -}; +fn listeners_for(id: i64, event: &str) -> Vec { + statics::listeners() + .lock() + .unwrap() + .get(&id) + .and_then(|m| m.get(event).cloned()) + .unwrap_or_default() +} + +// `drain_once_listeners` lives in `lifecycle::drain_once_listeners` so +// the file-size gate keeps a single owner for the EventEmitter surface. + +/// Returns 1 if queued events or live net handles keep the loop alive. +#[no_mangle] +pub extern "C" fn js_net_has_pending() -> i32 { + server_state::has_active_handles() as i32 +} + +/// True iff `handle` is a currently-registered net socket id. Mirrors the +/// perry-stdlib export so codegen's `HANDLE_METHOD_DISPATCH` keeps working. +pub fn is_net_socket_handle(handle: i64) -> bool { + statics::sockets().lock().unwrap().contains_key(&handle) +} + +/// True iff `handle` is a currently-registered net server id. +pub fn is_net_server_handle(handle: i64) -> bool { + statics::servers().lock().unwrap().contains_key(&handle) +} + +/// `server.listening` — boolean state exposed through handle property dispatch. +#[no_mangle] +pub extern "C" fn js_net_server_listening(handle: i64) -> i32 { + match statics::servers().lock() { + Ok(servers) => servers + .get(&handle) + .map(|server| if server.listening { 1 } else { 0 }) + .unwrap_or(0), + Err(_) => 0, + } +} + +/// `extern "C"` form of `is_net_socket_handle` — used by +/// perry-stdlib's `common::dispatch::dispatch_handle_method` +/// (HANDLE_METHOD_DISPATCH) when bundled-net is stripped and +/// the well-known flip routes 'net' to perry-ext-net. Returns +/// 1 for a registered socket handle, 0 otherwise. +/// +/// Closes the issue #91 regression: Map.get'd / struct-field / +/// wrapper-function receivers where codegen lost the static type +/// fall through to `js_native_call_method` → +/// `dispatch_handle_method` → this query → `dispatch_net_socket`. +/// Without the extern, the dispatch tower's `is_net_socket_handle` +/// reference resolved to perry-stdlib's no-op stub (compiled-out +/// when bundled-net is off) and Map-retrieved sockets silently +/// dispatched to undefined. +/// Distinct-symbol aliases for the socket EVENT-LISTENER surface (#5021's +/// twin-symbol disease). perry-stdlib exports same-named `js_net_socket_on` / +/// `_once` / `_remove_listener` twins, so in a build that links BOTH archives +/// the shared names bind to the bundled twin's EMPTY socket registry and the +/// listener registration is silently dropped: the socket connects, the reader +/// task delivers bytes, and the pump finds ZERO 'data' listeners — mysql2's +/// handshake then hangs to ETIMEDOUT. `write`/`end`/`destroy` were split out +/// for exactly this reason (#5010/#5021); the listener calls were not. +#[no_mangle] +pub unsafe extern "C" fn js_ext_net_socket_on(handle: i64, event_ptr: i64, cb: i64) { + js_net_socket_on(handle, event_ptr, cb) +} + +#[no_mangle] +pub unsafe extern "C" fn js_ext_net_socket_once(handle: i64, event_ptr: i64, cb: i64) -> i64 { + js_net_socket_once(handle, event_ptr, cb) +} + +#[no_mangle] +pub unsafe extern "C" fn js_ext_net_socket_remove_listener( + handle: i64, + event_ptr: i64, + cb: i64, +) -> i64 { + js_net_socket_remove_listener(handle, event_ptr, cb) +} + +#[no_mangle] +pub unsafe extern "C" fn js_ext_net_socket_remove_all_listeners( + handle: i64, + event_ptr: i64, +) -> i64 { + js_net_socket_remove_all_listeners(handle, event_ptr) +} + +/// `extern "C"` form of `is_net_server_handle` for method-value/property +/// dispatch on `net.Server` handles. +#[no_mangle] +pub extern "C" fn js_ext_net_is_server_handle(handle: i64) -> i32 { + if is_net_server_handle(handle) { + 1 + } else { + 0 + } +} #[cfg(test)] mod tests; diff --git a/crates/perry-ext-net/src/lifecycle.rs b/crates/perry-ext-net/src/lifecycle.rs index 9875eccaf0..da2754ec8e 100644 --- a/crates/perry-ext-net/src/lifecycle.rs +++ b/crates/perry-ext-net/src/lifecycle.rs @@ -20,10 +20,7 @@ //! `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 crate::statics; @@ -318,34 +315,6 @@ 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]) { - extern "C" { - fn js_value_is_closure(value_bits: i64) -> i32; - } - if let Some(callback) = 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(); - } - } -} - -/// Full Node overload for `socket.write(chunk[, encoding][, callback])`. -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_socket_write3( - handle: i64, - chunk: f64, - 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]); -} - /// `socket.end([data])` — optionally write a final chunk, then half-close the /// write side (#1852). `undefined`/`null` (the no-arg form, padded with /// `TAG_UNDEFINED`) yields `None` and we just send FIN. @@ -361,17 +330,6 @@ 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) { - 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(s) = sockets.get_mut(&handle) { if let Some(bytes) = crate::jsvalue_to_socket_bytes(f64::from_bits(chunk_bits as u64)) { @@ -395,18 +353,6 @@ pub unsafe extern "C" fn js_net_socket_end(handle: i64, chunk_bits: i64) { js_ext_net_socket_end(handle, chunk_bits); } -/// Full Node overload for `socket.end([data][, encoding][, callback])`. -#[no_mangle] -pub unsafe extern "C" fn js_ext_net_socket_end3( - handle: i64, - chunk_or_callback: f64, - 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]); -} - /// `socket.destroy()` — hard close. Flags the handle destroyed (so /// `socket.destroyed` / `readyState` reflect it) and sends the teardown /// command. @@ -497,7 +443,6 @@ fn register_listener_with_flag(handle: i64, event: String, cb: i64, once: bool) if cb == 0 { return; } - let releases_pending_data = event == "data"; { let mut listeners = statics::listeners().lock().unwrap(); listeners @@ -516,9 +461,6 @@ fn register_listener_with_flag(handle: i64, event: String, cb: i64, once: bool) .or_default() .insert(cb); } - if releases_pending_data { - crate::server_state::release_pending_server_data(handle); - } } /// Issue #2131 — drop any callback pointer flagged as a `once` listener diff --git a/crates/perry-ext-net/src/provider_lifecycle.rs b/crates/perry-ext-net/src/provider_lifecycle.rs deleted file mode 100644 index b6c848e8d3..0000000000 --- a/crates/perry-ext-net/src/provider_lifecycle.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::*; - -pub(super) unsafe fn init_provider(name: &'static [u8]) -> u64 { - js_async_hooks_provider_init(name.as_ptr(), name.len()) -} - -pub(super) unsafe fn init_provider_with_trigger(name: &'static [u8], trigger_async_id: u64) -> u64 { - js_async_hooks_provider_init_with_trigger(name.as_ptr(), name.len(), trigger_async_id) -} - -pub(super) struct ProviderScope(u64); - -impl ProviderScope { - pub(super) unsafe fn enter(async_id: u64) -> Self { - if async_id != 0 { - js_async_hooks_provider_enter(async_id); - } - Self(async_id) - } -} - -impl Drop for ProviderScope { - fn drop(&mut self) { - if self.0 != 0 { - unsafe { js_async_hooks_provider_leave(self.0) }; - } - } -} - -pub(super) unsafe fn prepare_event_provider(ev: &PendingNetEvent) { - match ev { - PendingNetEvent::ServerConnection(server_id, socket_id, _) => { - let server_async_id = statics::servers() - .lock() - .ok() - .and_then(|servers| servers.get(server_id).map(|server| server.async_id)) - .unwrap_or(0); - let needs_init = statics::sockets() - .lock() - .ok() - .and_then(|sockets| { - sockets - .get(socket_id) - .map(|socket| socket.tcp_async_id == 0) - }) - .unwrap_or(false); - if needs_init { - let async_id = init_provider_with_trigger(b"TCPWRAP", server_async_id); - if let Some(socket) = statics::sockets().lock().unwrap().get_mut(socket_id) { - socket.tcp_async_id = async_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) - }) - }); - if let Some(trigger) = trigger { - let async_id = init_provider_with_trigger(b"SHUTDOWNWRAP", trigger); - if let Some(socket) = statics::sockets().lock().unwrap().get_mut(id) { - socket.shutdown_async_id = async_id; - } - } - } - _ => {} - } -} - -pub(super) fn event_provider_id(ev: &PendingNetEvent) -> u64 { - match ev { - PendingNetEvent::Connect(id, _) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(id).map(|socket| socket.connect_async_id)) - .unwrap_or(0), - PendingNetEvent::SecureConnect(id) - | PendingNetEvent::Data(id, _) - | PendingNetEvent::End(id) - | PendingNetEvent::Error(id, _) - | PendingNetEvent::AbortError(id) - | PendingNetEvent::Close(id) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(id).map(|socket| socket.tcp_async_id)) - .unwrap_or(0), - PendingNetEvent::ShutdownComplete(id) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(id).map(|socket| socket.shutdown_async_id)) - .unwrap_or(0), - PendingNetEvent::ServerConnection(_, socket_id, _) => statics::sockets() - .lock() - .ok() - .and_then(|sockets| sockets.get(socket_id).map(|socket| socket.tcp_async_id)) - .unwrap_or(0), - PendingNetEvent::ServerListening(id) - | PendingNetEvent::ServerClose(id) - | PendingNetEvent::ServerError(id, _) - | PendingNetEvent::ServerDrop(id, _) => statics::servers() - .lock() - .ok() - .and_then(|servers| servers.get(id).map(|server| server.async_id)) - .unwrap_or(0), - } -} diff --git a/crates/perry-ext-net/src/server_state.rs b/crates/perry-ext-net/src/server_state.rs index e16cebf739..9feadc9dc3 100644 --- a/crates/perry-ext-net/src/server_state.rs +++ b/crates/perry-ext-net/src/server_state.rs @@ -111,10 +111,14 @@ pub(crate) fn defer_server_connection(server_id: i64, socket_id: i64) -> bool { pub(crate) fn buffer_pending_server_data(socket_id: i64, bytes: Bytes) { let mut state = connection_order_state().lock().unwrap(); - // Reads can win the race with JavaScript listener registration on both - // accepted and outbound sockets. Keep every otherwise-undeliverable - // chunk here; registration releases it immediately, while close() drops - // abandoned data through `discard_pending_server_data`. + let pending_connection = statics::sockets() + .lock() + .unwrap() + .get(&socket_id) + .is_some_and(|socket| socket.server_id.is_some() && !socket.server_connection_active); + if !pending_connection { + return; + } state .pending_socket_data .entry(socket_id) @@ -333,17 +337,6 @@ pub(crate) fn activate_connection(server_id: i64, socket_id: i64) { } } -/// Tell the socket task that JavaScript's accepted-socket callback has -/// returned. Any writes/end queued by that callback are already ahead of this -/// marker in the channel, so peer-EOF handling can safely auto-close after it. -pub(crate) fn release_connection_callback(socket_id: i64) { - if let Some(socket) = statics::sockets().lock().unwrap().get(&socket_id) { - let _ = socket - .cmd_tx - .send(crate::SocketCommand::ServerConnectionReady); - } -} - pub(crate) fn mark_socket_closed(socket_id: i64) { let mut sockets = statics::sockets().lock().unwrap(); let Some(socket) = sockets.get_mut(&socket_id) else { diff --git a/crates/perry-ext-net/src/task_spawn.rs b/crates/perry-ext-net/src/task_spawn.rs deleted file mode 100644 index f2a67b1c61..0000000000 --- a/crates/perry-ext-net/src/task_spawn.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Cooperative socket-task spawning on Perry's shared Tokio runtime. - -use std::pin::Pin; - -// perry-ffi v0.5.x's only async-runtime entry point was originally -// `spawn_blocking`, which boxed a `FnOnce()` on Tokio's blocking pool. Socket -// futures now run cooperatively through `spawn_async`, matching perry-stdlib's -// shared-runtime path without tying up one blocking-pool thread per socket. -pub(super) fn spawn_socket_runner(fut_factory: F) -where - F: FnOnce() -> Pin + Send>> + Send + 'static, -{ - // The socket is registered synchronously before this spawn, so - // `js_ext_net_has_active_handles` keeps the event loop alive for its life. - perry_ffi::spawn_async(async move { - fut_factory().await; - }); -} diff --git a/crates/perry-ext-net/src/tests.rs b/crates/perry-ext-net/src/tests.rs index 6363aa55cf..c276524a26 100644 --- a/crates/perry-ext-net/src/tests.rs +++ b/crates/perry-ext-net/src/tests.rs @@ -86,10 +86,7 @@ fn assert_rewritten(before: i64, after: i64) { #[test] fn gc_mutable_scanner_rewrites_listener_roots() { let _guard = GcTestGuard::new(); - perry_ffi::gc_register_mutable_root_scanner_named( - "perry-ext-net", - crate::gc_roots::scan_net_roots, - ); + perry_ffi::gc_register_mutable_root_scanner_named("perry-ext-net", scan_net_roots); // Keep an ordinary shadow-stack root as the control. Its rewrite proves // the collection copied live objects independently of scan_net_roots, so diff --git a/crates/perry-ext-net/src/tls.rs b/crates/perry-ext-net/src/tls.rs index bbc39514ef..4270af5c0b 100644 --- a/crates/perry-ext-net/src/tls.rs +++ b/crates/perry-ext-net/src/tls.rs @@ -926,12 +926,3 @@ pub unsafe extern "C" fn js_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f } handle } - -/// Collision-proof entry point for AOT calls. Both bundled stdlib net and -/// perry-ext-net export `js_tls_connect`; auto-optimized binaries may bind the -/// shared name to the bundled registry while their event pump owns ext-net -/// sockets. Keep generated TLS clients in the same backend as the pump. -#[no_mangle] -pub unsafe extern "C" fn js_ext_tls_connect(arg1: f64, arg2: f64, arg3: f64, arg4: f64) -> i64 { - js_tls_connect(arg1, arg2, arg3, arg4) -} diff --git a/crates/perry-ext-zlib/src/stream.rs b/crates/perry-ext-zlib/src/stream.rs index f67aaaa771..717208fc9f 100644 --- a/crates/perry-ext-zlib/src/stream.rs +++ b/crates/perry-ext-zlib/src/stream.rs @@ -66,9 +66,6 @@ extern "C" { // Async one-shot zlib helpers require a callable callback and throw // 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_enter(async_id: u64); - fn js_async_hooks_provider_leave(async_id: u64); fn js_native_call_method_str_key( object: f64, name_handle: i64, @@ -470,7 +467,6 @@ fn make_codec_state_with_level(codec: Codec, level: Compression) -> Option), - Finish(i64), End(i64), Error(i64, String), /// `.flush(cb)` completion callback — invoked (0 args) after its flushed /// 'data' is delivered. Callback(i64), /// `zlib.gzip(data, cb)` style one-shot completion callback. - OneShotCallback(i64, Result, String>, u64), + OneShotCallback(i64, Result, String>), } struct Statics { @@ -565,7 +560,7 @@ fn scan_zlib_roots(visitor: &mut GcRootVisitor<'_>) { // hazard as listeners. for ev in s.pending.iter_mut() { match ev { - ZlibEvent::Callback(cb) | ZlibEvent::OneShotCallback(cb, _, _) => { + ZlibEvent::Callback(cb) | ZlibEvent::OneShotCallback(cb, _) => { visitor.visit_i64_slot(cb); } _ => {} @@ -582,14 +577,12 @@ fn create_stream(codec: Codec, level: Compression) -> i64 { // unnecessary and unsafe in stripped well-known-wrapper builds. ensure_aux_pump_registered(); ensure_gc_scanner_registered(); - let async_id = unsafe { js_async_hooks_provider_init(b"ZLIB".as_ptr(), b"ZLIB".len()) }; let mut s = statics().lock().unwrap(); let id = s.next_id; s.next_id += 1; s.streams.insert( id, ZlibStreamState { - async_id, codec, level, codec_state: make_codec_state_with_level(codec, level), @@ -715,12 +708,11 @@ 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()); statics() .lock() .unwrap() .pending - .push_back(ZlibEvent::OneShotCallback(callback, result, async_id)); + .push_back(ZlibEvent::OneShotCallback(callback, result)); notify_main_thread(); } @@ -880,9 +872,6 @@ fn finish_stream(handle: i64) { let mut g = statics().lock().unwrap(); match result { Ok(out) => { - // Writable completion precedes the final readable bytes/end - // of a Transform stream in Node. - g.pending.push_back(ZlibEvent::Finish(handle)); if !out.is_empty() { g.pending.push_back(ZlibEvent::Data(handle, out)); } @@ -974,22 +963,11 @@ fn flush_buffered(handle: i64) { /// `OneShotCallback` carry only a closure, so they are not tied to a stream. fn event_stream_handle(ev: &ZlibEvent) -> Option { match ev { - ZlibEvent::Data(id, _) - | ZlibEvent::Finish(id) - | ZlibEvent::End(id) - | ZlibEvent::Error(id, _) => Some(*id), - ZlibEvent::Callback(_) | ZlibEvent::OneShotCallback(_, _, _) => None, + ZlibEvent::Data(id, _) | ZlibEvent::End(id) | ZlibEvent::Error(id, _) => Some(*id), + ZlibEvent::Callback(_) | ZlibEvent::OneShotCallback(_, _) => None, } } -fn stream_async_id(handle: i64) -> u64 { - statics() - .lock() - .ok() - .and_then(|g| g.streams.get(&handle).map(|stream| stream.async_id)) - .unwrap_or(0) -} - /// Splice late-flushed buffered `Data` (then the deferred `End`) ahead of any /// newer queued events for `handle`, so the FIFO `process_pending` drain still /// delivers a late consumer its chunks in write order. Inserts at the first @@ -1336,10 +1314,6 @@ 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); - if event_async_id != 0 { - js_async_hooks_provider_enter(event_async_id); - } match ev { ZlibEvent::Data(id, bytes) => { publish_bytes_written(id); @@ -1367,13 +1341,6 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { } } } - ZlibEvent::Finish(id) => { - for cb in listeners_for(id, "finish") { - 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 @@ -1399,9 +1366,6 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { // 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; } // Stream already gone — release the lock and fall through to @@ -1413,6 +1377,11 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); } } + for cb in listeners_for(id, "finish") { + if cb != 0 { + let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); + } + } for dest in pipes_for(id) { forward_end(dest); } @@ -1428,12 +1397,8 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { let _ = JsClosure::from_raw(cb as *const RawClosureHeader).call0(); } } - ZlibEvent::OneShotCallback(cb, result, async_id) => { - js_async_hooks_provider_enter(async_id); - js_async_hooks_provider_leave(async_id); - js_async_hooks_provider_enter(async_id); + ZlibEvent::OneShotCallback(cb, result) => { call_one_shot_callback(cb, result); - js_async_hooks_provider_leave(async_id); } ZlibEvent::Error(id, msg) => { let err_f64 = build_error_object(&msg); @@ -1445,9 +1410,6 @@ pub unsafe extern "C" fn js_ext_zlib_process_pending() -> i32 { drop_buffered_stream(&mut statics().lock().unwrap(), id); } } - if event_async_id != 0 { - js_async_hooks_provider_leave(event_async_id); - } } count } @@ -1663,7 +1625,6 @@ mod stream_tests { fn no_consumer_state() -> ZlibStreamState { ZlibStreamState { - async_id: 0, codec: Codec::Gzip, level: Compression::default(), codec_state: None, @@ -1691,7 +1652,6 @@ mod stream_tests { pipes: Vec::new(), output_buffer: b"buffered".to_vec(), end_buffered: true, - async_id: 0, } } diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index 34423b2301..be47e4de4e 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -1080,23 +1080,6 @@ pub(crate) fn lower_stmt( cn.to_string(), )); } - // Named-import factories lower directly to a - // receiver-less NativeMethodCall. The AST - // member-call registration in native_new.rs - // therefore never sees - // `import { createInterface } ...; const rl = - // createInterface(...)`, especially inside a - // Promise executor. Preserve the Interface - // identity here so adjacent `rl.on(...)` calls - // reach the readline FFI instead of generic - // small-handle dispatch (#6764). - if mod_name == "readline" && method == "createInterface" { - ctx.register_native_instance( - name.clone(), - "readline".to_string(), - "Interface".to_string(), - ); - } // Issue #769 — node:http / node:https CLIENT factories. // `const req = http.request(url, cb)` and `http.get` / `https.*` // variants return a ClientRequest handle; register under diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index 74ee48cbd1..db318cd4c7 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -214,7 +214,6 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Some(("net", "Server")), ("net", "BlockList") => Some(("net", "BlockList")), ("net", "SocketAddress") => Some(("net", "SocketAddress")), - ("readline", "createInterface") => Some(("readline", "Interface")), _ => None, }; if let Some((m, c)) = native_class { diff --git a/crates/perry-runtime/src/array/subclass.rs b/crates/perry-runtime/src/array/subclass.rs index b9151987f5..2650312f5f 100644 --- a/crates/perry-runtime/src/array/subclass.rs +++ b/crates/perry-runtime/src/array/subclass.rs @@ -254,6 +254,9 @@ fn dense_layout_for_value(value: f64) -> Option<(*const ObjectHeader, DenseSubcl return None; } let obj = js.as_pointer::(); + if obj.is_null() || !crate::object::is_valid_obj_ptr(obj.cast::()) { + return None; + } let header = unsafe { crate::value::addr_class::try_read_gc_header(obj as usize)? }; if header.obj_type != crate::gc::GC_TYPE_OBJECT || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 diff --git a/crates/perry-runtime/src/async_context.rs b/crates/perry-runtime/src/async_context.rs index 53567fcdd0..770d1bafe3 100644 --- a/crates/perry-runtime/src/async_context.rs +++ b/crates/perry-runtime/src/async_context.rs @@ -6,7 +6,6 @@ //! while the callback runs. use std::cell::RefCell; -use std::collections::HashMap; use crate::gc::{RuntimeHandle, RuntimeHandleScope}; @@ -24,23 +23,11 @@ impl AsyncContextSnapshot { #[derive(Clone)] struct AsyncContextEntry { handle: i64, - generation: u64, stores: Vec, } thread_local! { static ACTIVE_CONTEXT: RefCell = RefCell::new(AsyncContextSnapshot::default()); - static HANDLE_GENERATIONS: RefCell> = RefCell::new(HashMap::new()); -} - -fn handle_generation(handle: i64) -> u64 { - HANDLE_GENERATIONS.with(|generations| generations.borrow().get(&handle).copied().unwrap_or(0)) -} - -fn discard_disabled_entries(snapshot: &mut AsyncContextSnapshot) { - snapshot - .entries - .retain(|entry| entry.generation == handle_generation(entry.handle)); } pub fn capture_context() -> AsyncContextSnapshot { @@ -51,15 +38,12 @@ pub fn enter_context(snapshot: &AsyncContextSnapshot) -> AsyncContextSnapshot { ACTIVE_CONTEXT.with(|ctx| { let mut ctx = ctx.borrow_mut(); let previous = ctx.clone(); - let mut next = snapshot.clone(); - discard_disabled_entries(&mut next); - *ctx = next; + *ctx = snapshot.clone(); previous }) } -pub fn restore_context(mut snapshot: AsyncContextSnapshot) { - discard_disabled_entries(&mut snapshot); +pub fn restore_context(snapshot: AsyncContextSnapshot) { ACTIVE_CONTEXT.with(|ctx| { *ctx.borrow_mut() = snapshot; }); @@ -120,21 +104,13 @@ pub extern "C" fn js_async_context_als_clear(handle: i64) { } pub fn push_store(handle: i64, store: f64) { - let generation = handle_generation(handle); ACTIVE_CONTEXT.with(|ctx| { let mut ctx = ctx.borrow_mut(); - ctx.entries - .retain(|entry| entry.handle != handle || entry.generation == generation); - if let Some(entry) = ctx - .entries - .iter_mut() - .find(|entry| entry.handle == handle && entry.generation == generation) - { + if let Some(entry) = ctx.entries.iter_mut().find(|entry| entry.handle == handle) { entry.stores.push(store); } else { ctx.entries.push(AsyncContextEntry { handle, - generation, stores: vec![store], }); } @@ -142,14 +118,9 @@ pub fn push_store(handle: i64, store: f64) { } pub fn pop_store(handle: i64) { - let generation = handle_generation(handle); ACTIVE_CONTEXT.with(|ctx| { let mut ctx = ctx.borrow_mut(); - if let Some(index) = ctx - .entries - .iter() - .position(|entry| entry.handle == handle && entry.generation == generation) - { + if let Some(index) = ctx.entries.iter().position(|entry| entry.handle == handle) { ctx.entries[index].stores.pop(); if ctx.entries[index].stores.is_empty() { ctx.entries.remove(index); @@ -159,12 +130,11 @@ pub fn pop_store(handle: i64) { } pub fn get_store(handle: i64) -> Option { - let generation = handle_generation(handle); ACTIVE_CONTEXT.with(|ctx| { ctx.borrow() .entries .iter() - .find(|entry| entry.handle == handle && entry.generation == generation) + .find(|entry| entry.handle == handle) .and_then(|entry| entry.stores.last().copied()) }) } @@ -176,16 +146,9 @@ pub fn get_store(handle: i64) -> Option { /// (which saves/restores exactly one slot for its own handle) still restores /// the pre-`run` value on exit (#788, differential case 21). pub fn set_store(handle: i64, store: f64) { - let generation = handle_generation(handle); ACTIVE_CONTEXT.with(|ctx| { let mut ctx = ctx.borrow_mut(); - ctx.entries - .retain(|entry| entry.handle != handle || entry.generation == generation); - if let Some(entry) = ctx - .entries - .iter_mut() - .find(|entry| entry.handle == handle && entry.generation == generation) - { + if let Some(entry) = ctx.entries.iter_mut().find(|entry| entry.handle == handle) { if let Some(slot) = entry.stores.last_mut() { *slot = store; } else { @@ -194,7 +157,6 @@ pub fn set_store(handle: i64, store: f64) { } else { ctx.entries.push(AsyncContextEntry { handle, - generation, stores: vec![store], }); } @@ -215,7 +177,7 @@ pub enum ContextGuardAction { /// `run()`: pop the one store slot the scope pushed for its handle. PopStore(i64), /// `exit()`: restore the handle's store stack removed at entry. - RestoreStores(i64, Option<(u64, Vec)>), + RestoreStores(i64, Option>), /// `runInAsyncScope()` / snapshot trampoline: restore the full snapshot. RestoreSnapshot(AsyncContextSnapshot), /// Silently pop one async_hooks execution-id frame (no `after` hook @@ -284,7 +246,7 @@ fn scan_context_guard_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) match &mut guard.action { ContextGuardAction::PopStore(_) | ContextGuardAction::RestoreExecutionIds => {} ContextGuardAction::RestoreStores(_, stores) => { - if let Some((_, stores)) = stores { + if let Some(stores) = stores { for store in stores.iter_mut() { visitor.visit_nanbox_f64_slot(store); } @@ -299,28 +261,6 @@ fn scan_context_guard_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) } pub fn clear_store(handle: i64) { - // `disable()` invalidates descendants captured from a currently-active - // store, but Node leaves already-captured work alone when the storage is - // disabled after its `run()` scope has returned. Generation-bump only in - // the former case so another ALS in the same pending snapshot is not - // disturbed either. - let was_active = ACTIVE_CONTEXT.with(|ctx| { - ctx.borrow() - .entries - .iter() - .any(|entry| entry.handle == handle) - }); - if was_active { - HANDLE_GENERATIONS.with(|generations| { - let mut generations = generations.borrow_mut(); - let generation = generations.entry(handle).or_insert(0); - *generation = generation.wrapping_add(1); - }); - } - remove_store(handle); -} - -fn remove_store(handle: i64) { ACTIVE_CONTEXT.with(|ctx| { ctx.borrow_mut() .entries @@ -328,17 +268,13 @@ fn remove_store(handle: i64) { }); } -pub fn take_store(handle: i64) -> Option<(u64, Vec)> { - let generation = handle_generation(handle); +pub fn take_store(handle: i64) -> Option> { ACTIVE_CONTEXT.with(|ctx| { let mut ctx = ctx.borrow_mut(); ctx.entries .iter() - .position(|entry| entry.handle == handle && entry.generation == generation) - .map(|index| { - let entry = ctx.entries.remove(index); - (entry.generation, entry.stores) - }) + .position(|entry| entry.handle == handle) + .map(|index| ctx.entries.remove(index).stores) }) } @@ -347,16 +283,14 @@ pub fn take_store(handle: i64) -> Option<(u64, Vec)> { /// `take_store` returns `Some` only for an existing entry, and live entries are /// kept non-empty by `pop_store`. The empty guard below is defensive for manual /// callers and prevents inert context entries from accumulating. -pub fn restore_store(handle: i64, stores: Option<(u64, Vec)>) { - remove_store(handle); - if let Some((generation, stores)) = stores { - if !stores.is_empty() && generation == handle_generation(handle) { +pub fn restore_store(handle: i64, stores: Option>) { + clear_store(handle); + if let Some(stores) = stores { + if !stores.is_empty() { ACTIVE_CONTEXT.with(|ctx| { - ctx.borrow_mut().entries.push(AsyncContextEntry { - handle, - generation, - stores, - }); + ctx.borrow_mut() + .entries + .push(AsyncContextEntry { handle, stores }); }); } } @@ -455,7 +389,6 @@ pub(crate) fn test_snapshot_with_store(store: f64) -> AsyncContextSnapshot { AsyncContextSnapshot { entries: vec![AsyncContextEntry { handle: -1, - generation: handle_generation(-1), stores: vec![store], }], } diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 9b4ddc0e52..67077a09c7 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -49,7 +49,6 @@ per_test_global! { static NEXT_ASYNC_ID: AtomicU64 = AtomicU64::new(2); pub static HOOKS_ACTIVE: AtomicUsize = AtomicUsize::new(0); static PROMISE_HOOKS_ACTIVE: AtomicUsize = AtomicUsize::new(0); - static TOP_LEVEL_RESOURCE: AtomicU64 = AtomicU64::new(0); } #[derive(Clone, Copy)] @@ -156,7 +155,6 @@ per_test_global! { static ASYNC_RESOURCE_HANDLES: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); static ASYNC_RESOURCE_HANDLE_COUNT: AtomicUsize = AtomicUsize::new(0); -const ASYNC_RESOURCE_SUBCLASS_KEY: &[u8] = b"__perryAsyncResourceBacking"; /// Live `AsyncHook` handles, for the same dynamic-receiver reason as /// `ASYNC_RESOURCE_HANDLES`. A helper that returns @@ -170,14 +168,7 @@ thread_local! { static EXECUTION_STACK: RefCell> = const { RefCell::new(Vec::new()) }; static CURRENT_EXECUTION_ID: Cell = const { Cell::new(0) }; static CURRENT_TRIGGER_ID: Cell = const { Cell::new(0) }; - // Node defers hook-list mutations made by a hook callback until the - // outermost hook-delivery cascade has finished. In particular, an init - // callback can synchronously create another resource; that nested init - // must still see the hook set that was active at the start of the outer - // init. Keep the last requested state for each hook while any lifecycle - // callback is on the stack, then commit the batch at depth zero. - static HOOK_CALLBACK_DEPTH: Cell = const { Cell::new(0) }; - static PENDING_HOOK_STATES: RefCell> = RefCell::new(HashMap::new()); + static IN_HOOK_CALLBACK: Cell = const { Cell::new(false) }; } pub struct AsyncHookHandle { @@ -186,33 +177,6 @@ pub struct AsyncHookHandle { pub struct AsyncResourceHandle { ids: AsyncResourceIds, - event_emitter: i64, -} - -pub(crate) fn is_async_resource_handle(handle: i64) -> bool { - handle != 0 && ASYNC_RESOURCE_HANDLES.lock().unwrap().contains(&handle) -} - -/// Resolve either a native `AsyncResource` handle or the ordinary object used -/// for a source-compiled subclass to its native backing allocation. -pub(crate) fn resolve_async_resource_handle(receiver: i64) -> Option { - if is_async_resource_handle(receiver) { - return Some(receiver); - } - let raw = receiver as usize; - if !crate::value::addr_class::is_plausible_heap_addr(raw) { - return None; - } - 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); - if !value.is_pointer() { - return None; - } - let backing = value.as_pointer::() as i64; - is_async_resource_handle(backing).then_some(backing) } #[inline(always)] @@ -262,20 +226,8 @@ pub extern "C" fn js_async_hooks_execution_async_resource() -> f64 { } } - let cached = TOP_LEVEL_RESOURCE.load(Ordering::Acquire); - if cached != 0 { - return f64::from_bits(cached); - } - - // Node exposes one stable bootstrap resource for the top-level execution - // scope. Returning a fresh object here made restoration checks fail after - // every nested AsyncResource scope and also broke metadata inheritance in - // init hooks. let obj = crate::object::js_object_alloc(0, 0); - let value = crate::value::js_nanbox_pointer(obj as i64); - TOP_LEVEL_RESOURCE.store(value.to_bits(), Ordering::Release); - crate::gc::runtime_write_barrier_root_nanbox(value.to_bits()); - value + crate::value::js_nanbox_pointer(obj as i64) } const ASYNC_WRAP_PROVIDER_CONSTANTS: &[(&str, f64)] = &[ @@ -356,8 +308,7 @@ pub fn js_async_hooks_async_wrap_providers() -> f64 { return f64::from_bits(cached); } - let obj = - crate::object::js_object_alloc_null_proto(0, ASYNC_WRAP_PROVIDER_CONSTANTS.len() as u32); + let obj = crate::object::js_object_alloc(0, ASYNC_WRAP_PROVIDER_CONSTANTS.len() as u32); for (name, value) in ASYNC_WRAP_PROVIDER_CONSTANTS { let key = js_string_from_bytes(name.as_ptr(), name.len() as u32); crate::object::js_object_set_field_by_name(obj, key, *value); @@ -532,38 +483,17 @@ pub extern "C" fn js_async_hook_enable(handle: i64) -> i64 { return handle; } let hook = unsafe { &*(handle as *const AsyncHookHandle) }; - if HOOK_CALLBACK_DEPTH.with(Cell::get) != 0 { - PENDING_HOOK_STATES.with(|pending| { - pending.borrow_mut().insert(hook.index, true); - }); - return handle; - } - set_hook_enabled(hook.index, true); - handle -} - -fn set_hook_enabled(index: usize, enabled: bool) { let mut hooks = HOOKS.lock().unwrap(); - if let Some(record) = hooks.get_mut(index) { - if record.enabled == enabled { - return; - } - let delta_is_visible = record.callbacks.has_any(); - if delta_is_visible { - if enabled { - HOOKS_ACTIVE.fetch_add(1, Ordering::Relaxed); - if record.track_promises { - PROMISE_HOOKS_ACTIVE.fetch_add(1, Ordering::Relaxed); - } - } else { - HOOKS_ACTIVE.fetch_sub(1, Ordering::Relaxed); - if record.track_promises { - PROMISE_HOOKS_ACTIVE.fetch_sub(1, Ordering::Relaxed); - } + if let Some(record) = hooks.get_mut(hook.index) { + if !record.enabled && record.callbacks.has_any() { + HOOKS_ACTIVE.fetch_add(1, Ordering::Relaxed); + if record.track_promises { + PROMISE_HOOKS_ACTIVE.fetch_add(1, Ordering::Relaxed); } } - record.enabled = enabled; + record.enabled = true; } + handle } #[no_mangle] @@ -572,13 +502,16 @@ pub extern "C" fn js_async_hook_disable(handle: i64) -> i64 { return handle; } let hook = unsafe { &*(handle as *const AsyncHookHandle) }; - if HOOK_CALLBACK_DEPTH.with(Cell::get) != 0 { - PENDING_HOOK_STATES.with(|pending| { - pending.borrow_mut().insert(hook.index, false); - }); - return handle; + let mut hooks = HOOKS.lock().unwrap(); + if let Some(record) = hooks.get_mut(hook.index) { + if record.enabled && record.callbacks.has_any() { + HOOKS_ACTIVE.fetch_sub(1, Ordering::Relaxed); + if record.track_promises { + PROMISE_HOOKS_ACTIVE.fetch_sub(1, Ordering::Relaxed); + } + } + record.enabled = false; } - set_hook_enabled(hook.index, false); handle } @@ -603,69 +536,35 @@ fn with_hook_callbacks( if !hooks_active() { return; } - let callbacks = enabled_callbacks(is_promise); - HOOK_CALLBACK_DEPTH.with(|depth| depth.set(depth.get() + 1)); - - // Hook membership is snapshotted once per lifecycle phase: disabling a - // hook from another hook callback must not remove it from the phase already - // in progress, and enabling one must not add it. Re-entrant lifecycle - // delivery is nevertheless required: an async operation started by an - // init/destroy callback is a new phase with a fresh membership snapshot. - // A process-wide "inside a hook" guard used to suppress those nested - // phases entirely. - // - // The callback pointers in this snapshot still have to remain moving-GC - // roots. A callback can allocate arbitrary JS objects; without the handles - // the first hook could evacuate the remaining hooks while their copied raw - // pointers stayed stale in this Rust Vec. - let scope = crate::gc::RuntimeHandleScope::new(); - let rooted: Vec<_> = callbacks - .iter() - .map(|callbacks| scope.root_raw_const_ptr(callbacks.for_phase(phase))) - .collect(); - for callback in rooted { - callback.with_const_ptr::(|callback| { - if !callback.is_null() { - f(callback); - } - }); - } - let outermost = HOOK_CALLBACK_DEPTH.with(|depth| { - let next = depth.get().saturating_sub(1); - depth.set(next); - next == 0 - }); - if outermost { - let pending = PENDING_HOOK_STATES.with(|states| std::mem::take(&mut *states.borrow_mut())); - for (index, enabled) in pending { - set_hook_enabled(index, enabled); + IN_HOOK_CALLBACK.with(|guard| { + if guard.get() { + return; } - } -} - -/// Model the Promise that Node uses to evaluate an ESM entry module. Perry's -/// compiled entry does not allocate that wrapper Promise, but its init event -/// is observable by hooks enabled during module evaluation. -pub(crate) fn init_esm_evaluation_promise() { - if !promise_hooks_active() { - return; - } - let resource = crate::object::js_object_alloc_null_proto(0, 0); - let value = crate::value::js_nanbox_pointer(resource as i64); - let _ = init_resource("PROMISE", value, false); -} - -/// Reserve an async id for a resource whose lifecycle is not observable yet. -/// -/// Promises created before the first hook is enabled still need a stable id so -/// a later child reaction can name that promise as its trigger. They must not -/// be inserted into `RESOURCES`, because doing so would turn the resource value -/// into a strong GC root before any observer exists. -pub fn reserve_resource_ids(trigger_async_id: u64) -> AsyncResourceIds { - AsyncResourceIds { - async_id: NEXT_ASYNC_ID.fetch_add(1, Ordering::Relaxed), - trigger_async_id, - } + guard.set(true); + let callbacks = enabled_callbacks(is_promise); + + // Hook membership is snapshotted once per lifecycle phase: disabling + // a hook from another hook callback must not remove it from the phase + // already in progress, and enabling one must not add it. The callback + // pointers in that snapshot still have to remain moving-GC roots, + // though. A callback can allocate arbitrary JS objects; previously the + // first hook could therefore evacuate the remaining hooks while their + // copied raw pointers stayed in this Rust Vec. Dispatching the next + // stale pointer caused the multi-hook #6764 fixtures to segfault. + let scope = crate::gc::RuntimeHandleScope::new(); + let rooted: Vec<_> = callbacks + .iter() + .map(|callbacks| scope.root_raw_const_ptr(callbacks.for_phase(phase))) + .collect(); + for callback in rooted { + callback.with_const_ptr::(|callback| { + if !callback.is_null() { + f(callback); + } + }); + } + guard.set(false); + }); } pub fn init_resource(type_name: &str, resource: f64, force_allocate: bool) -> AsyncResourceIds { @@ -691,29 +590,13 @@ pub fn init_resource_with_trigger( } let async_id = NEXT_ASYNC_ID.fetch_add(1, Ordering::Relaxed); - // Native resources such as an accepted TCP socket can be materialized by - // the main-thread pump after the originating callback has returned. At - // that point Perry's current execution id is the bootstrap scope even - // though the provider has an explicit trigger. Inherit the trigger's - // captured store in that narrow case so AsyncLocalStorage crosses the - // native hand-off just as it does in Node. - let context = if execution_async_id_u64() == 0 && trigger_async_id != 0 { - RESOURCES - .lock() - .unwrap() - .get(&trigger_async_id) - .map(|meta| meta.context.clone()) - .unwrap_or_else(crate::async_context::capture_context) - } else { - crate::async_context::capture_context() - }; RESOURCES.lock().unwrap().insert( async_id, ResourceMeta { type_name: type_name.to_string(), trigger_async_id, resource, - context, + context: crate::async_context::capture_context(), destroyed: false, }, ); @@ -819,7 +702,8 @@ fn destroy_with_kind(async_id: u64, is_promise: bool) { meta.destroyed = true; true } - Some(_) | None => false, + Some(_) => false, + None => true, } }; if !should_emit { @@ -835,141 +719,10 @@ pub fn destroy(async_id: u64) { destroy_with_kind(async_id, false); } -/// Explicit `AsyncResource.emitDestroy()` notification. Unlike native -/// provider teardown, Node does not make this API idempotent: every call emits -/// a destroy hook for the resource id. Remove the tracked metadata after the -/// first notification, but continue delivering later explicit notifications. -fn emit_explicit_destroy(async_id: u64) { - if async_id == 0 { - return; - } - with_hook_callbacks(HookPhase::Destroy, false, |callback| { - js_closure_call1(callback, async_id as f64); - }); - RESOURCES.lock().unwrap().remove(&async_id); -} - 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, - ) - }); - before(ids.async_id, ids.trigger_async_id); - let result = scope.root_nanbox_f64(completion()); - after(ids.async_id); - destroy(ids.async_id); - 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) { - 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), - ); - before(ids.async_id, ids.trigger_async_id); - crate::async_context::push_context_guard( - crate::async_context::ContextGuardAction::RestoreExecutionIds, - ); -} - -/// 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); - if let Some(action) = crate::async_context::pop_context_guard() { - crate::async_context::apply_context_guard(action); - } -} - -pub fn run_resource_scope(ids: AsyncResourceIds, completion: impl FnOnce()) { - enter_resource_scope(ids); - completion(); - 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); @@ -1003,7 +756,7 @@ fn string_header_to_string(ptr: *const StringHeader) -> String { } unsafe { let len = (*ptr).byte_len as usize; - let data = crate::string::string_data(ptr); + let data = (ptr as *const u8).add(std::mem::size_of::()); String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() } } @@ -1161,22 +914,12 @@ fn trigger_id_from_options(options: f64) -> u64 { throw_null_trigger_async_id_options(); } - // Node's constructor first validates the option and then consumes it, - // making an accessor observable twice. Preserve that exact ordering; the - // `requireManualDestroy` option is read after both trigger-id reads. - let first_trigger_value = object_field(options, b"triggerAsyncId"); - if !JSValue::from_bits(first_trigger_value.to_bits()).is_undefined() { - let _ = trigger_async_id_or_throw(first_trigger_value); - } let trigger_value = object_field(options, b"triggerAsyncId"); let trigger_value_kind = JSValue::from_bits(trigger_value.to_bits()); - let trigger_id = if trigger_value_kind.is_undefined() { - execution_async_id_u64() - } else { - trigger_async_id_or_throw(trigger_value) - }; - let _ = object_field(options, b"requireManualDestroy"); - trigger_id + if trigger_value_kind.is_undefined() { + return execution_async_id_u64(); + } + trigger_async_id_or_throw(trigger_value) } fn render_apply_value(value: f64) -> String { @@ -1254,206 +997,20 @@ fn validate_bind_callback(value: f64) { #[no_mangle] pub extern "C" fn js_async_resource_new(type_value: f64, options: f64) -> i64 { - new_async_resource_with_public_value(type_value, options, None) -} - -fn new_async_resource_with_public_value( - type_value: f64, - options: f64, - public_resource: Option, -) -> i64 { let scope = crate::gc::RuntimeHandleScope::new(); let type_handle = scope.root_nanbox_f64(type_value); let options_handle = scope.root_nanbox_f64(options); let type_name = require_string_arg("type", type_handle.get_nanbox_f64()); - if type_name.is_empty() && hooks_active() { - crate::fs::validate::throw_type_error_with_code( - "The \"type\" argument must be a non-empty string", - "ERR_ASYNC_TYPE", - ); - } let trigger_async_id = trigger_id_from_options(options_handle.get_nanbox_f64()); - // The public resource object is the constructor handle itself. Allocate it - // before firing init so the fourth callback argument is already the exact - // object returned by `new AsyncResource(...)`, as it is in Node. - let handle = Box::into_raw(Box::new(AsyncResourceHandle { - ids: AsyncResourceIds { - async_id: 0, - trigger_async_id, - }, - event_emitter: 0, - })) as i64; + let ids = init_resource_with_trigger(&type_name, TAG_UNDEFINED_F64, true, trigger_async_id); + let handle = Box::into_raw(Box::new(AsyncResourceHandle { ids })) as i64; ASYNC_RESOURCE_HANDLES.lock().unwrap().insert(handle); ASYNC_RESOURCE_HANDLE_COUNT.fetch_add(1, Ordering::Relaxed); - let resource_value = public_resource.unwrap_or_else(|| crate::value::js_nanbox_pointer(handle)); - let ids = init_resource_with_trigger(&type_name, resource_value, true, trigger_async_id); - unsafe { (*(handle as *mut AsyncResourceHandle)).ids = ids }; - handle -} - -/// Initialize the native backing for a source-compiled -/// `class X extends AsyncResource` while keeping the public subclass object as -/// the resource passed to hooks and returned by `executionAsyncResource()`. -#[no_mangle] -pub extern "C" fn js_async_resource_subclass_init( - this_value: f64, - type_value: f64, - options: f64, -) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let this_handle = scope.root_nanbox_f64(this_value); - let type_handle = scope.root_nanbox_f64(type_value); - let options_handle = scope.root_nanbox_f64(options); - let backing = new_async_resource_with_public_value( - type_handle.get_nanbox_f64(), - 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; - 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, - ); - crate::object::js_object_set_field_by_name( - raw, - key, - crate::value::js_nanbox_pointer(backing), - ); - for (name, length) in [ - ("asyncId", 0), - ("triggerAsyncId", 0), - ("emitDestroy", 0), - ("runInAsyncScope", 2), - ("bind", 2), - ] { - let method = if name == "bind" { - async_resource_bind_method_value(backing) - } else { - crate::object::async_resource_prototype_method_value(name, length) - }; - let method_handle = scope.root_nanbox_f64(method); - let method_key = js_string_from_bytes(name.as_ptr(), name.len() as u32); - let current_this = this_handle.get_nanbox_f64(); - let current_raw = - crate::value::js_nanbox_get_pointer(current_this) as *mut ObjectHeader; - crate::object::js_object_set_field_by_name( - current_raw, - method_key, - method_handle.get_nanbox_f64(), - ); - crate::object::set_builtin_property_attrs( - current_raw as usize, - name.to_string(), - crate::object::PropertyAttrs::new(true, false, true), - ); - } - } - this_handle.get_nanbox_f64() -} - -/// Link the backing AsyncResource owned by EventEmitterAsyncResource to its -/// public emitter. Node exposes this as `emitter.asyncResource.eventEmitter`. -/// Both sides are stable native handles, so the link does not need GC rooting. -pub fn set_async_resource_event_emitter(handle: i64, event_emitter: i64) { - if handle == 0 || !ASYNC_RESOURCE_HANDLES.lock().unwrap().contains(&handle) { - return; + let resource_value = crate::value::js_nanbox_pointer(handle); + if let Some(meta) = RESOURCES.lock().unwrap().get_mut(&ids.async_id) { + meta.resource = resource_value; } - unsafe { (*(handle as *mut AsyncResourceHandle)).event_emitter = event_emitter }; -} - -#[no_mangle] -pub extern "C" fn js_async_resource_set_event_emitter(handle: i64, event_emitter: i64) { - set_async_resource_event_emitter(handle, event_emitter); -} - -/// Node exposes `AsyncResource#bind` as an instance-bound function: unlike -/// the other prototype methods, extracting it and calling it later keeps the -/// originating resource as its receiver. Use a dedicated rest trampoline -/// instead of the generic class-method reifier, whose ordinary semantics use -/// the call-site `this` for a detached function. -extern "C" fn async_resource_bind_method_trampoline( - closure: *const ClosureHeader, - rest: f64, -) -> f64 { - if closure.is_null() { - 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() { - 0 - } else { - js_array_length(args_array) - }; - let callback = if args_len == 0 { - TAG_UNDEFINED_F64 - } else { - crate::array::js_array_get_f64(args_array, 0) - }; - let this_arg = if args_len < 2 { - TAG_UNDEFINED_F64 - } else { - crate::array::js_array_get_f64(args_array, 1) - }; - let bound = js_async_resource_bind(handle, callback, this_arg); - if bound == 0 { - TAG_UNDEFINED_F64 - } else { - crate::value::js_nanbox_pointer(bound) - } -} - -fn async_resource_bind_method_value(handle: i64) -> f64 { - let trampoline = async_resource_bind_method_trampoline as *const u8; - js_register_closure_rest(trampoline, 0); - let closure = js_closure_alloc(trampoline, 1); - if closure.is_null() { - return TAG_UNDEFINED_F64; - } - js_closure_set_capture_ptr(closure, 0, handle); - crate::object::set_builtin_closure_length(closure as usize, 2); - crate::object::set_bound_native_closure_name(closure, "bind"); - crate::value::js_nanbox_pointer(closure as i64) -} - -pub fn try_async_resource_property_dispatch(handle: i64, property: &str) -> Option { - if ASYNC_RESOURCE_HANDLE_COUNT.load(Ordering::Relaxed) == 0 - || !ASYNC_RESOURCE_HANDLES.lock().unwrap().contains(&handle) - { - return None; - } - // User-defined own properties shadow AsyncResource.prototype just as they - // do on Node's ordinary public resource object. The backing allocation is - // a native Box, so keep expandos in the same traced side table used by - // small native handles rather than ever treating it as an ObjectHeader. - if let Some(value) = crate::object::handle_expando::handle_expando_get(handle, property) { - return Some(value); - } - if property == "bind" { - return Some(async_resource_bind_method_value(handle)); - } - if let Some((name, length)) = match property { - "asyncId" => Some(("asyncId", 0)), - "triggerAsyncId" => Some(("triggerAsyncId", 0)), - "emitDestroy" => Some(("emitDestroy", 0)), - "runInAsyncScope" => Some(("runInAsyncScope", 2)), - _ => None, - } { - return Some(crate::object::async_resource_prototype_method_value( - name, length, - )); - } - if property != "eventEmitter" { - return None; - } - let emitter = unsafe { (*(handle as *const AsyncResourceHandle)).event_emitter }; - Some(if emitter == 0 { - TAG_UNDEFINED_F64 - } else { - crate::value::js_nanbox_pointer(emitter) - }) + handle } /// Dynamic method dispatch for `AsyncResource` receivers whose static type @@ -1462,7 +1019,7 @@ pub fn try_async_resource_property_dispatch(handle: i64, property: &str) -> Opti /// can never be claimed. Returns `None` when the receiver is not a live /// AsyncResource handle or the method name is not part of its vocabulary. pub fn try_async_resource_method_dispatch( - receiver: i64, + handle: i64, method_name: &str, args_ptr: *const f64, args_len: usize, @@ -1476,7 +1033,9 @@ pub fn try_async_resource_method_dispatch( ) { return None; } - let handle = resolve_async_resource_handle(receiver)?; + if !ASYNC_RESOURCE_HANDLES.lock().unwrap().contains(&handle) { + return None; + } let scope = crate::gc::RuntimeHandleScope::new(); let raw_args: Vec = if args_ptr.is_null() || args_len == 0 { Vec::new() @@ -1490,7 +1049,7 @@ pub fn try_async_resource_method_dispatch( "triggerAsyncId" => js_async_resource_trigger_async_id(handle), "emitDestroy" => { js_async_resource_emit_destroy(handle); - crate::value::js_nanbox_pointer(receiver) + crate::value::js_nanbox_pointer(handle) } "runInAsyncScope" => { // runInAsyncScope(fn[, thisArg, ...args]) @@ -1535,27 +1094,27 @@ fn pack_rest_args_array(rest: &[f64]) -> i64 { #[no_mangle] pub extern "C" fn js_async_resource_async_id(handle: i64) -> f64 { - let Some(handle) = resolve_async_resource_handle(handle) else { + if handle == 0 { return 0.0; - }; + } let resource = unsafe { &*(handle as *const AsyncResourceHandle) }; resource.ids.async_id as f64 } #[no_mangle] pub extern "C" fn js_async_resource_trigger_async_id(handle: i64) -> f64 { - let Some(handle) = resolve_async_resource_handle(handle) else { + if handle == 0 { return 0.0; - }; + } let resource = unsafe { &*(handle as *const AsyncResourceHandle) }; async_id_to_js_number(resource.ids.trigger_async_id) } #[no_mangle] pub extern "C" fn js_async_resource_emit_destroy(handle: i64) -> i64 { - if let Some(backing) = resolve_async_resource_handle(handle) { - let resource = unsafe { &*(backing as *const AsyncResourceHandle) }; - emit_explicit_destroy(resource.ids.async_id); + if handle != 0 { + let resource = unsafe { &*(handle as *const AsyncResourceHandle) }; + destroy(resource.ids.async_id); } handle } @@ -1567,9 +1126,9 @@ 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 { + if handle == 0 { return TAG_UNDEFINED_F64; - }; + } if !is_callable_value(callback_value) { throw_apply_not_function(callback_value); } @@ -1606,29 +1165,20 @@ pub extern "C" fn js_async_resource_run_in_async_scope( 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) } + let result = 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() } 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() - } else { - unsafe { (arr as *const u8).add(std::mem::size_of::()) as *const f64 } - }; - unsafe { js_closure_call_array(callback as i64, data, len) } - } - }); + 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(prev_this); - let threw = outcome.is_err(); - let result_handle = scope.root_nanbox_f64(match outcome { - Ok(result) | Err(result) => result, - }); + let result_handle = scope.root_nanbox_f64(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(); @@ -1640,9 +1190,6 @@ pub extern "C" fn js_async_resource_run_in_async_scope( if let Some(action) = crate::async_context::pop_context_guard() { crate::async_context::apply_context_guard(action); } - if threw { - crate::exception::js_throw(result_handle.get_nanbox_f64()); - } result_handle.get_nanbox_f64() } @@ -1686,9 +1233,9 @@ 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 { + if handle == 0 { return 0; - }; + } register_bind_trampoline_once(); let scope = crate::gc::RuntimeHandleScope::new(); let callback_handle = scope.root_nanbox_f64(callback_value); @@ -1755,21 +1302,7 @@ pub extern "C" fn js_async_resource_static_bind_value( let scope = crate::gc::RuntimeHandleScope::new(); let callback_handle = scope.root_nanbox_f64(callback_value); let type_value = if JSValue::from_bits(type_value.to_bits()).is_undefined() { - let callback = crate::fs::extract_closure_ptr(callback_handle.get_nanbox_f64()); - let inferred = if callback.is_null() { - None - } else { - let own_name = crate::closure::closure_get_dynamic_prop(callback as usize, "name"); - let own_name = JSValue::from_bits(own_name.to_bits()); - if own_name.is_any_string() { - let name = js_string_value_to_string(f64::from_bits(own_name.bits())); - (!name.is_empty()).then_some(name) - } else { - unsafe { crate::builtins::function_name_for_ptr((*callback).func_ptr as usize) } - .filter(|name| !name.is_empty()) - } - }; - let default_type = inferred.as_deref().unwrap_or("bound-anonymous-fn"); + let default_type = b"AsyncResource"; box_string( js_string_from_bytes(default_type.as_ptr(), default_type.len() as u32) as *const u8, ) @@ -1898,11 +1431,9 @@ extern "C" fn async_local_storage_snapshot_trampoline( rest: f64, ) -> f64 { let snapshot_id = js_closure_get_capture_ptr(closure, 0) as usize; + let this_arg = crate::object::js_implicit_this_get(); run_with_context_snapshot(snapshot_id, || { - // AsyncLocalStorage.snapshot() intentionally invokes the supplied - // callback as a plain function. The receiver used to call the snapshot - // wrapper itself is not forwarded. - call_callback_with_rest(callback_value, TAG_UNDEFINED_F64, rest) + call_callback_with_rest(callback_value, this_arg, rest) }) } @@ -1960,11 +1491,7 @@ pub fn scan_async_hooks_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_ drop(hooks); let mut resources = RESOURCES.lock().unwrap(); for meta in resources.values_mut() { - // Resource identity is weak: the resource's owning scheduler/promise - // keeps it alive, while its finalizer enqueues the destroy event and - // removes this metadata. Marking the value here made PROMISE entries - // immortal and forced the runtime to fake destroy-at-settlement. - visitor.visit_metadata_nanbox_f64_slot(&mut meta.resource); + visitor.visit_nanbox_f64_slot(&mut meta.resource); crate::async_context::scan_snapshot_roots_mut(&mut meta.context, visitor); } drop(resources); @@ -1980,19 +1507,171 @@ pub fn scan_async_hooks_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_ visitor.visit_nanbox_u64_slot(&mut providers_bits); ASYNC_WRAP_PROVIDERS.store(providers_bits, Ordering::Relaxed); } +} - let mut top_level_bits = TOP_LEVEL_RESOURCE.load(Ordering::Relaxed); - if top_level_bits != 0 { - visitor.visit_nanbox_u64_slot(&mut top_level_bits); - TOP_LEVEL_RESOURCE.store(top_level_bits, Ordering::Relaxed); - } +#[cfg(test)] +pub fn reset_for_tests() { + HOOKS.lock().unwrap().clear(); + RESOURCES.lock().unwrap().clear(); + GC_DESTROY_QUEUE.lock().unwrap().clear(); + CONTEXT_SNAPSHOTS.lock().unwrap().clear(); + ASYNC_WRAP_PROVIDERS.store(0, Ordering::Relaxed); + HOOKS_ACTIVE.store(0, Ordering::Relaxed); + PROMISE_HOOKS_ACTIVE.store(0, Ordering::Relaxed); + NEXT_ASYNC_ID.store(2, Ordering::Relaxed); + NEXT_CONTEXT_SNAPSHOT_ID.store(1, Ordering::Relaxed); + CURRENT_EXECUTION_ID.with(|c| c.set(0)); + CURRENT_TRIGGER_ID.with(|c| c.set(0)); + IN_HOOK_CALLBACK.with(|c| c.set(false)); + EXECUTION_STACK.with(|s| s.borrow_mut().clear()); } #[cfg(test)] -mod test_support; +pub(crate) fn test_seed_async_hooks_scanner_roots(callback: *const ClosureHeader, resource: f64) { + reset_for_tests(); + HOOKS.lock().unwrap().push(HookRecord { + callbacks: HookCallbacks { + init: callback, + before: callback, + after: callback, + destroy: callback, + promise_resolve: callback, + }, + enabled: true, + track_promises: true, + }); + HOOKS_ACTIVE.store(1, Ordering::Relaxed); + PROMISE_HOOKS_ACTIVE.store(1, Ordering::Relaxed); + RESOURCES.lock().unwrap().insert( + 1, + ResourceMeta { + type_name: "test".to_string(), + trigger_async_id: 0, + resource, + context: crate::async_context::AsyncContextSnapshot::default(), + destroyed: false, + }, + ); +} + #[cfg(test)] -pub use test_support::reset_for_tests; +pub(crate) fn test_async_hooks_scanner_snapshot() -> (usize, u64) { + let callback = HOOKS + .lock() + .unwrap() + .first() + .map(|hook| hook.callbacks.init as usize) + .unwrap_or(0); + let resource_bits = RESOURCES + .lock() + .unwrap() + .get(&1) + .map(|meta| meta.resource.to_bits()) + .unwrap_or(0); + (callback, resource_bits) +} + #[cfg(test)] -pub(crate) use test_support::{ - test_async_hooks_scanner_snapshot, test_seed_async_hooks_scanner_roots, -}; +mod tests { + use super::*; + + // #7680: no lock needed here anymore. `NEXT_ASYNC_ID` / `HOOKS` / + // `RESOURCES` / etc. are `per_test_global!`, so this thread's + // `reset_for_tests()` and `init_resource` calls touch only this thread's + // own instances — a concurrent test on another thread cannot land a + // `+2` between the two `init_resource` calls below, which is exactly + // the #7672 shape this test's own docstring warns about (a wrong VALUE, + // not a hang). + + #[test] + fn resource_ids_are_monotonic_even_without_hooks() { + reset_for_tests(); + let a = init_resource("A", TAG_UNDEFINED_F64, true); + let b = init_resource("B", TAG_UNDEFINED_F64, true); + // Ids start above 1 (Node reserves 1 for the root context) and are + // monotonic. + assert!(a.async_id > 1); + assert_eq!(b.async_id, a.async_id + 1); + } + + #[test] + fn before_after_restore_execution_ids() { + reset_for_tests(); + let ids = init_resource("A", TAG_UNDEFINED_F64, true); + before(ids.async_id, ids.trigger_async_id); + assert_eq!(execution_async_id_u64(), ids.async_id); + after(ids.async_id); + assert_eq!(execution_async_id_u64(), 0); + } + + #[test] + fn track_promises_filters_hooks_and_activity() { + reset_for_tests(); + let mut callbacks = HookCallbacks::empty(); + callbacks.init = std::ptr::NonNull::::dangling().as_ptr(); + HOOKS.lock().unwrap().extend([ + HookRecord { + callbacks, + enabled: false, + track_promises: false, + }, + HookRecord { + callbacks, + enabled: false, + track_promises: true, + }, + ]); + let suppressed = AsyncHookHandle { index: 0 }; + let tracked = AsyncHookHandle { index: 1 }; + + js_async_hook_enable(&suppressed as *const AsyncHookHandle as i64); + assert!(hooks_active()); + assert!(!promise_hooks_active()); + + js_async_hook_enable(&tracked as *const AsyncHookHandle as i64); + assert!(promise_hooks_active()); + assert_eq!(enabled_callbacks(false).len(), 2); + assert_eq!(enabled_callbacks(true).len(), 1); + + js_async_hook_disable(&tracked as *const AsyncHookHandle as i64); + assert!(hooks_active()); + assert!(!promise_hooks_active()); + js_async_hook_disable(&suppressed as *const AsyncHookHandle as i64); + assert!(!hooks_active()); + reset_for_tests(); + } + + /// #7680: plants the #7672 shape directly rather than relying on a + /// scheduling accident — install a resource on THIS thread, run + /// `reset_for_tests()` (what all four of the pre-fix lock domains + /// eventually call) on ANOTHER thread, and assert the resource survived. + /// Revert the `per_test_global!` conversion above (back to bare + /// `static`s) and this fails with the resource gone — a foreign + /// thread's `reset_for_tests()` wiped `NEXT_ASYNC_ID` out from under a + /// resource this thread had already allocated, id and all. + #[test] + fn async_hooks_state_survives_a_foreign_reset_for_tests() { + reset_for_tests(); + let ids = init_resource("survivor", TAG_UNDEFINED_F64, true); + assert!( + RESOURCES.lock().unwrap().contains_key(&ids.async_id), + "the probe installed nothing, so survived-vs-wiped would be vacuous" + ); + + std::thread::spawn(reset_for_tests) + .join() + .expect("the clearing thread panicked"); + + assert!( + RESOURCES.lock().unwrap().contains_key(&ids.async_id), + "a resource installed on this thread was destroyed by a GC test guard's \ + async_hooks reset running on another thread (#7680). Per-thread storage \ + (`per_test_global!`) is what prevents this." + ); + assert!( + NEXT_ASYNC_ID.load(Ordering::Relaxed) > ids.async_id, + "NEXT_ASYNC_ID must not have been rewound by the foreign reset either" + ); + reset_for_tests(); + } +} diff --git a/crates/perry-runtime/src/async_hooks/test_support.rs b/crates/perry-runtime/src/async_hooks/test_support.rs deleted file mode 100644 index be53907020..0000000000 --- a/crates/perry-runtime/src/async_hooks/test_support.rs +++ /dev/null @@ -1,161 +0,0 @@ -use super::*; - -pub fn reset_for_tests() { - HOOKS.lock().unwrap().clear(); - RESOURCES.lock().unwrap().clear(); - GC_DESTROY_QUEUE.lock().unwrap().clear(); - CONTEXT_SNAPSHOTS.lock().unwrap().clear(); - ASYNC_WRAP_PROVIDERS.store(0, Ordering::Relaxed); - TOP_LEVEL_RESOURCE.store(0, Ordering::Relaxed); - HOOKS_ACTIVE.store(0, Ordering::Relaxed); - PROMISE_HOOKS_ACTIVE.store(0, Ordering::Relaxed); - NEXT_ASYNC_ID.store(2, Ordering::Relaxed); - NEXT_CONTEXT_SNAPSHOT_ID.store(1, Ordering::Relaxed); - CURRENT_EXECUTION_ID.with(|c| c.set(0)); - CURRENT_TRIGGER_ID.with(|c| c.set(0)); - EXECUTION_STACK.with(|s| s.borrow_mut().clear()); -} - -pub(crate) fn test_seed_async_hooks_scanner_roots(callback: *const ClosureHeader, resource: f64) { - reset_for_tests(); - HOOKS.lock().unwrap().push(HookRecord { - callbacks: HookCallbacks { - init: callback, - before: callback, - after: callback, - destroy: callback, - promise_resolve: callback, - }, - enabled: true, - track_promises: true, - }); - HOOKS_ACTIVE.store(1, Ordering::Relaxed); - PROMISE_HOOKS_ACTIVE.store(1, Ordering::Relaxed); - RESOURCES.lock().unwrap().insert( - 1, - ResourceMeta { - type_name: "test".to_string(), - trigger_async_id: 0, - resource, - context: crate::async_context::AsyncContextSnapshot::default(), - destroyed: false, - }, - ); -} - -pub(crate) fn test_async_hooks_scanner_snapshot() -> (usize, u64) { - let callback = HOOKS - .lock() - .unwrap() - .first() - .map(|hook| hook.callbacks.init as usize) - .unwrap_or(0); - let resource_bits = RESOURCES - .lock() - .unwrap() - .get(&1) - .map(|meta| meta.resource.to_bits()) - .unwrap_or(0); - (callback, resource_bits) -} - -#[cfg(test)] -mod tests { - use super::*; - - // #7680: no lock needed here anymore. The per-test globals isolate this - // thread's reset and resource-id sequence from concurrent tests. - #[test] - fn resource_ids_are_monotonic_even_without_hooks() { - reset_for_tests(); - let a = init_resource("A", TAG_UNDEFINED_F64, true); - let b = init_resource("B", TAG_UNDEFINED_F64, true); - assert!(a.async_id > 1); - assert_eq!(b.async_id, a.async_id + 1); - } - - #[test] - fn before_after_restore_execution_ids() { - reset_for_tests(); - let ids = init_resource("A", TAG_UNDEFINED_F64, true); - before(ids.async_id, ids.trigger_async_id); - assert_eq!(execution_async_id_u64(), ids.async_id); - after(ids.async_id); - assert_eq!(execution_async_id_u64(), 0); - } - - #[test] - fn track_promises_filters_hooks_and_activity() { - reset_for_tests(); - let mut callbacks = HookCallbacks::empty(); - callbacks.init = std::ptr::NonNull::::dangling().as_ptr(); - HOOKS.lock().unwrap().extend([ - HookRecord { - callbacks, - enabled: false, - track_promises: false, - }, - HookRecord { - callbacks, - enabled: false, - track_promises: true, - }, - ]); - let suppressed = AsyncHookHandle { index: 0 }; - let tracked = AsyncHookHandle { index: 1 }; - js_async_hook_enable(&suppressed as *const AsyncHookHandle as i64); - assert!(hooks_active()); - assert!(!promise_hooks_active()); - js_async_hook_enable(&tracked as *const AsyncHookHandle as i64); - assert!(promise_hooks_active()); - assert_eq!(enabled_callbacks(false).len(), 2); - assert_eq!(enabled_callbacks(true).len(), 1); - js_async_hook_disable(&tracked as *const AsyncHookHandle as i64); - assert!(hooks_active()); - assert!(!promise_hooks_active()); - js_async_hook_disable(&suppressed as *const AsyncHookHandle as i64); - assert!(!hooks_active()); - reset_for_tests(); - } - - #[test] - fn async_hooks_state_survives_a_foreign_reset_for_tests() { - reset_for_tests(); - let ids = init_resource("survivor", TAG_UNDEFINED_F64, true); - assert!(RESOURCES.lock().unwrap().contains_key(&ids.async_id)); - std::thread::spawn(reset_for_tests) - .join() - .expect("the clearing thread panicked"); - assert!(RESOURCES.lock().unwrap().contains_key(&ids.async_id)); - assert!(NEXT_ASYNC_ID.load(Ordering::Relaxed) > ids.async_id); - reset_for_tests(); - } - - #[test] - fn native_async_resource_accepts_string_and_symbol_expandos() { - reset_for_tests(); - crate::symbol::test_clear_symbol_side_table_roots(); - let type_ptr = js_string_from_bytes(b"ExpandoResource".as_ptr(), 15); - let type_value = crate::value::js_nanbox_string(type_ptr as i64); - let handle = js_async_resource_new(type_value, TAG_UNDEFINED_F64); - assert!(is_async_resource_handle(handle)); - let resource = crate::value::js_nanbox_pointer(handle); - let symbol = unsafe { crate::symbol::js_symbol_new_empty() }; - unsafe { - crate::symbol::js_object_set_symbol_property(resource, symbol, TAG_UNDEFINED_F64); - } - crate::value::js_dyn_index_set(resource, symbol, 41.0); - assert_eq!( - unsafe { crate::symbol::js_object_get_symbol_property(resource, symbol) }.to_bits(), - 41.0f64.to_bits() - ); - let name_ptr = js_string_from_bytes(b"label".as_ptr(), 5); - let name = crate::value::js_nanbox_string(name_ptr as i64); - crate::proxy::js_put_value_set(resource, name, 42.0, resource, 1); - assert_eq!( - try_async_resource_property_dispatch(handle, "label").map(f64::to_bits), - Some(42.0f64.to_bits()) - ); - reset_for_tests(); - } -} diff --git a/crates/perry-runtime/src/child_process/emitter.rs b/crates/perry-runtime/src/child_process/emitter.rs index 8953d87654..312ca96a42 100644 --- a/crates/perry-runtime/src/child_process/emitter.rs +++ b/crates/perry-runtime/src/child_process/emitter.rs @@ -43,12 +43,6 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { return true; } - let async_ids = - cp_handle_of(target).and_then(|handle| reactor::cp_async_scope_for_target(handle, target)); - if let Some(ids) = async_ids { - crate::async_hooks::enter_resource_scope(ids); - } - let key = cp_listener_key(event); let mut i: u32 = 0; let mut fired = false; @@ -78,9 +72,6 @@ pub(crate) fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool { crate::node_stream::emit_to_stream_listeners(target, event.as_bytes(), args); } - if let Some(ids) = async_ids { - crate::async_hooks::leave_resource_scope(ids.async_id); - } fired } diff --git a/crates/perry-runtime/src/child_process/reactor.rs b/crates/perry-runtime/src/child_process/reactor.rs index ef4136a0e1..b31ef82be3 100644 --- a/crates/perry-runtime/src/child_process/reactor.rs +++ b/crates/perry-runtime/src/child_process/reactor.rs @@ -99,9 +99,6 @@ static CP_EVENT_QUEUE: Mutex> = Mutex::new(Vec::new()); struct LiveChild { /// NaN-boxed ChildProcess object — a GC root (see `cp_reactor_scan_roots_mut`). cp_bits: u64, - process_ids: crate::async_hooks::AsyncResourceIds, - pipe_ids: [crate::async_hooks::AsyncResourceIds; 3], - pipe_bits: [u64; 3], pid: i32, stdin: Option, stdout_open: bool, @@ -199,24 +196,6 @@ pub(super) struct CpExecPending { static CP_LIVE: Mutex>> = Mutex::new(None); -fn cp_init_async_resources( - cp: f64, - stdin_obj: f64, - stdout_obj: f64, - stderr_obj: f64, -) -> ( - crate::async_hooks::AsyncResourceIds, - [crate::async_hooks::AsyncResourceIds; 3], -) { - let process_ids = crate::async_hooks::init_resource("PROCESSWRAP", cp, true); - let pipe_ids = [ - crate::async_hooks::init_resource("PIPEWRAP", stdin_obj, true), - crate::async_hooks::init_resource("PIPEWRAP", stdout_obj, true), - crate::async_hooks::init_resource("PIPEWRAP", stderr_obj, true), - ]; - (process_ids, pipe_ids) -} - thread_local! { /// Re-entrancy guard — an emitted handler may itself drive the event loop /// (`await`), which re-enters `js_run_stdlib_pump` → `cp_reactor_pump`. @@ -507,7 +486,6 @@ 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); // For fork, keep a clone of the IPC socket for send/disconnect; the reader // thread owns the original. @@ -526,13 +504,6 @@ fn cp_register_live_child_parts( handle, LiveChild { cp_bits: cp.to_bits(), - process_ids, - pipe_ids, - pipe_bits: [ - stdin_obj.to_bits(), - stdout_obj.to_bits(), - stderr_obj.to_bits(), - ], pid: pid as i32, stdin: stdin_pipe, stdout_open, @@ -1120,8 +1091,6 @@ 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 mut guard = cp_live_lock(); @@ -1130,13 +1099,6 @@ pub(super) fn cp_exec_async( handle, LiveChild { cp_bits: cp.to_bits(), - process_ids, - pipe_ids, - pipe_bits: [ - TAG_NULL_F64.to_bits(), - stdout_obj.to_bits(), - stderr_obj.to_bits(), - ], pid: pid as i32, stdin: None, stdout_open, @@ -1541,8 +1503,6 @@ fn cp_reactor_pump_inner() { abort_signal_bits: lc.abort_signal_bits, abort_listener_bits: lc.abort_listener_bits, exec: lc.exec.take(), - process_ids: lc.process_ids, - pipe_ids: lc.pipe_ids, }); lc.abort_signal_bits = 0; lc.abort_listener_bits = 0; @@ -1564,9 +1524,7 @@ fn cp_reactor_pump_inner() { cp_set_field(cp, b"exitCode", code_f); cp_set_field(cp, b"signalCode", signal_f); cp_emit(cp, "exit", &[code_f, signal_f]); - crate::async_hooks::enter_resource_scope(item.process_ids); cp_exec_fire_close(exec, item.code, item.signal, item.pid); - crate::async_hooks::leave_resource_scope(item.process_ids.async_id); cp_emit(cp, "close", &[code_f, signal_f]); } else { let cp = f64::from_bits(item.cp_bits); @@ -1584,10 +1542,6 @@ fn cp_reactor_pump_inner() { if let Some(map) = cp_live_lock().as_mut() { map.remove(&item.handle); } - crate::async_hooks::destroy(item.process_ids.async_id); - for pipe in item.pipe_ids { - crate::async_hooks::destroy(pipe.async_id); - } CP_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); } } @@ -1603,8 +1557,6 @@ struct CpCloseItem { abort_signal_bits: u64, abort_listener_bits: u64, exec: Option>, - process_ids: crate::async_hooks::AsyncResourceIds, - pipe_ids: [crate::async_hooks::AsyncResourceIds; 3], } #[inline] @@ -1624,23 +1576,6 @@ fn cp_lookup_cp_bits(handle: u64) -> Option { .and_then(|map| map.get(&handle).map(|lc| lc.cp_bits)) } -pub(super) fn cp_async_scope_for_target( - handle: u64, - target: f64, -) -> Option { - let target_bits = target.to_bits(); - let guard = cp_live_lock(); - let child = guard.as_ref()?.get(&handle)?; - if target_bits == child.cp_bits { - return Some(child.process_ids); - } - child - .pipe_bits - .iter() - .position(|bits| *bits == target_bits) - .map(|index| child.pipe_ids[index]) -} - // ============================================================================ // Live `stdin.write()` / `kill()` — called from the mod.rs method bodies. // ============================================================================ @@ -1920,6 +1855,133 @@ pub(crate) fn cp_reactor_scan_roots_mut(visitor: &mut crate::gc::RuntimeRootVisi } } +// ============================================================================ +// Tests +// ============================================================================ + +/// Windows termination tests. `child.kill()`, spawn `{ timeout }`, +/// `AbortSignal`, and the exec `maxBuffer` breach all funnel through +/// `cp_live_kill_signum` / `cp_live_kill_signal` → `cp_win_kill`, so +/// terminating one live child through the shared path exercises the machinery +/// all of them rely on. #[cfg(all(test, windows))] -#[path = "reactor/windows_kill_tests.rs"] -mod windows_kill_tests; +mod windows_kill_tests { + use super::*; + + /// Raw `cp_win_dup_proc_handle` + `cp_win_kill`: sig-0 existence probe on + /// a live child, terminate through the duplicated handle, probe + kill + /// failure after death. The held duplicate keeps naming the original + /// process object even once the child is reaped — exactly the property + /// that closes the pid-reuse race. + #[test] + fn win_kill_probe_and_terminate() { + let mut child = std::process::Command::new("ping") + .args(["-n", "30", "127.0.0.1"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn ping"); + let proc_handle = cp_win_dup_proc_handle(&child); + assert_ne!(proc_handle, 0, "DuplicateHandle should succeed"); + + // POSIX `kill(pid, 0)` analogue: existence probe, no side effect. + assert!( + cp_win_kill(proc_handle, 0), + "probe should see the live child" + ); + + // Any terminating signal degrades to `TerminateProcess(handle, 1)`. + assert!(cp_win_kill(proc_handle, 15), "terminate should succeed"); + let status = child.wait().expect("wait after TerminateProcess"); + assert_eq!(status.code(), Some(1), "TerminateProcess exit code"); + + // The duplicate still names the original (now-dead) process after the + // reap, so both the probe and a second kill deterministically fail — + // no pid-recycling flake window exists for a handle. + assert!(!cp_win_kill(proc_handle, 0), "probe should fail once dead"); + assert!( + !cp_win_kill(proc_handle, 15), + "kill after death reports undelivered" + ); + + // The test owns this duplicate (no LiveChild registry entry) — close + // it by hand. + unsafe { + let _ = windows_sys::Win32::Foundation::CloseHandle( + proc_handle as windows_sys::Win32::Foundation::HANDLE, + ); + } + } + + /// Registry-level liveness: the pump removes the entry once the child has + /// fully closed (exit reported + both streams at EOF). + fn handle_is_live(handle: u64) -> bool { + cp_live_lock() + .as_ref() + .is_some_and(|map| map.contains_key(&handle)) + } + + /// End-to-end through the reactor: spawn a long-running child via the + /// spawn FFI, terminate it through the same shared path `child.kill()` / + /// `{ timeout }` / `AbortSignal` use, and drive the pump until the exit + /// machinery completes. Asserts the Node-shaped exit for a Windows kill: + /// `exitCode: null`, `signalCode: 'SIGTERM'`. + #[test] + fn reactor_kill_terminates_live_child() { + // `ping -n 30 127.0.0.1` runs ~29s if not killed — long enough that a + // pass can only come from the kill path, short enough to bound a + // failure without hanging the suite. + let cmd = "ping"; + let cmd_ptr = crate::string::js_string_from_bytes(cmd.as_ptr(), cmd.len() as u32); + let mut args = crate::array::js_array_alloc(3); + for a in ["-n", "30", "127.0.0.1"] { + let s = crate::string::js_string_from_bytes(a.as_ptr(), a.len() as u32); + args = crate::array::js_array_push_f64(args, crate::value::js_nanbox_string(s as i64)); + } + let start = std::time::Instant::now(); + let cp = js_child_process_spawn_streams(cmd_ptr as i64, args as i64, 0); + + let pid = cp_get_field(cp, b"pid"); + assert!(pid > 0.0, "spawn should set a real pid, got {pid}"); + let handle = cp_get_field(cp, b"__cpHandle") as u64; + assert!(handle_is_live(handle)); + + // Phase 0 marks `spawned` — Phase B refuses to close before that. + cp_reactor_pump(); + + // Kill through the shared path (undefined signal → SIGTERM). + assert!( + cp_live_kill(handle, cp_undefined()), + "kill should be delivered" + ); + + // Drive the pump until the exit machinery completes: waiter reaps → + // `Exited` event → exit/close emitted → registry entry removed. + let deadline = std::time::Instant::now() + Duration::from_secs(15); + while handle_is_live(handle) { + assert!( + std::time::Instant::now() < deadline, + "child did not close within 15s of kill()" + ); + cp_reactor_pump(); + std::thread::sleep(Duration::from_millis(10)); + } + // Well under ping's ~29s natural runtime — it died from the kill. + assert!(start.elapsed() < Duration::from_secs(20)); + + // Node's Windows kill shape: exitCode null, signalCode 'SIGTERM' + // (the requested signal, not TerminateProcess's synthetic exit code). + assert_eq!( + cp_get_field(cp, b"exitCode").to_bits(), + TAG_NULL_F64.to_bits(), + "exitCode should be null after a signal kill" + ); + assert_eq!( + cp_value_to_string(cp_get_field(cp, b"signalCode")).as_deref(), + Some("SIGTERM") + ); + + // The child is reaped and deregistered — a second kill reports false. + assert!(!cp_live_kill(handle, cp_undefined())); + } +} diff --git a/crates/perry-runtime/src/child_process/reactor/windows_kill_tests.rs b/crates/perry-runtime/src/child_process/reactor/windows_kill_tests.rs deleted file mode 100644 index 7813c9d068..0000000000 --- a/crates/perry-runtime/src/child_process/reactor/windows_kill_tests.rs +++ /dev/null @@ -1,65 +0,0 @@ -use super::*; - -#[test] -fn win_kill_probe_and_terminate() { - let mut child = std::process::Command::new("ping") - .args(["-n", "30", "127.0.0.1"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("spawn ping"); - let proc_handle = cp_win_dup_proc_handle(&child); - assert_ne!(proc_handle, 0, "DuplicateHandle should succeed"); - assert!(cp_win_kill(proc_handle, 0)); - assert!(cp_win_kill(proc_handle, 15)); - let status = child.wait().expect("wait after TerminateProcess"); - assert_eq!(status.code(), Some(1)); - assert!(!cp_win_kill(proc_handle, 0)); - assert!(!cp_win_kill(proc_handle, 15)); - unsafe { - let _ = windows_sys::Win32::Foundation::CloseHandle( - proc_handle as windows_sys::Win32::Foundation::HANDLE, - ); - } -} - -fn handle_is_live(handle: u64) -> bool { - cp_live_lock() - .as_ref() - .is_some_and(|map| map.contains_key(&handle)) -} - -#[test] -fn reactor_kill_terminates_live_child() { - let cmd = "ping"; - let cmd_ptr = crate::string::js_string_from_bytes(cmd.as_ptr(), cmd.len() as u32); - let mut args = crate::array::js_array_alloc(3); - for a in ["-n", "30", "127.0.0.1"] { - let s = crate::string::js_string_from_bytes(a.as_ptr(), a.len() as u32); - args = crate::array::js_array_push_f64(args, crate::value::js_nanbox_string(s as i64)); - } - let start = std::time::Instant::now(); - let cp = js_child_process_spawn_streams(cmd_ptr as i64, args as i64, 0); - let pid = cp_get_field(cp, b"pid"); - assert!(pid > 0.0, "spawn should set a real pid, got {pid}"); - let handle = cp_get_field(cp, b"__cpHandle") as u64; - assert!(handle_is_live(handle)); - cp_reactor_pump(); - assert!(cp_live_kill(handle, cp_undefined())); - let deadline = std::time::Instant::now() + Duration::from_secs(15); - while handle_is_live(handle) { - assert!(std::time::Instant::now() < deadline); - cp_reactor_pump(); - std::thread::sleep(Duration::from_millis(10)); - } - assert!(start.elapsed() < Duration::from_secs(20)); - assert_eq!( - cp_get_field(cp, b"exitCode").to_bits(), - TAG_NULL_F64.to_bits() - ); - assert_eq!( - cp_value_to_string(cp_get_field(cp, b"signalCode")).as_deref(), - Some("SIGTERM") - ); - assert!(!cp_live_kill(handle, cp_undefined())); -} diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index 5dc56cd58b..dc9b934c99 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -78,7 +78,7 @@ pub unsafe extern "C" fn js_native_call_value( // probe can only match a bound native callable, which exists only once // `callable_exports` minted one (arming the table). if let Some(ops) = crate::object::nm_namespace_ops() { - if let Some(result) = unsafe { (ops.ee_dynamic_super)(func_value, args_ptr, args_len) } { + if let Some(result) = unsafe { (ops.ee_dynamic_super)(func_value) } { return result; } } diff --git a/crates/perry-runtime/src/dgram.rs b/crates/perry-runtime/src/dgram.rs index 17b151ee4d..e4cbcb0f6e 100644 --- a/crates/perry-runtime/src/dgram.rs +++ b/crates/perry-runtime/src/dgram.rs @@ -109,8 +109,6 @@ pub(crate) const KEY_ABORT_LISTENER: &[u8] = b"__perryDgramAbortListener"; /// Reactor id for the live OS socket (real mode only); links a JS socket back /// to its `UdpSocket` + recv thread in [`crate::dgram_reactor`]. pub(crate) const KEY_REACTOR_ID: &[u8] = b"__perryDgramReactorId"; -pub(crate) const KEY_ASYNC_ID: &[u8] = b"__perryDgramAsyncId"; -pub(crate) const KEY_TRIGGER_ASYNC_ID: &[u8] = b"__perryDgramTriggerAsyncId"; type MethodThunk = extern "C" fn(*const ClosureHeader, f64) -> f64; @@ -495,23 +493,7 @@ pub(crate) fn socket_object(socket_type: &str) -> f64 { ); } } - let scope = crate::gc::RuntimeHandleScope::new(); - let socket = scope.root_nanbox_f64(socket); - let ids = crate::async_hooks::init_resource("UDPWRAP", socket.get_nanbox_f64(), true); - set_hidden_value(socket.get_nanbox_f64(), KEY_ASYNC_ID, ids.async_id as f64); - set_hidden_value( - socket.get_nanbox_f64(), - KEY_TRIGGER_ASYNC_ID, - ids.trigger_async_id as f64, - ); - socket.get_nanbox_f64() -} - -pub(crate) fn socket_async_ids(socket: f64) -> crate::async_hooks::AsyncResourceIds { - crate::async_hooks::AsyncResourceIds { - async_id: get_hidden_value(socket, KEY_ASYNC_ID).unwrap_or(0.0) as u64, - trigger_async_id: get_hidden_value(socket, KEY_TRIGGER_ASYNC_ID).unwrap_or(0.0) as u64, - } + socket } extern "C" fn dgram_async_dispose(closure: *const ClosureHeader) -> f64 { diff --git a/crates/perry-runtime/src/dgram/listeners.rs b/crates/perry-runtime/src/dgram/listeners.rs index 940b339854..ae9d6ec64b 100644 --- a/crates/perry-runtime/src/dgram/listeners.rs +++ b/crates/perry-runtime/src/dgram/listeners.rs @@ -214,15 +214,12 @@ pub(crate) fn emit_event_value(socket: f64, event: f64, args: &[f64]) -> bool { if snapshot.is_empty() { return false; } - let async_ids = socket_async_ids(socket); - crate::async_hooks::enter_resource_scope(async_ids); if snapshot.iter().any(|(_, once)| *once) { remove_once_listeners(socket, event); } for (listener, _) in snapshot { call_function(listener, socket, args); } - crate::async_hooks::leave_resource_scope(async_ids.async_id); true } diff --git a/crates/perry-runtime/src/dgram/ops.rs b/crates/perry-runtime/src/dgram/ops.rs index 813b995683..8ef380f9f4 100644 --- a/crates/perry-runtime/src/dgram/ops.rs +++ b/crates/perry-runtime/src/dgram/ops.rs @@ -189,9 +189,7 @@ pub(crate) fn bind_impl(socket: f64, args: &[f64]) -> f64 { Ok(()) => { emit_event(socket, "listening", &[]); if let Some(callback) = callback_from_args(args) { - crate::async_hooks::run_resource_scope(socket_async_ids(socket), || { - call_function(callback, socket, &[]); - }); + call_function(callback, socket, &[]); } } Err(error) => { @@ -226,7 +224,6 @@ pub(crate) fn close_impl(socket: f64, args: &[f64]) -> f64 { if is_truthy_hidden(socket, KEY_CLOSED) { return undefined_value(); } - let async_ids = socket_async_ids(socket); if deterministic() { remove_bound_socket(socket); } else if let Some(id) = reactor_id(socket) { @@ -238,12 +235,8 @@ pub(crate) fn close_impl(socket: f64, args: &[f64]) -> f64 { set_hidden_value(socket, KEY_CLOSED, bool_value(true)); emit_event(socket, "close", &[]); if let Some(callback) = callback_from_args(args) { - // The UDPWRAP represents the socket lifetime. Node does not enter the - // socket's execution scope merely to invoke the optional close() - // completion callback (event listeners have their own dispatch path). call_function(callback, socket, &[]); } - crate::async_hooks::destroy(async_ids.async_id); socket } diff --git a/crates/perry-runtime/src/dns.rs b/crates/perry-runtime/src/dns.rs index 76a9fa7293..5453e9f7d5 100644 --- a/crates/perry-runtime/src/dns.rs +++ b/crates/perry-runtime/src/dns.rs @@ -985,10 +985,12 @@ fn lookup_all_result(addresses: &[ResolvedAddress]) -> f64 { array_value_from_values(&values) } -fn queue_callback(callback_value: f64, args: &[f64], provider_type: &'static str) { +fn queue_callback(callback_value: f64, args: &[f64]) { let callback = closure_ptr_from_value(callback_value) .unwrap_or_else(|| throw_error_value(invalid_callback_error(callback_value))); - crate::timer::schedule_native_callback(callback as i64, args, provider_type); + unsafe { + crate::builtins::js_queue_next_tick_args(callback as i64, args.as_ptr(), args.len() as i32); + } } fn lookup_value(hostname: &str, options: LookupOptions) -> Result { @@ -1151,12 +1153,16 @@ fn promise_reverse_args(args: i64) -> String { fn call_success_callback(callback: f64, value: f64) { let args = [null_value(), value]; - queue_callback(callback, &args, "QUERYWRAP"); + unsafe { + crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); + } } fn call_error_callback(callback: f64, error: f64) { let args = [error]; - queue_callback(callback, &args, "QUERYWRAP"); + unsafe { + crate::closure::js_native_call_value(callback, args.as_ptr(), args.len()); + } } fn dns_callback_resolve(args: i64, default_kind: Option) -> f64 { @@ -1258,16 +1264,5 @@ fn resolver_object(initial_servers: Vec) -> *mut ObjectHeader { for method in RESOLVER_RESOLVE_METHODS { js_object_set_field_by_name(obj, key(method), method_value(method)); } - 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( - "DNSCHANNEL", - js_nanbox_pointer(obj_ptr as i64), - true, - ); - }); - }); - obj_ptr + obj } diff --git a/crates/perry-runtime/src/dns/ffi.rs b/crates/perry-runtime/src/dns/ffi.rs index fe239d503c..059fc60c1b 100644 --- a/crates/perry-runtime/src/dns/ffi.rs +++ b/crates/perry-runtime/src/dns/ffi.rs @@ -35,11 +35,7 @@ pub extern "C" fn js_dns_lookup(args: i64) -> f64 { Ok(values) => values, Err(error) => vec![error], }; - queue_callback( - callback_handle.get_nanbox_f64(), - &callback_args, - "GETADDRINFOREQWRAP", - ); + queue_callback(callback_handle.get_nanbox_f64(), &callback_args); undefined_value() } @@ -66,7 +62,7 @@ pub extern "C" fn js_dns_lookup_service(args: i64) -> f64 { Ok((hostname, service)) => vec![null_value(), str_value(&hostname), str_value(&service)], Err(error) => vec![error], }; - queue_callback(callback_value, &callback_args, "GETNAMEINFOREQWRAP"); + queue_callback(callback_value, &callback_args); undefined_value() } diff --git a/crates/perry-runtime/src/fs/callbacks.rs b/crates/perry-runtime/src/fs/callbacks.rs index 89de75c6c5..2c226a2eda 100644 --- a/crates/perry-runtime/src/fs/callbacks.rs +++ b/crates/perry-runtime/src/fs/callbacks.rs @@ -7,6 +7,7 @@ use super::*; #[no_mangle] pub extern "C" fn js_fs_read_file_callback(path_value: f64, encoding: f64, callback: f64) -> f64 { + use crate::closure::js_closure_call2; const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; @@ -14,7 +15,7 @@ pub extern "C" fn js_fs_read_file_callback(path_value: f64, encoding: f64, callb unsafe { if let Some(err_val) = fs_callback_read_error(path_value, "open") { if !cb_ptr.is_null() { - defer_fs_callback_chain(cb_ptr, &[err_val, f64::from_bits(TAG_UNDEFINED)], 4); + js_closure_call2(cb_ptr, err_val, f64::from_bits(TAG_UNDEFINED)); } return f64::from_bits(TAG_UNDEFINED); } @@ -38,7 +39,7 @@ pub extern "C" fn js_fs_read_file_callback(path_value: f64, encoding: f64, callb }; if !cb_ptr.is_null() { - defer_fs_callback_chain(cb_ptr, &[f64::from_bits(TAG_NULL), data_val], 4); + js_closure_call2(cb_ptr, f64::from_bits(TAG_NULL), data_val); } f64::from_bits(TAG_UNDEFINED) } @@ -130,27 +131,54 @@ fn catch_callback_throw(call: impl FnOnce() -> f64) -> Result { } } -/// Deliver an fs completion on a later event-loop turn under a real -/// `FSREQCALLBACK` async resource. The timer queue roots both the callback and -/// values and brackets invocation with init/before/after/destroy, so every -/// callback-style fs API gets Node-compatible timing, hook ancestry, resource -/// identity, and AsyncLocalStorage propagation through one path. -fn defer_fs_callback(callback: *const ClosureHeader, args: &[f64]) { - if callback.is_null() { - return; +/// Trampoline body for a deferred single-arg fs completion callback. Invoked +/// with no JS args from the microtask drain (`js_closure_call0`); the real +/// callback and its single argument travel as tag-aware capture slots so the +/// GC keeps them live — and rewrites their addresses — across any collection +/// that lands between the enqueue and the drain. Slot 0 is the callback +/// NaN-boxed as a POINTER value, slot 1 the argument (an error value or `null`). +extern "C" fn deferred_fs_cb1_impl(closure: *const ClosureHeader) -> f64 { + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + let cb = extract_closure_ptr(crate::closure::js_closure_get_capture_f64(closure, 0)); + let arg0 = crate::closure::js_closure_get_capture_f64(closure, 1); + if !cb.is_null() { + crate::closure::js_closure_call1(cb, arg0); } - crate::timer::schedule_native_callback(callback as i64, args, "FSREQCALLBACK"); + f64::from_bits(TAG_UNDEFINED) } -fn defer_fs_callback_chain(callback: *const ClosureHeader, args: &[f64], count: usize) { +/// Deliver a void fs op's completion callback on a LATER tick instead of +/// synchronously (#6401). Node's async `fs.*` functions dispatch to the libuv +/// threadpool and never invoke the callback in the same turn they were called; +/// firing it inline reorders execution — code that runs `main()` from an fs +/// callback would then observe module-top-level `const`s that Node has already +/// initialized as still-uninitialized. We still run the syscall eagerly (as +/// before) and only defer the `(err)` / `(null)` delivery, mirroring the +/// already-deferred `fs.opendir`/`Dir.read` path (`dir_schedule_read_callback`). +fn defer_fs_cb1(callback: *const ClosureHeader, arg0: f64) { if callback.is_null() { return; } - crate::timer::schedule_native_callback_chain(callback as i64, args, "FSREQCALLBACK", count); + // Register the trampoline's arity (0 JS params) before it can first be + // dispatched, so `resolve_strategy` doesn't cache a stale strategy (#6475). + thread_local! { + static ARITY_REGISTERED: std::cell::Cell = const { std::cell::Cell::new(false) }; + } + ARITY_REGISTERED.with(|r| { + if !r.get() { + crate::closure::js_register_closure_arity(deferred_fs_cb1_impl as *const u8, 0); + r.set(true); + } + }); + let cb_boxed = crate::value::js_nanbox_pointer(callback as i64); + let closure = crate::closure::js_closure_alloc(deferred_fs_cb1_impl as *const u8, 2); + crate::closure::js_closure_set_capture_f64(closure, 0, cb_boxed); + crate::closure::js_closure_set_capture_f64(closure, 1, arg0); + crate::builtins::js_queue_microtask(closure as i64); } pub(crate) fn call_cb0(callback: *const ClosureHeader) { - defer_fs_callback(callback, &[f64::from_bits(0x7FFC_0000_0000_0002)]); + defer_fs_cb1(callback, f64::from_bits(0x7FFC_0000_0000_0002)); } /// Invoke a 2-arg callback with (err, undefined). Used by read-style ops @@ -158,14 +186,14 @@ pub(crate) fn call_cb0(callback: *const ClosureHeader) { pub(crate) unsafe fn call_cb_err2(callback: *const ClosureHeader, err_val: f64) { const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; if !callback.is_null() { - defer_fs_callback(callback, &[err_val, f64::from_bits(TAG_UNDEFINED)]); + crate::closure::js_closure_call2(callback, err_val, f64::from_bits(TAG_UNDEFINED)); } } /// Invoke a 1-arg callback with (err). Used by void ops (mkdir/unlink/rm/…) /// when the pre-flight probe detected an io::Error. pub(crate) unsafe fn call_cb_err1(callback: *const ClosureHeader, err_val: f64) { - defer_fs_callback(callback, &[err_val]); + defer_fs_cb1(callback, err_val); } /// `fs.writeFile(path, data, callback)` — sync write + immediate callback. @@ -318,7 +346,7 @@ pub extern "C" fn js_fs_exists_callback(path_value: f64, callback: f64) -> f64 { let cb = required_callback(callback); if !cb.is_null() { let arg = if exists { TAG_TRUE } else { TAG_FALSE }; - defer_fs_callback(cb, &[f64::from_bits(arg)]); + crate::closure::js_closure_call1(cb, f64::from_bits(arg)); } f64::from_bits(TAG_UNDEFINED) } @@ -350,7 +378,7 @@ pub extern "C" fn js_fs_readdir_callback(path_value: f64, arg1: f64, arg2: f64) let entries = f64::from_bits(crate::value::JSValue::pointer(entries.to_bits() as *const u8).bits()); if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), entries]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), entries); } f64::from_bits(TAG_UNDEFINED) } @@ -374,7 +402,7 @@ pub extern "C" fn js_fs_stat_callback(path_value: f64, arg1: f64, arg2: f64) -> } let stats = js_fs_stat_sync_options(path_value, options); if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), stats]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), stats); } f64::from_bits(TAG_UNDEFINED) } @@ -398,7 +426,7 @@ pub extern "C" fn js_fs_lstat_callback(path_value: f64, arg1: f64, arg2: f64) -> } let stats = js_fs_lstat_sync_options(path_value, options); if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), stats]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), stats); } f64::from_bits(TAG_UNDEFINED) } @@ -422,7 +450,7 @@ pub extern "C" fn js_fs_statfs_callback(path_value: f64, arg1: f64, arg2: f64) - } let stats = js_fs_statfs_sync_options(path_value, options); if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), stats]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), stats); } f64::from_bits(TAG_UNDEFINED) } @@ -441,7 +469,7 @@ pub extern "C" fn js_fs_opendir_callback(path_value: f64, arg1: f64, arg2: f64) } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), dir]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), dir); } f64::from_bits(TAG_UNDEFINED) } @@ -463,7 +491,7 @@ pub extern "C" fn js_fs_glob_callback(pattern_value: f64, arg1: f64, arg2: f64) }) { Ok(entries) => { if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), entries]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), entries); } } Err(err) => unsafe { call_cb_err2(cb, err) }, @@ -489,7 +517,7 @@ pub extern "C" fn js_fs_fstat_callback(fd_value: f64, arg1: f64, arg2: f64) -> f if !cb.is_null() { match result { Ok(stats) => { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), stats]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), stats); } Err(err) => unsafe { call_cb_err2(cb, err) }, } @@ -651,7 +679,7 @@ pub extern "C" fn js_fs_readlink_callback(path_value: f64, arg1: f64, arg2: f64) } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), value]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), value); } f64::from_bits(TAG_UNDEFINED) } @@ -675,7 +703,7 @@ pub extern "C" fn js_fs_realpath_callback(path_value: f64, arg1: f64, arg2: f64) } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), value]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), value); } f64::from_bits(TAG_UNDEFINED) } @@ -709,7 +737,7 @@ pub extern "C" fn js_fs_mkdtemp_callback(prefix_value: f64, arg1: f64, arg2: f64 }, }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), value]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), value); } f64::from_bits(TAG_UNDEFINED) } @@ -743,7 +771,7 @@ pub extern "C" fn js_fs_open_callback(path_value: f64, arg1: f64, arg2: f64, arg }, }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), fd]); + crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), fd); } f64::from_bits(TAG_UNDEFINED) } @@ -996,7 +1024,7 @@ pub extern "C" fn js_fs_read_callback( crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "read") { - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } let bytes = match crate::fs::read_sync_result( @@ -1010,12 +1038,12 @@ pub extern "C" fn js_fs_read_callback( // The callback form reports the syscall error, it does not throw. Err(err) => { let err_val = unsafe { build_fs_error_value_no_path(&err, "read") }; - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, buffer_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffer_value); } f64::from_bits(TAG_UNDEFINED) } @@ -1033,7 +1061,7 @@ pub extern "C" fn js_fs_read_callback_options( crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "read") { - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } let buffer_len = buffer_len_from_value(buffer_value) as f64; @@ -1048,12 +1076,12 @@ pub extern "C" fn js_fs_read_callback_options( // The callback form reports the syscall error, it does not throw. Err(err) => { let err_val = unsafe { build_fs_error_value_no_path(&err, "read") }; - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, buffer_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffer_value); } f64::from_bits(TAG_UNDEFINED) } @@ -1066,7 +1094,7 @@ pub extern "C" fn js_fs_write_callback(fd_value: f64, data_value: f64, callback: crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "write") { - defer_fs_callback(cb, &[err_val, 0.0, data_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, data_value); return f64::from_bits(TAG_UNDEFINED); } let bytes = match crate::fs::write_string_sync_result( @@ -1077,12 +1105,12 @@ pub extern "C" fn js_fs_write_callback(fd_value: f64, data_value: f64, callback: Ok(bytes) => bytes, Err(err) => { let err_val = unsafe { build_fs_error_value_no_path(&err, "write") }; - defer_fs_callback(cb, &[err_val, 0.0, data_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, data_value); return f64::from_bits(TAG_UNDEFINED); } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, data_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, data_value); } f64::from_bits(TAG_UNDEFINED) } @@ -1100,7 +1128,7 @@ pub extern "C" fn js_fs_write_buffer_callback_options( crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "write") { - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } let buffer_len = buffer_len_from_value(buffer_value) as f64; @@ -1119,12 +1147,12 @@ pub extern "C" fn js_fs_write_buffer_callback_options( Ok(bytes) => bytes, Err(err) => { let err_val = unsafe { build_fs_error_value_no_path(&err, "write") }; - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, buffer_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffer_value); } f64::from_bits(TAG_UNDEFINED) } @@ -1144,7 +1172,7 @@ pub extern "C" fn js_fs_write_buffer_callback( crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "write") { - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } let bytes = match crate::fs::write_buffer_sync_result( @@ -1157,12 +1185,12 @@ pub extern "C" fn js_fs_write_buffer_callback( Ok(bytes) => bytes, Err(err) => { let err_val = unsafe { build_fs_error_value_no_path(&err, "write") }; - defer_fs_callback(cb, &[err_val, 0.0, buffer_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffer_value); return f64::from_bits(TAG_UNDEFINED); } }; if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, buffer_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffer_value); } f64::from_bits(TAG_UNDEFINED) } @@ -1180,12 +1208,12 @@ pub extern "C" fn js_fs_readv_callback( crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "read") { - defer_fs_callback(cb, &[err_val, 0.0, buffers_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffers_value); return f64::from_bits(TAG_UNDEFINED); } let bytes = js_fs_readv_sync(fd_value, buffers_value, position_value); if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, buffers_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffers_value); } f64::from_bits(TAG_UNDEFINED) } @@ -1203,12 +1231,12 @@ pub extern "C" fn js_fs_writev_callback( crate::fs::validate::validate_fd(fd_value); let cb = required_callback(callback); if let Some(err_val) = crate::fs::validate::fd_open_callback_error(fd_value, "write") { - defer_fs_callback(cb, &[err_val, 0.0, buffers_value]); + crate::closure::js_closure_call3(cb, err_val, 0.0, buffers_value); return f64::from_bits(TAG_UNDEFINED); } let bytes = js_fs_writev_sync(fd_value, buffers_value, position_value); if !cb.is_null() { - defer_fs_callback(cb, &[f64::from_bits(TAG_NULL), bytes, buffers_value]); + crate::closure::js_closure_call3(cb, f64::from_bits(TAG_NULL), bytes, buffers_value); } f64::from_bits(TAG_UNDEFINED) } 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..63abfd9668 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -60,7 +60,6 @@ struct FsWatchState { listeners: HashMap>, signal: f64, abort_listener: f64, - async_ids: crate::async_hooks::AsyncResourceIds, } #[derive(Clone, PartialEq)] @@ -590,7 +589,6 @@ fn close_fs_watcher(id: usize) { return; }; crate::timer::clearInterval(state.timer_id); - crate::async_hooks::destroy(state.async_ids.async_id); remove_abort_listener(state.signal, state.abort_listener); let close_listeners = take_event_listeners(&mut state.listeners, "close"); for listener in close_listeners { @@ -1463,7 +1461,6 @@ pub extern "C" fn js_fs_watch(path_value: f64, arg1: f64, arg2: f64) -> f64 { }; let id = next_watch_id(); let object_value = build_fs_watcher_object(id); - let async_ids = crate::async_hooks::init_resource("FSEVENTWRAP", object_value, true); let timer_callback = poll_closure_value(fs_watcher_poll_impl as *const u8, id); let timer_id = crate::timer::setInterval(timer_callback as i64, FS_WATCH_POLL_INTERVAL_MS); if !persistent { @@ -1490,7 +1487,6 @@ pub extern "C" fn js_fs_watch(path_value: f64, arg1: f64, arg2: f64) -> f64 { listeners, signal: signal_value, abort_listener, - async_ids, }, ); }); @@ -1530,7 +1526,6 @@ 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 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") }; @@ -1688,12 +1683,8 @@ pub(crate) fn scan_fs_watcher_roots_mut(visitor: &mut crate::gc::RuntimeRootVisi } pub(crate) fn promise_value_fs(value: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let value = scope.root_nanbox_f64(value); - crate::async_hooks::run_provider_completion("FSREQPROMISE", || { - let promise = crate::promise::js_promise_resolved(value.get_nanbox_f64()); - f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) - }) + let promise = crate::promise::js_promise_resolved(value); + f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) } pub(crate) fn promise_undefined_fs() -> f64 { @@ -1701,11 +1692,7 @@ pub(crate) fn promise_undefined_fs() -> f64 { } pub(crate) fn promise_rejected_fs(reason: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let reason = scope.root_nanbox_f64(reason); - crate::async_hooks::run_provider_completion("FSREQPROMISE", || { - let promise = crate::promise::js_promise_new(); - crate::promise::js_promise_reject(promise, reason.get_nanbox_f64()); - f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) - }) + let promise = crate::promise::js_promise_new(); + crate::promise::js_promise_reject(promise, reason); + f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) } diff --git a/crates/perry-runtime/src/fs/filehandle.rs b/crates/perry-runtime/src/fs/filehandle.rs index be05274c27..bd0a81d858 100644 --- a/crates/perry-runtime/src/fs/filehandle.rs +++ b/crates/perry-runtime/src/fs/filehandle.rs @@ -1050,22 +1050,14 @@ pub(crate) extern "C" fn filehandle_writer_impl( self_value } -fn filehandle_close_promise() -> f64 { - crate::async_hooks::run_provider_completion("FILEHANDLECLOSEREQ", || { - let promise = - crate::promise::js_promise_resolved(f64::from_bits(crate::value::TAG_UNDEFINED)); - f64::from_bits(crate::value::JSValue::pointer(promise as *const u8).bits()) - }) -} - pub(crate) extern "C" fn filehandle_close_impl(closure: *const ClosureHeader) -> f64 { let fd = filehandle_fd(closure); if let Some(handle) = filehandle_object(closure) { close_filehandle_fd(filehandle_field_fd(handle).unwrap_or(fd), handle); - return filehandle_close_promise(); + return promise_undefined_fs(); } let _ = js_fs_close_sync(fd as f64); - filehandle_close_promise() + promise_undefined_fs() } pub(crate) extern "C" fn filehandle_sync_impl(closure: *const ClosureHeader) -> f64 { diff --git a/crates/perry-runtime/src/gc/roots.rs b/crates/perry-runtime/src/gc/roots.rs index bef4506c5c..5f9be0112f 100644 --- a/crates/perry-runtime/src/gc/roots.rs +++ b/crates/perry-runtime/src/gc/roots.rs @@ -1386,26 +1386,6 @@ impl<'a> RuntimeRootVisitor<'a> { } } - /// Visit a metadata-only NaN-boxed heap pointer. - /// - /// Unlike `visit_nanbox_f64_slot`, this repairs an address only during a - /// rewrite pass and never marks the referent. It is used by weak identity - /// side tables whose entries are pruned by the referent's finalizer. - pub fn visit_metadata_nanbox_f64_slot(&mut self, slot: &mut f64) -> bool { - let bits = slot.to_bits(); - let tag = bits & TAG_MASK; - if !matches!(tag, POINTER_TAG | STRING_TAG | BIGINT_TAG) { - return false; - } - let addr = (bits & POINTER_MASK) as usize; - if let Some(new_addr) = self.visit_metadata_raw_addr(addr) { - *slot = f64::from_bits(tag | (new_addr as u64 & POINTER_MASK)); - true - } else { - false - } - } - /// Visit a raw metadata-only `usize` slot address. /// /// # Safety diff --git a/crates/perry-runtime/src/module_require.rs b/crates/perry-runtime/src/module_require.rs index 015ccea408..eab17d5baf 100644 --- a/crates/perry-runtime/src/module_require.rs +++ b/crates/perry-runtime/src/module_require.rs @@ -1213,11 +1213,6 @@ fn dynamic_import_fallback_promise(spec: f64, deferred_note: Option) -> let promise = crate::promise::js_promise_resolved(ns_handle.get_nanbox_f64()); return js_nanbox_pointer(promise as i64); } - #[cfg(feature = "dyn-eval")] - if let Some(namespace) = dynamic_import_javascript_data_url(&spec_str) { - let promise = crate::promise::js_promise_resolved(namespace); - return js_nanbox_pointer(promise as i64); - } let message = deferred_note.unwrap_or_else(|| format!("Cannot find module '{spec_str}'")); let msg_ptr = js_string_from_bytes(message.as_ptr(), message.len() as u32); crate::node_submodules::register_error_code_pub(msg_ptr, "ERR_MODULE_NOT_FOUND"); @@ -1228,49 +1223,6 @@ fn dynamic_import_fallback_promise(spec: f64, deferred_note: Option) -> js_nanbox_pointer(promise as i64) } -#[cfg(feature = "dyn-eval")] -fn dynamic_import_javascript_data_url(specifier: &str) -> Option { - let encoded = specifier.strip_prefix("data:text/javascript,")?; - let mut decoded = Vec::with_capacity(encoded.len()); - let bytes = encoded.as_bytes(); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'%' && index + 2 < bytes.len() { - let hi = (bytes[index + 1] as char).to_digit(16)?; - let lo = (bytes[index + 2] as char).to_digit(16)?; - decoded.push(((hi << 4) | lo) as u8); - index += 3; - } else { - decoded.push(bytes[index]); - index += 1; - } - } - let source = std::str::from_utf8(&decoded).ok()?.trim(); - let declaration = source.strip_prefix("export const ")?; - let (name, expression) = declaration.split_once('=')?; - let name = name.trim(); - if name.is_empty() - || !name.chars().enumerate().all(|(index, ch)| { - ch == '_' - || ch == '$' - || (index == 0 && ch.is_ascii_alphabetic()) - || (index > 0 && ch.is_ascii_alphanumeric()) - }) - { - return None; - } - let expression = expression - .trim() - .strip_suffix(';') - .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)) -} - /// Codegen entry for the unresolved / no-match dynamic-`import()` fallthrough /// arms (#6660). Returns a NaN-boxed promise; never throws synchronously /// (`import()` always rejects, per spec). diff --git a/crates/perry-runtime/src/node_stream.rs b/crates/perry-runtime/src/node_stream.rs index 93ee525797..df673b81a8 100644 --- a/crates/perry-runtime/src/node_stream.rs +++ b/crates/perry-runtime/src/node_stream.rs @@ -99,7 +99,6 @@ const STREAM_END_EMITTED_KEY: &[u8] = b"__perryStreamEndEmitted"; const STREAM_ENDED_KEY: &[u8] = b"__perryStreamEnded"; const STREAM_MAX_LISTENERS_KEY: &[u8] = b"__perryStreamMaxListeners"; const STREAM_CAPTURE_REJECTIONS_KEY: &[u8] = b"__perryStreamCaptureRejections"; -const EVENT_EMITTER_ASYNC_RESOURCE_KEY: &[u8] = b"__perryEventEmitterAsyncResource"; const WRITABLE_WRITE_KEY: &[u8] = b"__perryWritableWrite"; const WRITABLE_FINISH_SCHEDULED_KEY: &[u8] = b"__perryWritableFinishScheduled"; const WRITABLE_FINISH_EMITTED_KEY: &[u8] = b"__perryWritableFinishEmitted"; diff --git a/crates/perry-runtime/src/node_stream_constructors.rs b/crates/perry-runtime/src/node_stream_constructors.rs index 5476ca631f..f6101e1901 100644 --- a/crates/perry-runtime/src/node_stream_constructors.rs +++ b/crates/perry-runtime/src/node_stream_constructors.rs @@ -360,13 +360,12 @@ mod pipeline; mod web_adapter; pub use builders::{ - js_array_subclass_init, js_event_emitter_async_resource_subclass_init, - js_event_emitter_subclass_init, js_node_stream_duplex_new, js_node_stream_duplex_subclass_init, - js_node_stream_passthrough_new, js_node_stream_readable_from, - js_node_stream_readable_from_options, js_node_stream_readable_new, - js_node_stream_readable_subclass_init, js_node_stream_transform_new, - js_node_stream_transform_subclass_init, js_node_stream_writable_new, - js_node_stream_writable_subclass_init, + js_array_subclass_init, js_event_emitter_subclass_init, js_node_stream_duplex_new, + js_node_stream_duplex_subclass_init, js_node_stream_passthrough_new, + js_node_stream_readable_from, js_node_stream_readable_from_options, + js_node_stream_readable_new, js_node_stream_readable_subclass_init, + js_node_stream_transform_new, js_node_stream_transform_subclass_init, + js_node_stream_writable_new, js_node_stream_writable_subclass_init, }; pub use introspection::{ diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index 6f8588d421..23797f42b4 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -94,69 +94,6 @@ pub extern "C" fn js_event_emitter_subclass_init(this: f64) -> f64 { this } -/// Initialize a source-compiled subclass of EventEmitterAsyncResource on its -/// already-allocated `this` object. The listener surface remains the generic -/// object-backed EventEmitter implementation; a hidden AsyncResource supplies -/// the execution scope, lifecycle, ids, and `asyncResource.eventEmitter` -/// back-reference. -#[no_mangle] -pub extern "C" fn js_event_emitter_async_resource_subclass_init(this: f64, options: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let this_handle = scope.root_nanbox_f64(this); - let options_handle = scope.root_nanbox_f64(options); - js_event_emitter_subclass_init(this_handle.get_nanbox_f64()); - - let this = this_handle.get_nanbox_f64(); - let raw = raw_ptr_from_value(this); - if raw == 0 || unsafe { gc_type_for_ptr(raw) } != Some(crate::gc::GC_TYPE_OBJECT) { - return this; - } - let options = options_handle.get_nanbox_f64(); - let options_value = JSValue::from_bits(options.to_bits()); - let mut name = if options_value.is_any_string() { - options - } else { - let key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); - let options = options_handle.get_nanbox_f64(); - let options_obj = raw_ptr_from_value(options) as *const ObjectHeader; - if options_obj.is_null() { - f64::from_bits(crate::value::TAG_UNDEFINED) - } else { - crate::object::js_object_get_field_by_name_f64(options_obj, key) - } - }; - if JSValue::from_bits(name.to_bits()).is_undefined() { - let current_obj = raw_ptr_from_value(this_handle.get_nanbox_f64()) as *mut ObjectHeader; - let class_id = unsafe { (*current_obj).class_id }; - let default_name = crate::object::class_name_for_id(class_id) - .unwrap_or_else(|| "EventEmitterAsyncResource".to_string()); - let name_ptr = - 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 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 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); - unsafe { - crate::object::js_object_set_field_by_name( - raw as *mut ObjectHeader, - hidden_key(EVENT_EMITTER_ASYNC_RESOURCE_KEY), - f64::from_bits(crate::value::js_nanbox_pointer(resource).to_bits()), - ); - install_event_emitter_async_resource_instance_methods( - raw as *mut ObjectHeader, - this_handle.get_nanbox_f64(), - ); - } - this_handle.get_nanbox_f64() -} - /// `super(n)` for a source-compiled `class X extends Array` (e.g. lru-cache's /// `ZeroArray`: `class ZeroArray extends Array { constructor(n){ super(n); /// this.fill(0) } }`). Perry models the subclass instance as a plain object, diff --git a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs index f8b370a622..893064dc8a 100644 --- a/crates/perry-runtime/src/node_stream_constructors/pipeline.rs +++ b/crates/perry-runtime/src/node_stream_constructors/pipeline.rs @@ -221,12 +221,6 @@ pub extern "C" fn js_node_stream_finished(args: *const crate::array::ArrayHeader .is_some_and(|v| v.to_bits() == TAG_FALSE); if watch_close || watch_finish { add_finished_once_listeners(stream, callback, watch_finish, watch_close); - } else { - // The default callback form observes normal stream completion too. - // A shared once-guard makes the first terminal event sufficient for - // this lightweight stream model and prevents the duplex sequence - // (`finish`, `end`, `close`) from invoking the callback repeatedly. - add_finished_cleanup_completion_listener(stream, callback); } if let Some(signal) = options_signal(options) { add_finished_signal_abort_listener(stream, signal, callback); diff --git a/crates/perry-runtime/src/node_stream_dispatch.rs b/crates/perry-runtime/src/node_stream_dispatch.rs index 913fcb8e9e..d846a52dd5 100644 --- a/crates/perry-runtime/src/node_stream_dispatch.rs +++ b/crates/perry-runtime/src/node_stream_dispatch.rs @@ -257,201 +257,6 @@ pub(crate) fn install_event_emitter_prototype_methods(proto: *mut ObjectHeader) } } -enum EventEmitterAsyncResourceBacking { - ExternalEmitter(i64), - RuntimeResource(i64), -} - -fn event_emitter_async_resource_backing(receiver: f64) -> Option { - let bits = receiver.to_bits(); - if bits >> 48 == 0x7FFD { - let handle = (bits & crate::value::POINTER_MASK) as i64; - if crate::object::event_emitter_async_resource_handle_probe() - .is_some_and(|probe| unsafe { probe(handle) }) - { - return Some(EventEmitterAsyncResourceBacking::ExternalEmitter(handle)); - } - let raw = handle as usize; - if crate::value::addr_class::is_plausible_heap_addr(raw) { - let key = hidden_key(EVENT_EMITTER_ASYNC_RESOURCE_KEY); - let value = 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) { - return Some(EventEmitterAsyncResourceBacking::RuntimeResource(resource)); - } - } - } - } - None -} - -fn require_event_emitter_async_resource_receiver( - closure: *const ClosureHeader, -) -> EventEmitterAsyncResourceBacking { - if let Some(backing) = event_emitter_async_resource_backing(this_value(closure)) { - return backing; - } - crate::node_submodules::diagnostics::throw_type_error_no_code( - b"Cannot read private member from an object whose class did not declare it", - ) -} - -extern "C" fn ns_ee_async_resource_emit_rest( - closure: *const ClosureHeader, - event: f64, - rest: f64, -) -> f64 { - let backing = require_event_emitter_async_resource_receiver(closure); - let runtime_async_id = match backing { - EventEmitterAsyncResourceBacking::RuntimeResource(resource) => { - crate::async_hooks::js_async_resource_async_id(resource) as u64 - } - EventEmitterAsyncResourceBacking::ExternalEmitter(_) => 0, - }; - if runtime_async_id != 0 { - crate::async_hooks::js_async_hooks_provider_enter(runtime_async_id); - } - let result = ns_emit_rest(closure, event, rest); - if runtime_async_id != 0 { - crate::async_hooks::js_async_hooks_provider_leave(runtime_async_id); - } - result -} - -extern "C" fn ns_ee_async_resource_destroy(closure: *const ClosureHeader) -> f64 { - match require_event_emitter_async_resource_receiver(closure) { - EventEmitterAsyncResourceBacking::ExternalEmitter(handle) => { - crate::object::event_emitter_async_resource_dispatch() - .map(|dispatch| unsafe { dispatch(handle, 3) }) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) - } - EventEmitterAsyncResourceBacking::RuntimeResource(resource) => { - crate::async_hooks::js_async_resource_emit_destroy(resource) as f64 - } - } -} - -extern "C" fn ns_ee_async_resource_getter(closure: *const ClosureHeader) -> f64 { - let operation = crate::closure::js_closure_get_capture_ptr(closure, 1) as u32; - match require_event_emitter_async_resource_receiver(closure) { - EventEmitterAsyncResourceBacking::ExternalEmitter(handle) => { - crate::object::event_emitter_async_resource_dispatch() - .map(|dispatch| unsafe { dispatch(handle, operation) }) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) - } - EventEmitterAsyncResourceBacking::RuntimeResource(resource) => match operation { - 0 => crate::async_hooks::js_async_resource_async_id(resource), - 1 => crate::async_hooks::js_async_resource_trigger_async_id(resource), - 2 => f64::from_bits(crate::value::js_nanbox_pointer(resource).to_bits()), - _ => f64::from_bits(crate::value::TAG_UNDEFINED), - }, - } -} - -/// Complete the `EventEmitterAsyncResource.prototype` surface. Its inherited -/// 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) { - 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); - - 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), - ); - }; - install_method("emit", ns_ee_async_resource_emit_rest as *const u8); - install_method("emitDestroy", ns_ee_async_resource_destroy as *const u8); - - for (name, operation) in [ - ("asyncId", 0_i64), - ("triggerAsyncId", 1), - ("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, 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), - ); - } -} - -pub(crate) unsafe fn install_event_emitter_async_resource_instance_methods( - obj: *mut ObjectHeader, - this_value: f64, -) { - 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); - for (name, function) in [ - ("emit", ns_ee_async_resource_emit_rest as *const u8), - ("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()), - ); - } - - // Source-compiled subclasses are plain runtime objects rather than - // external emitter handles. Bind the resource accessors directly to the - // instance just like emit/emitDestroy; relying only on the native parent - // prototype leaves dynamic loop/property reads as `undefined`. - for (name, operation) in [ - ("asyncId", 0_i64), - ("triggerAsyncId", 1), - ("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), - ); - } -} - -#[no_mangle] -pub extern "C" fn js_event_emitter_async_resource_subclass_backing(receiver: i64) -> i64 { - let receiver = - f64::from_bits(crate::value::POINTER_TAG | (receiver as u64 & crate::value::POINTER_MASK)); - match event_emitter_async_resource_backing(receiver) { - Some(EventEmitterAsyncResourceBacking::RuntimeResource(resource)) => resource, - _ => 0, - } -} - pub(super) fn register_stub_arities() { let register = |func: *const u8, arity: u32| { crate::closure::js_register_closure_arity(func, arity); diff --git a/crates/perry-runtime/src/node_submodules/blob.rs b/crates/perry-runtime/src/node_submodules/blob.rs index 1558b90783..feb8ca5e0f 100644 --- a/crates/perry-runtime/src/node_submodules/blob.rs +++ b/crates/perry-runtime/src/node_submodules/blob.rs @@ -72,35 +72,19 @@ thread_local! { static NEXT_FILE_BLOB_STREAM_ID: RefCell = const { RefCell::new(1) }; } -fn blob_reader_promise_value(value: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let value = scope.root_nanbox_f64(value); - crate::async_hooks::run_provider_completion("BLOBREADER", || { - promise_value(value.get_nanbox_f64()) - }) -} - -fn blob_reader_promise_rejected(reason: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let reason = scope.root_nanbox_f64(reason); - crate::async_hooks::run_provider_completion("BLOBREADER", || { - promise_rejected(reason.get_nanbox_f64()) - }) -} - extern "C" fn blob_text_method(closure: *const ClosureHeader) -> f64 { let bytes = captured_blob_bytes(closure); - blob_reader_promise_value(bytes_to_text_value(&bytes)) + promise_value(bytes_to_text_value(&bytes)) } extern "C" fn blob_array_buffer_method(closure: *const ClosureHeader) -> f64 { let bytes = captured_blob_bytes(closure); - blob_reader_promise_value(bytes_to_array_buffer_value(&bytes)) + promise_value(bytes_to_array_buffer_value(&bytes)) } extern "C" fn blob_bytes_method(closure: *const ClosureHeader) -> f64 { let bytes = captured_blob_bytes(closure); - blob_reader_promise_value(bytes_to_uint8_array_value(&bytes)) + promise_value(bytes_to_uint8_array_value(&bytes)) } extern "C" fn blob_slice_method( @@ -136,22 +120,22 @@ extern "C" fn blob_stream_method(closure: *const ClosureHeader) -> f64 { extern "C" fn file_blob_text_method(closure: *const ClosureHeader) -> f64 { match read_file_blob_bytes(captured_file_blob_id(closure)) { - Ok(bytes) => blob_reader_promise_value(bytes_to_text_value(&bytes)), - Err(reason) => blob_reader_promise_rejected(reason), + Ok(bytes) => promise_value(bytes_to_text_value(&bytes)), + Err(reason) => promise_rejected(reason), } } extern "C" fn file_blob_array_buffer_method(closure: *const ClosureHeader) -> f64 { match read_file_blob_bytes(captured_file_blob_id(closure)) { - Ok(bytes) => blob_reader_promise_value(bytes_to_array_buffer_value(&bytes)), - Err(reason) => blob_reader_promise_rejected(reason), + Ok(bytes) => promise_value(bytes_to_array_buffer_value(&bytes)), + Err(reason) => promise_rejected(reason), } } extern "C" fn file_blob_bytes_method(closure: *const ClosureHeader) -> f64 { match read_file_blob_bytes(captured_file_blob_id(closure)) { - Ok(bytes) => blob_reader_promise_value(bytes_to_uint8_array_value(&bytes)), - Err(reason) => blob_reader_promise_rejected(reason), + Ok(bytes) => promise_value(bytes_to_uint8_array_value(&bytes)), + Err(reason) => promise_rejected(reason), } } diff --git a/crates/perry-runtime/src/node_submodules/fs_promises.rs b/crates/perry-runtime/src/node_submodules/fs_promises.rs index abf3d92f4e..17ccde3feb 100644 --- a/crates/perry-runtime/src/node_submodules/fs_promises.rs +++ b/crates/perry-runtime/src/node_submodules/fs_promises.rs @@ -51,8 +51,8 @@ fn catch_fs_promises_throw(call: impl FnOnce() -> f64) -> Result { fn promise_from_sync_value(call: impl FnOnce() -> f64) -> f64 { match catch_fs_promises_throw(call) { - Ok(value) => crate::fs::promise_value_fs(value), - Err(err) => crate::fs::promise_rejected_fs(err), + Ok(value) => promise_value(value), + Err(err) => promise_rejected(err), } } @@ -61,28 +61,28 @@ fn promise_from_sync_undefined(call: impl FnOnce()) -> f64 { call(); f64::from_bits(crate::value::TAG_UNDEFINED) }) { - Ok(_) => crate::fs::promise_undefined_fs(), - Err(err) => crate::fs::promise_rejected_fs(err), + Ok(_) => promise_undefined(), + Err(err) => promise_rejected(err), } } fn promise_from_result_undefined(call: impl FnOnce() -> Result<(), f64>) -> f64 { match catch_fs_promises_throw(|| match call() { - Ok(()) => crate::fs::promise_undefined_fs(), - Err(err_val) => crate::fs::promise_rejected_fs(err_val), + Ok(()) => promise_undefined(), + Err(err_val) => promise_rejected(err_val), }) { Ok(promise) => promise, - Err(err) => crate::fs::promise_rejected_fs(err), + Err(err) => promise_rejected(err), } } fn promise_from_result_value(call: impl FnOnce() -> Result) -> f64 { match catch_fs_promises_throw(|| match call() { - Ok(value) => crate::fs::promise_value_fs(value), - Err(err_val) => crate::fs::promise_rejected_fs(err_val), + Ok(value) => promise_value(value), + Err(err_val) => promise_rejected(err_val), }) { Ok(promise) => promise, - Err(err) => crate::fs::promise_rejected_fs(err), + Err(err) => promise_rejected(err), } } @@ -128,19 +128,12 @@ pub(crate) extern "C" fn thunk_fs_promises_open( ) -> f64 { 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); - // FILEHANDLE is owned by the public handle and remains live - // after open; Node does not emit before/after/destroy for it. - let _ = ids; - promise - } - Err(err_val) => crate::fs::promise_rejected_fs(err_val), + Ok(handle) => promise_value(handle), + Err(err_val) => promise_rejected(err_val), } }) { Ok(promise) => promise, - Err(err) => crate::fs::promise_rejected_fs(err), + Err(err) => promise_rejected(err), } } @@ -153,12 +146,12 @@ pub(crate) extern "C" fn thunk_fs_promises_writeFile( ) -> f64 { match catch_fs_promises_throw(|| { match unsafe { crate::fs::write_file_path_or_fd_result(path, data, options) } { - Ok(()) => crate::fs::promise_undefined_fs(), - Err(err) => crate::fs::promise_rejected_fs(err), + Ok(()) => promise_undefined(), + Err(err) => promise_rejected(err), } }) { Ok(promise) => promise, - Err(err) => crate::fs::promise_rejected_fs(err), + Err(err) => promise_rejected(err), } } @@ -213,7 +206,7 @@ pub(crate) extern "C" fn thunk_fs_promises_lchmod( let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); crate::node_submodules::register_error_code_pub(msg, "ERR_METHOD_NOT_IMPLEMENTED"); let err = crate::error::js_error_new_with_message(msg); - return crate::fs::promise_rejected_fs(crate::value::js_nanbox_pointer(err as i64)); + return promise_rejected(crate::value::js_nanbox_pointer(err as i64)); } promise_from_result_undefined(|| unsafe { crate::fs::js_fs_lchmod_result(path, mode) }) } @@ -397,13 +390,7 @@ pub(crate) extern "C" fn thunk_fs_promises_opendir( _closure: *const ClosureHeader, path: f64, ) -> 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) - } - Err(err) => crate::fs::promise_rejected_fs(err), - } + promise_from_result_value(|| crate::fs::js_fs_opendir_value_with_path(path)) } pub(crate) extern "C" fn thunk_fs_promises_glob( diff --git a/crates/perry-runtime/src/node_vm.rs b/crates/perry-runtime/src/node_vm.rs index 5428e268aa..d6bf6490c5 100644 --- a/crates/perry-runtime/src/node_vm.rs +++ b/crates/perry-runtime/src/node_vm.rs @@ -1263,14 +1263,6 @@ fn execute_in_state(source: &str, state: &ContextState) -> f64 { result } -/// Evaluate the expression payload of a runtime-loaded JavaScript data URL in -/// the main realm. Dynamic import lives outside this module, so keep the -/// interpreter entry narrow instead of exposing VM context internals. -#[cfg(feature = "dyn-eval")] -pub(crate) fn eval_dynamic_module_expression(source: &str) -> f64 { - execute_in_state(source, &main_context_state()) -} - fn script_metadata(script_value: f64) -> Option { object_ptr_from_value(script_value) .and_then(|ptr| scripts().lock().unwrap().get(&(ptr as usize)).cloned()) diff --git a/crates/perry-runtime/src/object/class_handles.rs b/crates/perry-runtime/src/object/class_handles.rs index e8f6cd32cc..9df63256b1 100644 --- a/crates/perry-runtime/src/object/class_handles.rs +++ b/crates/perry-runtime/src/object/class_handles.rs @@ -100,8 +100,6 @@ pub type FetchHandleKindProbeFn = unsafe extern "C" fn(id: usize) -> u8; /// as heap objects. pub type EventEmitterHandleProbeFn = unsafe extern "C" fn(handle: i64) -> bool; pub type EventEmitterAsyncResourceHandleProbeFn = unsafe extern "C" fn(handle: i64) -> bool; -pub type EventEmitterAsyncResourceDispatchFn = - unsafe extern "C" fn(handle: i64, operation: u32) -> f64; pub type EventEmitterGetDomainFn = unsafe extern "C" fn(handle: i64) -> i64; pub type EventEmitterSetDomainFn = unsafe extern "C" fn(handle: i64, domain: i64) -> i32; @@ -163,7 +161,6 @@ static FETCH_HANDLE_KIND_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut static EVENT_EMITTER_HANDLE_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static EVENT_EMITTER_ASYNC_RESOURCE_HANDLE_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); -static EVENT_EMITTER_ASYNC_RESOURCE_DISPATCH_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static EVENT_EMITTER_GET_DOMAIN_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static EVENT_EMITTER_SET_DOMAIN_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); static NET_SOCKET_HANDLE_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); @@ -486,23 +483,6 @@ pub unsafe extern "C" fn js_register_event_emitter_async_resource_handle_probe( EVENT_EMITTER_ASYNC_RESOURCE_HANDLE_PROBE_PTR.store(f as *mut (), Ordering::Release); } -#[inline] -pub fn event_emitter_async_resource_dispatch() -> Option { - let p = EVENT_EMITTER_ASYNC_RESOURCE_DISPATCH_PTR.load(Ordering::Acquire); - if p.is_null() { - None - } else { - Some(unsafe { std::mem::transmute::<*mut (), EventEmitterAsyncResourceDispatchFn>(p) }) - } -} - -#[no_mangle] -pub unsafe extern "C" fn js_register_event_emitter_async_resource_dispatch( - f: EventEmitterAsyncResourceDispatchFn, -) { - EVENT_EMITTER_ASYNC_RESOURCE_DISPATCH_PTR.store(f as *mut (), Ordering::Release); -} - #[inline] pub fn event_emitter_get_domain() -> Option { let p = EVENT_EMITTER_GET_DOMAIN_PTR.load(Ordering::Acquire); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 27e330209c..d7ae50b51f 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -17,28 +17,25 @@ //! `pub use class_registry::*` glob in `object/mod.rs`). Pure relocation. pub use super::class_handles::{ - event_emitter_async_resource_dispatch, event_emitter_async_resource_handle_probe, - event_emitter_get_domain, event_emitter_handle_probe, event_emitter_on, - event_emitter_set_domain, fetch_handle_kind_probe, handle_method_dispatch, - handle_own_property_names_dispatch, handle_property_dispatch, handle_property_set_dispatch, - handle_prototype_dispatch, http_agent_handle_probe, - js_register_event_emitter_async_resource_dispatch, - js_register_event_emitter_async_resource_handle_probe, js_register_event_emitter_get_domain, - js_register_event_emitter_handle_probe, js_register_event_emitter_on, - js_register_event_emitter_set_domain, js_register_fetch_handle_kind_probe, - js_register_handle_method_dispatch, js_register_handle_own_property_names_dispatch, - js_register_handle_property_dispatch, js_register_handle_property_set_dispatch, - js_register_handle_prototype_dispatch, js_register_http_agent_handle_probe, - js_register_net_socket_handle_probe, js_register_stream_expando_set, - js_register_stream_handle_kind_probe, js_register_stream_handle_probe, - js_register_tls_handle_kind_probe, net_socket_handle_probe, stream_expando_set, - stream_handle_kind_probe, stream_handle_probe, tls_handle_kind_probe, - EventEmitterAsyncResourceDispatchFn, EventEmitterAsyncResourceHandleProbeFn, - EventEmitterGetDomainFn, EventEmitterHandleProbeFn, EventEmitterOnFn, EventEmitterSetDomainFn, - FetchHandleKindProbeFn, HandleMethodDispatchFn, HandleOwnPropertyNamesDispatchFn, - HandlePropertyDispatchFn, HandlePropertySetDispatchFn, HandlePrototypeDispatchFn, - HttpAgentHandleProbeFn, NetSocketHandleProbeFn, StreamHandleKindProbeFn, StreamHandleProbeFn, - TlsHandleKindProbeFn, + event_emitter_async_resource_handle_probe, event_emitter_get_domain, + event_emitter_handle_probe, event_emitter_on, event_emitter_set_domain, + fetch_handle_kind_probe, handle_method_dispatch, handle_own_property_names_dispatch, + handle_property_dispatch, handle_property_set_dispatch, handle_prototype_dispatch, + http_agent_handle_probe, js_register_event_emitter_async_resource_handle_probe, + js_register_event_emitter_get_domain, js_register_event_emitter_handle_probe, + js_register_event_emitter_on, js_register_event_emitter_set_domain, + js_register_fetch_handle_kind_probe, js_register_handle_method_dispatch, + js_register_handle_own_property_names_dispatch, js_register_handle_property_dispatch, + js_register_handle_property_set_dispatch, js_register_handle_prototype_dispatch, + js_register_http_agent_handle_probe, js_register_net_socket_handle_probe, + js_register_stream_expando_set, js_register_stream_handle_kind_probe, + js_register_stream_handle_probe, js_register_tls_handle_kind_probe, net_socket_handle_probe, + stream_expando_set, stream_handle_kind_probe, stream_handle_probe, tls_handle_kind_probe, + EventEmitterAsyncResourceHandleProbeFn, EventEmitterGetDomainFn, EventEmitterHandleProbeFn, + EventEmitterOnFn, EventEmitterSetDomainFn, FetchHandleKindProbeFn, HandleMethodDispatchFn, + HandleOwnPropertyNamesDispatchFn, HandlePropertyDispatchFn, HandlePropertySetDispatchFn, + HandlePrototypeDispatchFn, HttpAgentHandleProbeFn, NetSocketHandleProbeFn, + StreamHandleKindProbeFn, StreamHandleProbeFn, TlsHandleKindProbeFn, }; use super::*; diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index f48d3aa48c..ee46eaf666 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -125,30 +125,11 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, mut parent_val if !super::super::native_module::is_native_module_constructor_export(&module, &method) { throw_object_type_error(b"Class extends value is not a constructor"); } - let module = super::super::native_module::normalize_native_module_alias(&module); - if module == "wasi" && method == "WASI" { + if super::super::native_module::normalize_native_module_alias(&module) == "wasi" + && method == "WASI" + { register_class(class_id, crate::wasi::CLASS_ID_WASI); } - if module == "async_hooks" { - let parent = match method.as_str() { - "AsyncLocalStorage" => 0xFFFF0078, - "AsyncResource" => 0xFFFF0079, - _ => 0, - }; - if parent != 0 { - register_class(class_id, parent); - } - } - if module == "events" { - let parent = match method.as_str() { - "EventEmitter" => 0xFFFF0076, - "EventEmitterAsyncResource" => 0xFFFF0077, - _ => 0, - }; - if parent != 0 { - register_class(class_id, parent); - } - } return; } // Spec: a non-`null` superclass that is not a constructor throws a TypeError @@ -1778,10 +1759,6 @@ pub fn method_owner_class_id(class_id: u32, name: &str) -> Option { None } -#[cfg(test)] -#[path = "parent_static/unstamped_tests.rs"] -mod unstamped_tests; - #[cfg(test)] mod shape_authority_tests_8067 { fn key<'scope>( @@ -1802,6 +1779,70 @@ mod shape_authority_tests_8067 { super::js_object_mark_class(1); } + /// #8113 replaces #8067's "saved lineage beats an interim self-heal" test. + /// + /// The self-heal it modelled is GONE: `typed_feedback::object_shape` used to + /// mint a lineage-free descriptor for an unstamped receiver, which under + /// #8113 would also publish a live inline-slot bound of ZERO — a read-only + /// observation path silently truncating the object's payload. The property + /// worth pinning is now the stronger one: an unstamped receiver MISSES, and + /// observing it publishes nothing at all. + /// + /// The clear here is manufactured with a test-only helper. No production + /// path clears a stamp any more (`shapes::clear_object_shape_stamp` is + /// `#[cfg(test)]`), which is what makes the window this used to model + /// unreachable rather than merely narrow. + #[test] + fn an_unstamped_receiver_misses_instead_of_being_self_healed() { + let _lock = crate::gc::global_side_table_test_lock(); + unsafe { + const CID: u32 = 0x8068; + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(crate::object::js_object_alloc(CID, 1)); + let ((), obj) = obj_handle.across_mut::(|| { + obj_handle.with_mut_ptr::(|obj| { + super::js_object_mark_class(obj as i64) + }) + }); + let predecessor = crate::object::shapes::object_shape_descriptor(obj) + .expect("marked class descriptor"); + assert_eq!( + predecessor.object_kind, + crate::object::shapes::ShapeObjectKind::Class + ); + assert_eq!(predecessor.live_inline_slot_count, 1); + + assert!(crate::object::shapes::clear_object_shape_stamp(obj)); + let (interim, obj) = obj_handle.across_mut::(|| { + crate::typed_feedback::test_object_shape_token(obj as usize) + }); + assert_eq!( + interim, 0, + "an unstamped receiver must MISS; minting a lineage-free \ + descriptor for it would publish a zero live-slot bound" + ); + assert!( + crate::object::shapes::object_shape_descriptor(obj).is_none(), + "observing an unstamped receiver must not publish a descriptor" + ); + + // The mutator's saved lineage still restores both facts exactly. + crate::object::shapes::synchronize_object_shape_descriptor_from( + obj, + Some(predecessor), + predecessor.live_inline_slot_count, + ); + let restored = + crate::object::shapes::object_shape_descriptor(obj).expect("restored descriptor"); + assert_eq!( + restored.object_kind, + crate::object::shapes::ShapeObjectKind::Class, + "the mutator's saved semantic lineage must survive the window" + ); + assert_eq!(restored.live_inline_slot_count, 1); + } + } + #[test] fn class_kind_survives_static_field_installation_and_deletion() { let _lock = crate::gc::global_side_table_test_lock(); diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/unstamped_tests.rs b/crates/perry-runtime/src/object/class_registry/parent_static/unstamped_tests.rs deleted file mode 100644 index a0ba95e0a9..0000000000 --- a/crates/perry-runtime/src/object/class_registry/parent_static/unstamped_tests.rs +++ /dev/null @@ -1,46 +0,0 @@ -/// #8113 replaces #8067's "saved lineage beats an interim self-heal" test. -/// An unstamped receiver must miss without publishing a lineage-free shape. -#[test] -fn an_unstamped_receiver_misses_instead_of_being_self_healed() { - let _lock = crate::gc::global_side_table_test_lock(); - unsafe { - const CID: u32 = 0x8068; - let scope = crate::gc::RuntimeHandleScope::new(); - let obj_handle = scope.root_raw_mut_ptr(crate::object::js_object_alloc(CID, 1)); - let ((), obj) = obj_handle.across_mut::(|| { - obj_handle.with_mut_ptr::(|obj| { - super::js_object_mark_class(obj as i64) - }) - }); - let predecessor = - crate::object::shapes::object_shape_descriptor(obj).expect("marked class descriptor"); - assert_eq!( - predecessor.object_kind, - crate::object::shapes::ShapeObjectKind::Class - ); - assert_eq!(predecessor.live_inline_slot_count, 1); - - assert!(crate::object::shapes::clear_object_shape_stamp(obj)); - let (interim, obj) = obj_handle.across_mut::(|| { - crate::typed_feedback::test_object_shape_token(obj as usize) - }); - assert_eq!( - interim, 0, - "an unstamped receiver must miss instead of publishing a zero-slot shape" - ); - assert!(crate::object::shapes::object_shape_descriptor(obj).is_none()); - - crate::object::shapes::synchronize_object_shape_descriptor_from( - obj, - Some(predecessor), - predecessor.live_inline_slot_count, - ); - let restored = - crate::object::shapes::object_shape_descriptor(obj).expect("restored descriptor"); - assert_eq!( - restored.object_kind, - crate::object::shapes::ShapeObjectKind::Class - ); - assert_eq!(restored.live_inline_slot_count, 1); - } -} diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 3fe6bc5fd1..2076a32439 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -620,10 +620,7 @@ pub(crate) unsafe fn nm_ee_prototype_install( ) { proto.with_mut_ptr::(|proto| { - crate::node_stream::install_event_emitter_prototype_methods(proto); - if method == "EventEmitterAsyncResource" { - crate::node_stream::install_event_emitter_async_resource_prototype(proto); - } + crate::node_stream::install_event_emitter_prototype_methods(proto) }); } } diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index a0d0689350..693c614547 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -1901,7 +1901,7 @@ pub(crate) unsafe fn nm_get_own_descriptor( .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); return Some(build_data_descriptor(value, false, true, false)); } - if matches!(module_name.as_str(), "module" | "async_hooks") { + if module_name == "module" { return Some(build_data_descriptor( f64::from_bits(value.bits()), true, diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 89c5a87662..64ae285ea0 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -204,7 +204,6 @@ pub(crate) mod enumeration; mod field_ops; mod for_in_stable; mod get_field_by_name; -mod get_field_by_name_async; #[cfg(test)] mod get_field_by_name_probe_tests; mod get_field_by_name_tail; @@ -270,7 +269,6 @@ pub use field_ops::{ }; pub use for_in_stable::js_for_in_keys_stable_value; pub use get_field_by_name::js_object_get_field_by_name; -pub(crate) use get_field_by_name_async::async_resource_property; pub(crate) use get_field_by_name_tail::get_field_by_name_object_tail; pub(super) use has_property::native_module_own_field_by_key; pub(crate) use has_property::{ diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 1c10f88b50..475a40f87b 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -539,7 +539,7 @@ pub extern "C" fn js_object_get_field_by_name( { if !key.is_null() { unsafe { - let key_ptr = crate::string::string_data(key); + let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); let ta = addr as *const crate::typedarray::TypedArrayHeader; @@ -869,20 +869,6 @@ pub extern "C" fn js_object_get_field_by_name( } else { 0 }; - if raw != 0 && !key.is_null() { - unsafe { - let key_ptr = (key as *const u8).add(std::mem::size_of::()); - let key_len = (*key).byte_len as usize; - if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) - { - if let Some(value) = - crate::async_hooks::try_async_resource_property_dispatch(raw as i64, name) - { - return JSValue::from_bits(value.to_bits()); - } - } - } - } if crate::value::addr_class::is_small_handle(raw) { if !key.is_null() { unsafe { @@ -890,11 +876,6 @@ pub extern "C" fn js_object_get_field_by_name( (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"constructor" { - if let Some(value) = crate::timer::timer_constructor_value(raw as i64) { - return JSValue::from_bits(value.to_bits()); - } - } if let Some(method) = timer_handle_method_name_static(key_bytes) { if crate::timer::is_known_timer_id(raw as i64) { let this_f64 = f64::from_bits( diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_async.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_async.rs deleted file mode 100644 index 427f7c4832..0000000000 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_async.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; - -/// Resolve expandos on registry-backed AsyncResource handles before ordinary -/// object/handle property dispatch. Copy the key payload so hook code cannot -/// invalidate a borrowed slice by triggering a moving collection. -pub(crate) fn async_resource_property( - obj: *const ObjectHeader, - key: *const crate::StringHeader, -) -> Option { - if key.is_null() { - return None; - } - let key = unsafe { crate::string::OwnedStringBytes::copy_from_header(key) }; - let name = std::str::from_utf8(key.as_bytes()).ok()?; - crate::async_hooks::try_async_resource_property_dispatch(obj as i64, name) - .map(|value| JSValue::from_bits(value.to_bits())) -} diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 2e1cb30b9d..da053cb3aa 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -22,9 +22,6 @@ pub(crate) fn get_field_by_name_object_tail( // undefined/null tag or null pointer — return undefined return JSValue::undefined(); } - if let Some(value) = async_resource_property(raw, key) { - return value; - } // Issue #340: small-handle receivers (raw < 0x100000) come // from native modules (axios, fastify, ioredis, ...) that // store objects in registries and expose integer ids. The @@ -41,11 +38,6 @@ pub(crate) fn get_field_by_name_object_tail( (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"constructor" { - if let Some(value) = crate::timer::timer_constructor_value(raw as i64) { - return JSValue::from_bits(value.to_bits()); - } - } if let Some(method) = timer_handle_method_name_static(key_bytes) { if crate::timer::is_known_timer_id(raw as i64) { let this_f64 = f64::from_bits( @@ -115,9 +107,6 @@ pub(crate) fn get_field_by_name_object_tail( if obj.is_null() { return JSValue::undefined(); } - if let Some(value) = async_resource_property(obj, key) { - return value; - } // Same handle-receiver path for already-stripped pointers — happens // when the codegen passes a raw i64 handle through the slow path. if crate::value::addr_class::is_handle_band(obj as usize) { @@ -126,11 +115,6 @@ pub(crate) fn get_field_by_name_object_tail( let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"constructor" { - if let Some(value) = crate::timer::timer_constructor_value(obj as i64) { - return JSValue::from_bits(value.to_bits()); - } - } if let Some(method) = timer_handle_method_name_static(key_bytes) { if crate::timer::is_known_timer_id(obj as i64) { let this_f64 = diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs index f6f1d9949d..d677f3c552 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -480,19 +480,6 @@ pub extern "C" fn js_object_get_field_ic_miss( // `< 0x100000` proxy / HANDLE_PROPERTY_DISPATCH routing below — matching // the ordering in `js_object_get_field_by_name`. The macOS heap floor // (0x200_0000_0000 in is_valid_obj_ptr) masked this; Linux's is 0x1000. - if !key.is_null() { - unsafe { - let key_ptr = crate::string::string_data(key); - let key_len = (*key).byte_len as usize; - if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)) { - if let Some(value) = - crate::async_hooks::try_async_resource_property_dispatch(obj as i64, name) - { - return value; - } - } - } - } if crate::value::addr_class::is_above_handle_band(obj as usize) { // #7753: `arr.length` on a receiver codegen could not prove is an array. // @@ -572,11 +559,6 @@ pub extern "C" fn js_object_get_field_ic_miss( let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); - if key_bytes == b"constructor" { - if let Some(value) = crate::timer::timer_constructor_value(obj as i64) { - return value; - } - } if let Some(method) = timer_handle_method_name_static(key_bytes) { if crate::timer::is_known_timer_id(obj as i64) { let this_f64 = diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index b0927a44e9..b40b84375a 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -8,8 +8,6 @@ use super::*; // Keep in sync with perry-codegen/src/expr/instance_misc1.rs. const CLASS_ID_EVENT_EMITTER: u32 = 0xFFFF0076; const CLASS_ID_EVENT_EMITTER_ASYNC_RESOURCE: u32 = 0xFFFF0077; -const CLASS_ID_ASYNC_LOCAL_STORAGE: u32 = 0xFFFF0078; -const CLASS_ID_ASYNC_RESOURCE: u32 = 0xFFFF0079; const CLASS_ID_PROMISE: u32 = 0xFFFF0027; const CLASS_ID_NET_SOCKET: u32 = 0xFFFF00B4; const CLASS_ID_CRYPTO: u32 = 0xFFFF00C0; @@ -338,34 +336,6 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { { return f64::from_bits(crate::value::TAG_TRUE); } - if module == "async_hooks" - && matches!(method.as_str(), "AsyncLocalStorage" | "AsyncResource") - { - 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 - && ordinary_has_instance_prototype_walk(value, type_ref)) - } else { - let candidate = small_native_handle_id(value).unwrap_or(raw as i64); - let native = (candidate != 0) && { - super::class_handles::handle_property_dispatch().is_some_and(|dispatch| { - let property = b"getStore"; - let result = - unsafe { dispatch(candidate, property.as_ptr(), property.len()) }; - value_is_callable(result) - }) - }; - native - || (raw >= crate::value::addr_class::HANDLE_BAND_MAX - && ordinary_has_instance_prototype_walk(value, type_ref)) - }; - return f64::from_bits(if matched { - crate::value::TAG_TRUE - } else { - TAG_FALSE - }); - } if module == "tty" && matches!(method.as_str(), "ReadStream" | "WriteStream") && crate::tty::is_tty_stream_instance(value, method.as_str()) @@ -1170,26 +1140,6 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { false_val }; } - if class_id == CLASS_ID_ASYNC_RESOURCE { - return if crate::async_hooks::resolve_async_resource_handle(value_addr(value) as i64) - .is_some() - { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_ASYNC_LOCAL_STORAGE { - let candidate = small_native_handle_id(value).unwrap_or(value_addr(value) as i64); - let matched = candidate != 0 && { - super::class_handles::handle_property_dispatch().is_some_and(|dispatch| { - let property = b"getStore"; - let result = unsafe { dispatch(candidate, property.as_ptr(), property.len()) }; - value_is_callable(result) - }) - }; - return if matched { true_val } else { false_val }; - } if class_id == CLASS_ID_NET_SOCKET { return if let Some(handle) = small_native_handle_id(value) { let net_socket = crate::object::net_socket_handle_probe() diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 2cf6d643bd..da1923b313 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -17,7 +17,6 @@ use std::sync::{ }; mod async_hooks_exports; -pub(crate) use async_hooks_exports::async_resource_prototype_method_value; mod callable_export_arity_table; mod callable_export_check; mod callable_export_table; @@ -448,29 +447,13 @@ static NM_NAMESPACE_OPS_IMPL: super::NmNamespaceOps = super::NmNamespaceOps { /// Dynamic-`super()` EventEmitter-subclass init (extracted from /// `closure::dispatch::value_call`; see `NmNamespaceOps::ee_dynamic_super`). -unsafe fn nm_ee_dynamic_super( - func_value: f64, - args_ptr: *const f64, - args_len: usize, -) -> Option { +unsafe fn nm_ee_dynamic_super(func_value: f64) -> Option { let (module, method) = bound_native_callable_module_and_method(func_value)?; if module.trim_start_matches("node:") == "events" && (method == "EventEmitter" || method == "EventEmitterAsyncResource") { let this_val = super::js_implicit_this_get(); if crate::value::JSValue::from_bits(this_val.to_bits()).is_pointer() { - if method == "EventEmitterAsyncResource" { - let options = if !args_ptr.is_null() && args_len > 0 { - *args_ptr - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - }; - return Some( - crate::node_stream::js_event_emitter_async_resource_subclass_init( - this_val, options, - ), - ); - } return Some(crate::node_stream::js_event_emitter_subclass_init(this_val)); } } diff --git a/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs b/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs index 171cd7f209..43db54c96a 100644 --- a/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs +++ b/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs @@ -52,30 +52,6 @@ extern "C" fn async_hooks_prototype_method_thunk( } let args_array = crate::value::js_nanbox_get_pointer(rest); - let receiver_raw = if receiver.to_bits() >> 48 == 0x7FFD { - (receiver.to_bits() & crate::value::POINTER_MASK) as i64 - } else { - 0 - }; - let args = if args_array == 0 { - Vec::new() - } else { - let array = args_array as *const crate::array::ArrayHeader; - let len = crate::array::js_array_length(array) as usize; - (0..len) - .map(|index| f64::from_bits(crate::array::js_array_get(array, index as u32).bits())) - .collect::>() - }; - if let Ok(name) = std::str::from_utf8(name) { - if let Some(result) = crate::async_hooks::try_async_resource_method_dispatch( - receiver_raw, - name, - args.as_ptr(), - args.len(), - ) { - return result; - } - } crate::object::js_native_call_method_apply(receiver, name_ptr, name_len, args_array) } } @@ -188,23 +164,6 @@ fn attach_prototype(constructor_value: f64, methods: &[(&str, u32)]) -> f64 { ) } -/// Materialize an unbound `AsyncResource.prototype` method for native-handle -/// property reads whose static type was erased. Invocation observes the -/// call-site receiver through `IMPLICIT_THIS`, just like the real prototype. -pub(crate) fn async_resource_prototype_method_value(name: &'static str, length: u32) -> f64 { - let thunk = async_hooks_prototype_method_thunk as *const u8; - crate::closure::js_register_closure_rest(thunk, 0); - let closure = crate::closure::js_closure_alloc(thunk, 2); - if closure.is_null() { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - crate::closure::js_closure_set_capture_ptr(closure, 0, name.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(closure, 1, name.len() as i64); - super::callable_exports::set_builtin_closure_length(closure as usize, length); - super::callable_exports::set_bound_native_closure_name(closure, name); - crate::value::js_nanbox_pointer(closure as i64) -} - pub(super) fn attach_async_local_storage_prototype(constructor_value: f64) -> f64 { attach_prototype(constructor_value, ASYNC_LOCAL_STORAGE_METHODS) } 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..93174de757 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 @@ -152,13 +152,6 @@ pub(crate) unsafe fn nm_dispatch_async_hooks( typed_kind ); match (module_name, method_name) { - ("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); - crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) - } ("async_hooks", "createHook") => { ptr_to_f64(crate::async_hooks::js_async_hooks_create_hook(arg(0)) as *const u8) } diff --git a/crates/perry-runtime/src/object/nm_namespace_hooks.rs b/crates/perry-runtime/src/object/nm_namespace_hooks.rs index abcfbf4c9d..ac06fe107f 100644 --- a/crates/perry-runtime/src/object/nm_namespace_hooks.rs +++ b/crates/perry-runtime/src/object/nm_namespace_hooks.rs @@ -51,7 +51,7 @@ pub(crate) struct NmNamespaceOps { /// Dynamic `super()` for `class X extends `: /// installs the EE methods on the fresh instance. `None` when the callee /// is not the bound events export (fall through to normal call dispatch). - pub ee_dynamic_super: unsafe fn(f64, *const f64, usize) -> Option, + pub ee_dynamic_super: unsafe fn(f64) -> Option, } static NM_NAMESPACE_OPS: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); diff --git a/crates/perry-runtime/src/object/reflect_support.rs b/crates/perry-runtime/src/object/reflect_support.rs index 6963e1f73f..faae487187 100644 --- a/crates/perry-runtime/src/object/reflect_support.rs +++ b/crates/perry-runtime/src/object/reflect_support.rs @@ -43,12 +43,8 @@ pub(crate) fn obj_value_no_extend(value: f64) -> bool { pub(crate) fn obj_value_has_own_key(value: f64, key: f64) -> bool { unsafe { if crate::symbol::js_is_symbol(key) != 0 { - // Presence cannot be inferred from the value: an own Symbol-keyed - // data property is allowed to contain `undefined`. The old value - // probe therefore turned an existing writable property into an - // apparent miss, which made OrdinarySet drop the first metadata - // overwrite on native AsyncResource handles. - return crate::symbol::has_own_symbol_property(value, key); + let v = crate::symbol::js_object_get_symbol_property(value, key); + return v.to_bits() != crate::value::TAG_UNDEFINED; } let obj = extract_obj_ptr(value); if obj.is_null() { diff --git a/crates/perry-runtime/src/object/tests.rs b/crates/perry-runtime/src/object/tests.rs index e67a5443bf..ad92b4d2fb 100644 --- a/crates/perry-runtime/src/object/tests.rs +++ b/crates/perry-runtime/src/object/tests.rs @@ -652,34 +652,6 @@ fn symbol_keys_keep_creation_order_across_accessor_redefine() { } } -#[test] -fn undefined_symbol_value_still_counts_as_an_own_property() { - let _global = crate::gc::global_side_table_test_lock(); - crate::symbol::test_clear_symbol_side_table_roots(); - unsafe { - let obj = js_object_alloc(0, 0); - assert!(!obj.is_null()); - let obj_value = crate::value::js_nanbox_pointer(obj as i64); - let symbol = crate::symbol::js_symbol_new_empty(); - crate::symbol::js_object_set_symbol_property( - obj_value, - symbol, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); - - assert!(crate::symbol::has_own_symbol_property(obj_value, symbol)); - assert!(super::reflect_support::obj_value_has_own_key( - obj_value, symbol - )); - crate::proxy::js_put_value_set(obj_value, symbol, 42.0, obj_value, 1); - assert_eq!( - crate::symbol::js_object_get_symbol_property(obj_value, symbol).to_bits(), - 42.0f64.to_bits(), - "OrdinarySet must overwrite an own Symbol property whose old value is undefined" - ); - } -} - /// #7916 / #8047: the per-object footprint accounting this issue is about, /// pinned as an executable fact rather than a comment. /// diff --git a/crates/perry-runtime/src/object/to_string_tag.rs b/crates/perry-runtime/src/object/to_string_tag.rs index 2b1bdd5770..842e574db8 100644 --- a/crates/perry-runtime/src/object/to_string_tag.rs +++ b/crates/perry-runtime/src/object/to_string_tag.rs @@ -436,7 +436,7 @@ pub unsafe extern "C" fn js_object_to_string(value: f64) -> f64 { /// caller's `None` arm. fn native_module_to_string_tag(module: &str) -> Option<&'static str> { match module { - "module" | "async_hooks" => Some("Module"), + "module" => Some("Module"), // `Object.prototype.toString.call(performance)` is // "[object Performance]" in Node. "perf_hooks" => Some("Performance"), diff --git a/crates/perry-runtime/src/os/signal.rs b/crates/perry-runtime/src/os/signal.rs index b2f1becdca..d567c5f657 100644 --- a/crates/perry-runtime/src/os/signal.rs +++ b/crates/perry-runtime/src/os/signal.rs @@ -1,15 +1,10 @@ use crate::fs::validate::{describe_received, is_numeric, throw_type_error_with_code}; use crate::string::{js_string_from_bytes, StringHeader}; use crate::value::{JSValue, TAG_TRUE}; -use std::collections::HashMap; #[cfg(unix)] use std::sync::atomic::AtomicI32; #[cfg(any(unix, windows))] use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{LazyLock, Mutex}; - -static SIGNAL_ASYNC_IDS: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); fn signal_number_by_name(name: &str) -> Option { #[cfg(unix)] @@ -97,18 +92,6 @@ static SIGQUIT_LISTENERS: AtomicUsize = AtomicUsize::new(0); #[cfg(unix)] static SIGQUIT_INSTALLED: AtomicBool = AtomicBool::new(false); #[cfg(unix)] -static SIGUSR1_PENDING: AtomicUsize = AtomicUsize::new(0); -#[cfg(unix)] -static SIGUSR1_LISTENERS: AtomicUsize = AtomicUsize::new(0); -#[cfg(unix)] -static SIGUSR1_INSTALLED: AtomicBool = AtomicBool::new(false); -#[cfg(unix)] -static SIGUSR2_PENDING: AtomicUsize = AtomicUsize::new(0); -#[cfg(unix)] -static SIGUSR2_LISTENERS: AtomicUsize = AtomicUsize::new(0); -#[cfg(unix)] -static SIGUSR2_INSTALLED: AtomicBool = AtomicBool::new(false); -#[cfg(unix)] static SIGABRT_PENDING: AtomicUsize = AtomicUsize::new(0); #[cfg(unix)] static SIGABRT_LISTENERS: AtomicUsize = AtomicUsize::new(0); @@ -165,20 +148,6 @@ static PROCESS_SIGNAL_SLOTS: &[ProcessSignalSlot] = &[ listeners: &SIGQUIT_LISTENERS, installed: &SIGQUIT_INSTALLED, }, - ProcessSignalSlot { - name: "SIGUSR1", - number: libc::SIGUSR1, - pending: &SIGUSR1_PENDING, - listeners: &SIGUSR1_LISTENERS, - installed: &SIGUSR1_INSTALLED, - }, - ProcessSignalSlot { - name: "SIGUSR2", - number: libc::SIGUSR2, - pending: &SIGUSR2_PENDING, - listeners: &SIGUSR2_LISTENERS, - installed: &SIGUSR2_INSTALLED, - }, ProcessSignalSlot { name: "SIGABRT", number: libc::SIGABRT, @@ -525,29 +494,6 @@ pub(crate) fn set_process_signal_listener_count(name: &str, count: usize) { { let _ = (name, count); } - - if count > 0 { - let needs_resource = !SIGNAL_ASYNC_IDS.lock().unwrap().contains_key(name); - if needs_resource { - let resource = crate::object::js_object_alloc_null_proto(0, 0); - let async_id = crate::async_hooks::init_resource( - "SIGNALWRAP", - crate::value::js_nanbox_pointer(resource as i64), - true, - ) - .async_id; - SIGNAL_ASYNC_IDS - .lock() - .unwrap() - .entry(name.to_string()) - .or_insert(async_id); - } - } else { - let async_id = SIGNAL_ASYNC_IDS.lock().unwrap().remove(name); - if let Some(async_id) = async_id { - crate::async_hooks::destroy(async_id); - } - } } /// Whether a *pending, undelivered* signal is waiting to be drained. diff --git a/crates/perry-runtime/src/promise/assimilate.rs b/crates/perry-runtime/src/promise/assimilate.rs index 122aa716d7..6e1a1bbd72 100644 --- a/crates/perry-runtime/src/promise/assimilate.rs +++ b/crates/perry-runtime/src/promise/assimilate.rs @@ -278,8 +278,6 @@ extern "C" fn native_promise_adoption_job(closure: *const crate::closure::Closur is_error, capture_context(), std::ptr::null_mut(), - unsafe { (*outer).async_id }, - unsafe { (*outer).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..510cf31bee 100644 --- a/crates/perry-runtime/src/promise/async_step.rs +++ b/crates/perry-runtime/src/promise/async_step.rs @@ -295,7 +295,7 @@ pub extern "C" fn js_promise_resolved_then( // The primitive fast path below bypasses `js_promise_new`/`then`, so it // would never fire `v8.promiseHooks` (#3139). When hooks are active, route // through the real resolve+then path instead. - if crate::v8::promise_hooks_active() || crate::async_hooks::promise_hooks_active() { + if crate::v8::promise_hooks_active() { bump(&MT_FAST_PATH_MISS); let p1 = js_promise_resolved(value); return js_promise_then(p1, on_fulfilled, on_rejected); @@ -510,46 +510,24 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * let can_reuse = !trap.trap_next.is_null() && trap.current_step == step_closure as usize; let trap_next = trap.trap_next; - let async_hooks_active = crate::async_hooks::promise_hooks_active(); - let (next, queued_value, is_error, direct_trigger_async_id) = if is_definitely_primitive(value) - { + let (next, queued_value, is_error) = if is_definitely_primitive(value) { // Primitive value: enqueue Task::AsyncStep directly. bump(&MT_FAST_PATH_HIT); - // The optimized path skips the `PromiseResolve(%Promise%, value)` - // Promise that Node creates for `await value`. That Promise is still - // observable through async_hooks, and the continuation Promise must - // name it as its trigger. Materialize it only while Promise hooks are - // active; the normal no-hook fast path remains allocation-free. - let awaited = if async_hooks_active { - js_promise_resolved(value) - } else { - std::ptr::null_mut() - }; - let awaited_async_id = if awaited.is_null() { - 0 - } else { - unsafe { (*awaited).async_id } - }; ( if can_reuse { bump(&MT_STEP_CHAIN_REUSE_HIT); trap_next - } else if !awaited.is_null() { - bump(&MT_STEP_CHAIN_REUSE_MISS); - super::then::js_promise_new_with_parent(awaited) } else { bump(&MT_STEP_CHAIN_REUSE_MISS); js_promise_new() }, value, false, - awaited_async_id, ) } else if js_value_is_promise(value) != 0 { let inner = crate::value::js_nanbox_get_pointer(value) as *mut Promise; if !inner.is_null() { let inner_state = unsafe { (*inner).state }; - let inner_async_id = unsafe { (*inner).async_id }; match inner_state { PromiseState::Fulfilled => { // Inner already settled with a primitive (the steady @@ -561,16 +539,12 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * if can_reuse { bump(&MT_STEP_CHAIN_REUSE_HIT); trap_next - } else if async_hooks_active { - bump(&MT_STEP_CHAIN_REUSE_MISS); - super::then::js_promise_new_with_parent(inner) } else { bump(&MT_STEP_CHAIN_REUSE_MISS); js_promise_new() }, unwrapped, false, - inner_async_id, ) } PromiseState::Rejected => { @@ -590,16 +564,12 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * if can_reuse { bump(&MT_STEP_CHAIN_REUSE_HIT); trap_next - } else if async_hooks_active { - bump(&MT_STEP_CHAIN_REUSE_MISS); - super::then::js_promise_new_with_parent(inner) } else { bump(&MT_STEP_CHAIN_REUSE_MISS); js_promise_new() }, reason, true, - inner_async_id, ) } PromiseState::Pending => { @@ -659,25 +629,6 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * crate::value::js_nanbox_get_pointer(v) as *mut Promise } }; - let (step_async_id, step_trigger_id) = if next.is_null() { - (0, 0) - } else if can_reuse && async_hooks_active { - let resource = crate::value::js_nanbox_pointer(next as i64); - let trigger_async_id = if direct_trigger_async_id != 0 { - direct_trigger_async_id - } else { - crate::async_hooks::execution_async_id_u64() - }; - let ids = crate::async_hooks::init_resource_with_trigger( - "PROMISE", - resource, - false, - trigger_async_id, - ); - (ids.async_id, ids.trigger_async_id) - } else { - unsafe { ((*next).async_id, (*next).trigger_async_id) } - }; crate::r#box::retain_async_box_activation(trap.box_activation); TASK_QUEUE.with(|q| { q.borrow_mut().push_back(Task::AsyncStep( @@ -687,8 +638,6 @@ pub extern "C" fn js_async_step_chain(value: f64, step_closure: ClosurePtr) -> * is_error, context, trap.box_activation, - step_async_id, - step_trigger_id, )); }); crate::event_pump::js_notify_promise_progress(); diff --git a/crates/perry-runtime/src/promise/microtasks.rs b/crates/perry-runtime/src/promise/microtasks.rs index db67ddb45a..84f7813443 100644 --- a/crates/perry-runtime/src/promise/microtasks.rs +++ b/crates/perry-runtime/src/promise/microtasks.rs @@ -348,12 +348,8 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // keep the macrotask-boundary ticks-first ordering. let ticks_allowed = !matches!(mode, MicrotaskDrainMode::PromiseJobsOnly); let mid_promise_job = CURRENT_MICROTASK_CALLBACK.with(|c| !c.get().is_null()); - let esm_checkpoint = ticks_allowed && consume_esm_eval_checkpoint(); - if esm_checkpoint { - crate::async_hooks::init_esm_evaluation_promise(); - } let mut esm_defer_tick_drain = if ticks_allowed { - esm_checkpoint || (reentrant && mid_promise_job) + consume_esm_eval_checkpoint() || (reentrant && mid_promise_job) } else { // PromiseJobsOnly never drains ticks; leave the one-shot ESM flag // for the first real checkpoint to consume. @@ -401,20 +397,9 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // No callback registered → propagate the value/reason // to the next promise without invoking anything. if callback.is_null() { - let async_id = if (*promise).next.is_null() { - 0 - } else { - (*(*promise).next).async_id - }; - let trigger_async_id = if (*promise).next.is_null() { - 0 - } else { - (*(*promise).next).trigger_async_id - }; CURRENT_MICROTASK_PROMISE.with(|c| c.set(promise)); 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() { if is_fulfilled { js_promise_resolve((*promise).next, value); @@ -422,7 +407,6 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { js_promise_reject((*promise).next, value); } } - crate::async_hooks::after_promise(async_id); let promise = CURRENT_MICROTASK_PROMISE.with(|c| c.replace(std::ptr::null_mut())); CURRENT_MICROTASK_VALUE.with(|c| c.set(0.0)); @@ -484,22 +468,15 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { } else { None }; - // A Promise reaction executes as the child Promise - // returned by `.then()`, not as its parent. The parent - // owns the callback slot in Perry, but Node exposes the - // child's id/resource to before/after and - // executionAsyncResource(). Capture those child ids as - // plain values before user code can move the heap. - let async_id = if (*promise).next.is_null() { - 0 - } else { - (*(*promise).next).async_id - }; - let trigger_async_id = if (*promise).next.is_null() { - 0 - } else { - (*(*promise).next).trigger_async_id - }; + // #1663: capture async_id + trigger as plain values BEFORE + // the callback. They are immutable for the promise's life, + // and the callback can re-entrantly drain microtasks (which + // can move the promise via GC or realloc the GC-root handle + // stack). Reading `(*promise).async_id` AFTER the callback + // to feed `after()` was the exact deref that segfaulted; use + // the captured value so `after()` needs no live promise. + let async_id = (*promise).async_id; + let trigger_async_id = (*promise).trigger_async_id; crate::async_hooks::before_promise(async_id, trigger_async_id); let promise = promise_handle.get_raw_mut_ptr::(); crate::v8::promise_hook_before(promise); @@ -594,17 +571,6 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { let callback = rooted_closure(&callback_handle); let value = value_handle.get_nanbox_f64(); let next = rooted_promise(&next_handle); - let async_id = if next.is_null() { - 0 - } else { - unsafe { (*next).async_id } - }; - let trigger_async_id = if next.is_null() { - 0 - } else { - unsafe { (*next).trigger_async_id } - }; - crate::async_hooks::before_promise(async_id, trigger_async_id); // Inline tasks are produced by `js_promise_resolved_then` // (the `Promise.resolve().then(cb_f, cb_e)` // fast path). We've already skipped allocating the @@ -618,7 +584,6 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { js_promise_reject(next, value); } } - crate::async_hooks::after_promise(async_id); restore_microtask_context(); ran += 1; continue; @@ -665,7 +630,6 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { CURRENT_MICROTASK_VALUE.with(|c| c.set(result)); let next_for_after = CURRENT_MICROTASK_NEXT.with(|c| c.get()); crate::v8::promise_hook_after(next_for_after); - crate::async_hooks::after_promise(async_id); if let Some(t) = t1 { MT_TIME_NS_CALLBACK .fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed); @@ -744,8 +708,6 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { is_error, task_context, box_activation, - step_async_id, - step_trigger_id, )) => { bump(&MT_RUN_COUNT); // The popped Task's activation reference transfers to the @@ -939,6 +901,16 @@ fn run_microtasks(mode: MicrotaskDrainMode) -> i32 { // `next` via GC, #1663) and feed the same id to `after()`. // `before`/`after` early-return on id 0, so this is a no-op // when async_hooks are inactive. + let step_async_id = if next.is_null() { + 0 + } else { + unsafe { (*next).async_id } + }; + let step_trigger_id = if next.is_null() { + 0 + } else { + unsafe { (*next).trigger_async_id } + }; crate::async_hooks::before_promise(step_async_id, step_trigger_id); let next = rooted_promise(&next_handle); crate::v8::promise_hook_before(next); diff --git a/crates/perry-runtime/src/promise/mod.rs b/crates/perry-runtime/src/promise/mod.rs index 3d31310e26..6782f06af6 100644 --- a/crates/perry-runtime/src/promise/mod.rs +++ b/crates/perry-runtime/src/promise/mod.rs @@ -542,9 +542,7 @@ pub struct Promise { pub(crate) on_rejected: ClosurePtr, /// Next promise in the chain (for .then()) pub(crate) next: *mut Promise, - /// Stable async_hooks asyncId for this Promise. IDs are reserved even - /// before hooks are enabled so a later reaction can still point at its - /// pre-created parent. + /// async_hooks asyncId for this Promise, 0 when hooks were inactive. pub(crate) async_id: u64, /// async_hooks triggerAsyncId captured at Promise creation. pub(crate) trigger_async_id: u64, @@ -612,8 +610,6 @@ pub(crate) enum Task { bool, AsyncContextSnapshot, *mut crate::r#box::AsyncBoxActivation, - u64, - u64, ), } diff --git a/crates/perry-runtime/src/promise/scanners.rs b/crates/perry-runtime/src/promise/scanners.rs index f778cc4e82..dd27803d03 100644 --- a/crates/perry-runtime/src/promise/scanners.rs +++ b/crates/perry-runtime/src/promise/scanners.rs @@ -41,7 +41,7 @@ pub fn scan_promise_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { visitor.visit_raw_const_ptr_slot(callback); scan_snapshot_roots_mut(context, visitor); } - Task::AsyncStep(cb, value, next, _, context, _, _, _) => { + Task::AsyncStep(cb, value, next, _, context, _) => { visitor.visit_raw_const_ptr_slot(cb); visitor.visit_raw_mut_ptr_slot(next); visitor.visit_nanbox_f64_slot(value); @@ -265,7 +265,7 @@ fn scan_task_step( scan_task_slot_promise_all(promise_state, value, context, visitor, state, remaining) } Task::Inline(cb, value, next, _, context) - | Task::AsyncStep(cb, value, next, _, context, _, _, _) => { + | Task::AsyncStep(cb, value, next, _, context, _) => { scan_task_slot_inline(cb, value, next, context, visitor, state, remaining) } Task::Microtask { @@ -770,8 +770,6 @@ pub(crate) fn test_seed_promise_scanner_roots( false, context.clone(), std::ptr::null_mut(), - 0, - 0, )); }); PROMISE_CONTEXTS.with(|contexts| { @@ -856,7 +854,7 @@ pub(crate) fn test_promise_scanner_snapshot() -> TestPromiseScannerSnapshot { snapshot.inline_next_ptr = *next_ptr as usize; snapshot.inline_value_bits = value.to_bits(); } - if let Some(Task::AsyncStep(callback_ptr, value, next_ptr, _, _, _, _, _)) = q.get(2) { + if let Some(Task::AsyncStep(callback_ptr, value, next_ptr, _, _, _)) = q.get(2) { snapshot.async_step_callback_ptr = *callback_ptr as usize; snapshot.async_step_next_ptr = *next_ptr as usize; snapshot.async_step_value_bits = value.to_bits(); diff --git a/crates/perry-runtime/src/promise/then.rs b/crates/perry-runtime/src/promise/then.rs index 1601cc99b7..ba5c7fa6f0 100644 --- a/crates/perry-runtime/src/promise/then.rs +++ b/crates/perry-runtime/src/promise/then.rs @@ -69,12 +69,6 @@ fn js_promise_new_with_parent_impl(parent: *mut Promise, force_malloc: bool) -> bump(&MT_PROMISE_NEW_COUNT); let async_hooks_active = crate::async_hooks::promise_hooks_active(); let lifecycle_hooks_active = async_hooks_active || crate::v8::promise_hooks_active(); - // Root the parent before allocating the child. The allocation itself may - // move an arena-resident Promise that predates hook enablement; rooting it - // afterwards records the retired address and gives the child a stale - // parent/trigger id. - let scope = crate::gc::RuntimeHandleScope::new(); - let parent_handle = scope.root_raw_mut_ptr(parent); let raw = if lifecycle_hooks_active || force_malloc { crate::gc::gc_malloc(std::mem::size_of::(), crate::gc::GC_TYPE_PROMISE) } else { @@ -85,57 +79,30 @@ fn js_promise_new_with_parent_impl(parent: *mut Promise, force_malloc: bool) -> ) }; let promise = raw as *mut Promise; + let scope = crate::gc::RuntimeHandleScope::new(); let promise_handle = scope.root_raw_mut_ptr(promise); + let parent_handle = scope.root_raw_mut_ptr(parent); unsafe { // GC_STORE_AUDIT(INIT): initializes freshly allocated Promise storage before the promise is published. ptr::write(promise, Promise::new()); - let trigger_async_id = parent_handle.with_mut_ptr::(|parent| { - if parent.is_null() { - crate::async_hooks::execution_async_id_u64() - } else { - (*parent).async_id - } - }); if async_hooks_active { - let ids = promise_handle.with_mut_ptr::(|promise| { - let resource = f64::from_bits( - 0x7FFD_0000_0000_0000 | (promise as u64 & 0x0000_FFFF_FFFF_FFFF), - ); - crate::async_hooks::init_resource_with_trigger( - "PROMISE", - resource, - false, - trigger_async_id, - ) - }); - promise_handle.with_mut_ptr::(|promise| { - (*promise).async_id = ids.async_id; - (*promise).trigger_async_id = ids.trigger_async_id; - }); - } else { - // Node assigns every Promise an id, not only promises created - // while an observer is enabled. Keep this reservation out of the - // resource table so an unobserved promise is not made immortal by - // a strong resource root. - let ids = crate::async_hooks::reserve_resource_ids(trigger_async_id); - promise_handle.with_mut_ptr::(|promise| { - (*promise).async_id = ids.async_id; - (*promise).trigger_async_id = ids.trigger_async_id; - }); + let promise = promise_handle.get_raw_mut_ptr::(); + let resource = + f64::from_bits(0x7FFD_0000_0000_0000 | (promise as u64 & 0x0000_FFFF_FFFF_FFFF)); + let ids = crate::async_hooks::init_resource("PROMISE", resource, false); + let promise = promise_handle.get_raw_mut_ptr::(); + (*promise).async_id = ids.async_id; + (*promise).trigger_async_id = ids.trigger_async_id; } } - promise_handle.with_mut_ptr::(|promise| { - parent_handle.with_mut_ptr::(|parent| { - crate::v8::promise_hook_init(promise, parent); - }); - }); - let (_, promise) = promise_handle.across_mut::(|| { - promise_handle.with_mut_ptr::(|promise| { - // #5142: a recycled address may carry expando properties (`p.status = …`) - // left by a previously-collected promise; a fresh promise must start clean. - crate::object::exotic_expando::expando_clear_on_alloc(promise as usize); - }); - }); + crate::v8::promise_hook_init( + promise_handle.get_raw_mut_ptr::(), + parent_handle.get_raw_mut_ptr::(), + ); + let promise = promise_handle.get_raw_mut_ptr::(); + // #5142: a recycled address may carry expando properties (`p.status = …`) + // left by a previously-collected promise; a fresh promise must start clean. + crate::object::exotic_expando::expando_clear_on_alloc(promise as usize); promise } @@ -275,6 +242,9 @@ pub extern "C" fn js_promise_resolve(promise: *mut Promise, value: f64) { // 1 s idle cap before the loop re-checks promise state. The notify // sets the flag so the immediately-following wait returns at once. crate::event_pump::js_notify_promise_progress(); + unsafe { + crate::async_hooks::destroy_promise((*promise).async_id); + } } /// Resolve a promise with another promise (Promise chaining/unwrapping) @@ -491,6 +461,9 @@ pub extern "C" fn js_promise_reject(promise: *mut Promise, reason: f64) { } // Issue #84: see js_promise_resolve — same wake reasoning. crate::event_pump::js_notify_promise_progress(); + unsafe { + crate::async_hooks::destroy_promise((*promise).async_id); + } } /// Register fulfillment callback, returns a new promise for chaining @@ -1767,8 +1740,6 @@ extern "C" fn finally_passthrough_fulfill( false, capture_context(), std::ptr::null_mut(), - unsafe { (*next).async_id }, - unsafe { (*next).trigger_async_id }, )); }); crate::event_pump::js_notify_promise_progress(); @@ -1795,8 +1766,6 @@ extern "C" fn finally_passthrough_reject( true, capture_context(), std::ptr::null_mut(), - unsafe { (*next).async_id }, - unsafe { (*next).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..996a23cc8a 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -1069,33 +1069,7 @@ fn small_handle_from_value(value: f64) -> Option { None } -/// Native `AsyncResource` values are process-stable `Box` pointers rather -/// than GC `ObjectHeader`s or ids in the small-handle band. Recognize their -/// exact registry membership before any generic object walk can interpret the -/// allocation (or its allocator prefix) as GC/object metadata. -fn async_resource_handle_from_value(value: f64) -> Option { - let bits = value.to_bits(); - let raw = match bits >> 48 { - top if top == (POINTER_TAG >> 48) => (bits & POINTER_MASK) as i64, - 0 => bits as i64, - _ => return None, - }; - crate::async_hooks::is_async_resource_handle(raw).then_some(raw) -} - 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) }; - return Some(true); - } - let Some(name) = key_to_rust_string(key) else { - return Some(false); - }; - crate::object::handle_expando::handle_expando_set(handle, &name, value); - return Some(true); - } - let handle = small_handle_from_value(target)?; let Some(name) = key_to_rust_string(key) else { // A SYMBOL-keyed write on a small native handle (e.g. the @@ -1429,11 +1403,8 @@ fn own_set_descriptor(target: f64, key: f64) -> Option { } if unsafe { crate::symbol::js_is_symbol(key) } != 0 { - // `undefined` is a valid value for an existing own property. Using a - // value read as the existence probe made the first overwrite of the - // symbol metadata installed by an async_hooks `init` callback take the - // absent-property path and disappear on native AsyncResource handles. - if !unsafe { crate::symbol::has_own_symbol_property(target, key) } { + let value = unsafe { crate::symbol::js_object_get_symbol_property(target, key) }; + if value.to_bits() == TAG_UNDEFINED { return None; } // An existing symbol-keyed own data property is non-writable when the diff --git a/crates/perry-runtime/src/proxy/reflect.rs b/crates/perry-runtime/src/proxy/reflect.rs index 8833a56912..d91cfc26d2 100644 --- a/crates/perry-runtime/src/proxy/reflect.rs +++ b/crates/perry-runtime/src/proxy/reflect.rs @@ -109,21 +109,6 @@ pub extern "C" fn js_reflect_set(target: f64, key: f64, value: f64, receiver: f6 if lookup(target).is_some() { return super::proxy_set_with_receiver(target, property_key, value, receiver); } - // ES-module namespace imports use the namespace exotic [[Set]] operation: - // their reflected data descriptors report writable=true, but writes still - // fail. `node:async_hooks` is materialized through Perry's shared native - // namespace object, so preserve that otherwise-unusual combination here. - let target_ptr = extract_pointer(target.to_bits()); - if target_ptr != 0 { - let obj = target_ptr as *const crate::object::ObjectHeader; - if crate::value::addr_class::is_plausible_heap_addr(target_ptr as usize) - && unsafe { (*obj).class_id } == crate::object::NATIVE_MODULE_CLASS_ID - && unsafe { crate::object::read_native_module_name(obj) }.as_deref() - == Some("async_hooks") - { - return nanbox_bool(false); - } - } reflect_ordinary_set_with_receiver(target, property_key, value, receiver) } diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index 8edf6e0d4d..dffcb0ddd2 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -338,14 +338,6 @@ pub extern "C" fn js_symbol_well_known_iterator() -> f64 { f64::from_bits(POINTER_TAG | (symbol as u64 & POINTER_MASK)) } -/// Provider-safe C ABI for separately linked native extensions that need to -/// expose a genuine async iterable. -#[no_mangle] -pub extern "C" fn js_symbol_well_known_async_iterator() -> f64 { - let symbol = well_known_symbol("asyncIterator"); - f64::from_bits(POINTER_TAG | (symbol as u64 & POINTER_MASK)) -} - /// O(1) check whether a raw pointer is a well-known symbol (Symbol.toPrimitive etc.). /// Used by `js_symbol_key_for` so the spec-mandated `undefined` return for /// well-known symbols is preserved. diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 44dd490707..83b79c635d 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -802,10 +802,7 @@ pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f6 let tag_f64 = f64::from_bits(crate::value::JSValue::pointer(tag_wk as *const u8).bits()); if sym_key_from_f64(sym_f64) == sym_key_from_f64(tag_f64) - && matches!( - crate::object::read_native_module_name(obj).as_deref(), - Some("module" | "async_hooks") - ) + && crate::object::read_native_module_name(obj).as_deref() == Some("module") { let tag = b"Module"; let value = js_string_from_bytes(tag.as_ptr(), tag.len() as u32); diff --git a/crates/perry-runtime/src/symbol/properties.rs b/crates/perry-runtime/src/symbol/properties.rs index 25417651c0..b2dbfac3b1 100644 --- a/crates/perry-runtime/src/symbol/properties.rs +++ b/crates/perry-runtime/src/symbol/properties.rs @@ -335,14 +335,6 @@ unsafe fn set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64 if obj_key == 0 || sym_key == 0 { return value_f64; } - // A base AsyncResource's public identity is a process-stable Box pointer, - // registered separately from Perry's moving GC heap. It intentionally - // uses POINTER_TAG so it behaves as an object, but bytes before/inside the - // Box are allocator state and AsyncResource ids, not GcHeader/ObjectHeader - // fields. Never use those bytes for frozen/extensible or class-setter - // decisions; symbol values for the handle live entirely in this side - // table. - let native_async_resource = crate::async_hooks::is_async_resource_handle(obj_key as i64); super::note_symbol_key_installed(sym_key); // #5437 (Next.js): a native HANDLE (small-id NaN-boxed POINTER, e.g. the // node:http IncomingMessage) carries per-request metadata in the symbol @@ -392,8 +384,7 @@ unsafe fn set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64 // like string-keyed ones: an existing prop is non-writable when frozen // (or its per-symbol attrs say so), a new prop is forbidden when // non-extensible. Only heap receivers carry the GC flag word. - if !native_async_resource - && (obj_f64.to_bits() >> 48) == 0x7FFD + if (obj_f64.to_bits() >> 48) == 0x7FFD && obj_key >= 0x10000 && crate::object::is_valid_obj_ptr(obj_key as *const u8) { @@ -420,7 +411,7 @@ unsafe fn set_symbol_property(obj_f64: f64, sym_f64: f64, value_f64: f64) -> f64 { return value_f64; } - } else if !native_async_resource { + } else { let jsval = crate::value::JSValue::from_bits(bits); if jsval.is_pointer() { let ptr = jsval.as_pointer::(); diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 0697c30bf1..1a45fe9c1a 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -12,11 +12,10 @@ mod async_lifecycle; use crate::promise::{js_promise_new, js_promise_resolve, Promise}; use async_lifecycle::{enqueue_destroy_ids, IntervalCallback}; use std::any::Any; -use std::collections::HashMap; use std::os::raw::c_int; use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, - LazyLock, Mutex, + Mutex, }; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -420,8 +419,6 @@ pub(crate) use gc_scan::{new_timer_root_scan_state, scan_timer_roots_mut_step}; use ref_states::{TimerRefStates, TIMER_REF_STATES_CAP}; static TIMER_REF_STATES: Mutex> = Mutex::new(None); -static TIMER_HANDLE_KINDS: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); static WARNED_NEGATIVE_TIMER_DELAY: AtomicBool = AtomicBool::new(false); static WARNED_NAN_TIMER_DELAY: AtomicBool = AtomicBool::new(false); @@ -601,42 +598,6 @@ fn set_timer_ref_state(id: i64, has_ref: bool) { .insert_bounded(id, has_ref, TIMER_REF_STATES_CAP); } -fn record_timer_handle_kind(id: i64, kind: CallbackTimerKind) { - TIMER_HANDLE_KINDS.lock().unwrap().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. -pub(crate) fn timer_constructor_value(id: i64) -> Option { - let kind = TIMER_HANDLE_KINDS.lock().unwrap().get(&id).copied()?; - let name = match kind { - CallbackTimerKind::Timeout => b"Timeout".as_slice(), - CallbackTimerKind::Immediate => b"Immediate".as_slice(), - }; - let scope = crate::gc::RuntimeHandleScope::new(); - let obj = scope.root_raw_mut_ptr(crate::object::js_object_alloc_null_proto(0, 0)); - let key = scope.root_string_ptr(crate::string::js_string_from_bytes(b"name".as_ptr(), 4)); - let value = scope.root_string_ptr(crate::string::js_string_from_bytes( - name.as_ptr(), - name.len() as u32, - )); - let (_, obj_ptr) = obj.across_mut::(|| { - obj.with_mut_ptr::(|obj_ptr| { - key.with_mut_ptr::(|key_ptr| { - value.with_mut_ptr::(|value_ptr| { - crate::object::js_object_set_field_by_name( - obj_ptr, - key_ptr, - f64::from_bits(crate::value::JSValue::string_ptr(value_ptr).bits()), - ); - }); - }); - }); - }); - Some(crate::value::js_nanbox_pointer(obj_ptr as i64)) -} - pub use ref_states::is_known_timer_id; fn throw_mock_timer_invalid_state(message: &str) -> ! { @@ -744,7 +705,6 @@ fn schedule_mock_callback_timer( let arg_handles = scope.root_nanbox_f64_slice(&args); let delay = normalize_timer_delay(delay_ms); let id = next_timer_id(); - record_timer_handle_kind(id, kind); let due_ms = state.current_ms + delay as f64; state.callbacks.push(MockCallbackTimer { id, @@ -770,7 +730,6 @@ fn schedule_mock_interval_timer(callback: i64, interval_ms: f64, args: Vec) let arg_handles = scope.root_nanbox_f64_slice(&args); let interval = normalize_timer_delay(interval_ms); let id = next_timer_id(); - record_timer_handle_kind(id, CallbackTimerKind::Timeout); let next_ms = state.current_ms + interval as f64; state.intervals.push(MockIntervalTimer { id, @@ -1029,7 +988,6 @@ pub extern "C" fn js_set_timeout_callback(callback: i64, delay_ms: f64) -> i64 { Vec::new(), "Timeout", CallbackTimerKind::Timeout, - None, ) } @@ -1041,7 +999,6 @@ pub extern "C" fn js_set_immediate_callback(callback: i64) -> i64 { Vec::new(), "Immediate", CallbackTimerKind::Immediate, - None, ) } @@ -1051,7 +1008,6 @@ fn schedule_callback_timer( args: Vec, type_name: &str, kind: CallbackTimerKind, - trigger_async_id: Option, ) -> i64 { crate::promise::bump(&PROFILE_CALLBACK_TIMER_REGISTRATIONS); if let Some(id) = schedule_mock_callback_timer(callback, delay_ms, args.clone(), kind) { @@ -1067,22 +1023,12 @@ fn schedule_callback_timer( let deadline = Instant::now() + Duration::from_millis(delay_ms); let id = next_timer_id(); - record_timer_handle_kind(id, kind); let mut context = crate::async_context::capture_context(); let context_roots = crate::async_context::root_snapshot(&scope, &context); - let (ids, callback) = - callback_handle.across_const::( - || match trigger_async_id { - Some(trigger_async_id) => crate::async_hooks::init_resource_with_trigger( - type_name, - timer_handle_value(id), - true, - trigger_async_id, - ), - None => crate::async_hooks::init_resource(type_name, timer_handle_value(id), true), - }, - ); + let (ids, callback) = callback_handle.across_const::(|| { + crate::async_hooks::init_resource(type_name, timer_handle_value(id), true) + }); crate::async_context::refresh_snapshot_from_roots(&mut context, &context_roots); CALLBACK_TIMERS.lock().unwrap().push(CallbackTimer { @@ -1132,7 +1078,6 @@ pub unsafe extern "C" fn js_set_timeout_callback_args( args, "Timeout", CallbackTimerKind::Timeout, - None, ) } @@ -1153,58 +1098,6 @@ pub unsafe extern "C" fn js_set_immediate_callback_args( args, "Immediate", CallbackTimerKind::Immediate, - None, - ) -} - -/// Schedule a native Node-style completion callback as its own async-hooks -/// provider. Native stdlib operations use the ordinary immediate queue for -/// deferred delivery, but must expose their actual provider name (for example -/// `PBKDF2REQUEST`) and execute with that provider's async id/resource rather -/// than masquerading as an `Immediate`. -pub fn schedule_native_callback(callback: i64, args: &[f64], provider_type: &'static str) -> i64 { - schedule_callback_timer( - callback, - 0.0, - args.to_vec(), - provider_type, - CallbackTimerKind::Immediate, - None, - ) -} - -/// Schedule the final callback in a provider chain while emitting the eager -/// native preparation stages ahead of it. Node's `fs.readFile` is implemented -/// as four chained FSREQCALLBACK operations (open, stat, read, close); Perry -/// performs those syscalls eagerly, but the observable hook graph must retain -/// the same four-resource ancestry. -pub fn schedule_native_callback_chain( - callback: i64, - args: &[f64], - provider_type: &'static str, - resource_count: usize, -) -> i64 { - let mut trigger = crate::async_hooks::execution_async_id_u64(); - for _ in 1..resource_count { - let resource = crate::object::js_object_alloc_null_proto(0, 0); - let ids = crate::async_hooks::init_resource_with_trigger( - provider_type, - crate::value::js_nanbox_pointer(resource as i64), - true, - trigger, - ); - crate::async_hooks::before(ids.async_id, ids.trigger_async_id); - crate::async_hooks::after(ids.async_id); - crate::async_hooks::destroy(ids.async_id); - trigger = ids.async_id; - } - schedule_callback_timer( - callback, - 0.0, - args.to_vec(), - provider_type, - CallbackTimerKind::Immediate, - Some(trigger), ) } @@ -1582,7 +1475,6 @@ fn schedule_interval_timer(callback: i64, interval_ms: f64, args: Vec) -> i let next_deadline = Instant::now() + Duration::from_millis(interval); let id = next_timer_id(); - record_timer_handle_kind(id, CallbackTimerKind::Timeout); let mut context = crate::async_context::capture_context(); let context_roots = crate::async_context::root_snapshot(&scope, &context); diff --git a/crates/perry-runtime/src/value/addr_class.rs b/crates/perry-runtime/src/value/addr_class.rs index 41c3397ed8..5295905933 100644 --- a/crates/perry-runtime/src/value/addr_class.rs +++ b/crates/perry-runtime/src/value/addr_class.rs @@ -163,7 +163,7 @@ pub fn is_stream_id_band(id: usize) -> bool { /// `test_gap_proxy_reflect` on Linux. Those dependents must be migrated to an /// explicit band check FIRST; only then can this floor be raised. #[inline(always)] -pub fn is_valid_obj_ptr(ptr: *const u8) -> bool { +pub(crate) fn is_valid_obj_ptr(ptr: *const u8) -> bool { let addr = ptr as u64; #[cfg(any( target_os = "android", diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 56655eb781..0a7e614f98 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -218,19 +218,6 @@ tls-runtime = [ "dep:x509-cert", ] -# Retains the TLS module/server and TLSSocket dispatch surface when `node:tls` -# is routed to perry-ext-net. Unlike `tls`, this must not pull bundled-net back -# in because the wrapper owns client sockets and their event pump. -external-tls-server = [ - "async-runtime", - "dep:tokio-rustls", - "dep:rustls", - "dep:rustls-native-certs", - "dep:rustls-pemfile", - "dep:base64", - "dep:x509-cert", -] - # Databases database = ["database-postgres", "database-mysql", "database-sqlite", "database-redis", "database-mongodb"] # `database-postgres` umbrella retained for backwards-compat; diff --git a/crates/perry-stdlib/src/async_local_storage.rs b/crates/perry-stdlib/src/async_local_storage.rs index 56dc0c90cc..4ab3b9c8bb 100644 --- a/crates/perry-stdlib/src/async_local_storage.rs +++ b/crates/perry-stdlib/src/async_local_storage.rs @@ -8,9 +8,10 @@ use perry_runtime::closure::{is_closure_ptr, js_closure_call_array, ClosureHeade use crate::common::{get_handle_mut, register_handle, Handle}; +const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; -const SUBCLASS_BACKING_KEY: &[u8] = b"__perryAsyncLocalStorageBacking"; // Keep the active context in the single perry-runtime provider. These must be // extern calls rather than Rust-path calls: in app-only dylib deployments the @@ -83,86 +84,6 @@ impl AsyncLocalStorageHandle { } } -fn throw_invalid_receiver() -> ! { - let message = b"Value of \"this\" must be of type AsyncLocalStorage"; - 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); - perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(err as i64)) -} - -pub(crate) fn resolve_async_local_storage_handle(receiver: Handle) -> Option { - if get_handle_mut::(receiver).is_some() { - return Some(receiver); - } - let raw = receiver as usize; - if !perry_runtime::value::addr_class::is_above_handle_band(raw) - || !perry_runtime::value::addr_class::is_valid_obj_ptr(raw as *const u8) - { - return None; - } - let key = perry_runtime::string::js_string_from_bytes( - SUBCLASS_BACKING_KEY.as_ptr(), - SUBCLASS_BACKING_KEY.len() as u32, - ); - let value = perry_runtime::object::js_object_get_field_by_name_f64( - raw as *const perry_runtime::object::ObjectHeader, - key, - ); - if value.to_bits() >> 48 != 0x7FFD { - return None; - } - let backing = (value.to_bits() & POINTER_MASK) as Handle; - get_handle_mut::(backing).map(|_| backing) -} - -/// Stamp an ordinary source-compiled subclass instance with a native ALS -/// backing. Its inherited methods receive the ordinary object as `this` and -/// resolve through the hidden handle above. -#[no_mangle] -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) - 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( - SUBCLASS_BACKING_KEY.as_ptr(), - SUBCLASS_BACKING_KEY.len() as u32, - ); - perry_runtime::object::js_object_set_field_by_name( - raw, - key, - perry_runtime::value::js_nanbox_pointer(backing), - ); - for method in [ - b"run".as_slice(), - b"getStore".as_slice(), - b"enterWith".as_slice(), - b"exit".as_slice(), - b"disable".as_slice(), - ] { - 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 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, - value_handle.get_nanbox_f64(), - ); - } - } - this_handle.get_nanbox_f64() -} - /// Create a new AsyncLocalStorage instance /// Returns a handle (i64) #[no_mangle] @@ -176,7 +97,7 @@ pub extern "C" fn js_async_local_storage_new() -> Handle { /// codegen `NA_VARARGS` lowering (#3093). #[no_mangle] pub unsafe extern "C" fn js_async_local_storage_run( - receiver: Handle, + handle: Handle, store: f64, callback: f64, args_array: i64, @@ -184,8 +105,6 @@ pub unsafe extern "C" fn js_async_local_storage_run( // Validate before mutating the async context so an invalid callback throws // without leaving a pushed store behind (#3092). let cb = validate_callback(callback); - 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 @@ -200,17 +119,18 @@ pub unsafe extern "C" fn js_async_local_storage_run( /// AsyncLocalStorage.getStore() /// Returns the current store (top of stack) or undefined #[no_mangle] -pub extern "C" fn js_async_local_storage_get_store(receiver: Handle) -> f64 { - let handle = - resolve_async_local_storage_handle(receiver).unwrap_or_else(|| throw_invalid_receiver()); - unsafe { js_async_context_als_get_store(handle) } +pub extern "C" fn js_async_local_storage_get_store(handle: Handle) -> f64 { + if get_handle_mut::(handle).is_some() { + return unsafe { js_async_context_als_get_store(handle) }; + } + f64::from_bits(TAG_UNDEFINED) } /// AsyncLocalStorage.enterWith(store) /// 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) { - if let Some(handle) = resolve_async_local_storage_handle(receiver) { +pub extern "C" fn js_async_local_storage_enter_with(handle: Handle, store: f64) { + if get_handle_mut::(handle).is_some() { unsafe { js_async_context_als_enter_with(handle, store) }; } } @@ -221,20 +141,24 @@ pub extern "C" fn js_async_local_storage_enter_with(receiver: Handle, store: f64 /// `NA_VARARGS` lowering (#3093). #[no_mangle] pub unsafe extern "C" fn js_async_local_storage_exit( - receiver: Handle, + handle: Handle, callback: f64, args_array: i64, ) -> f64 { // Validate before clearing the context so an invalid callback throws // without disturbing the saved store (#3092). let cb = validate_callback(callback); - let handle = - resolve_async_local_storage_handle(receiver).unwrap_or_else(|| throw_invalid_receiver()); - js_async_context_als_exit_enter(handle); + + let guarded = get_handle_mut::(handle).is_some(); + if guarded { + js_async_context_als_exit_enter(handle); + } let result = call_with_forwarded_args(cb, args_array); - js_async_context_als_scope_leave(); + if guarded { + js_async_context_als_scope_leave(); + } result } @@ -242,8 +166,8 @@ pub unsafe extern "C" fn js_async_local_storage_exit( /// AsyncLocalStorage.disable() /// Clear the store stack #[no_mangle] -pub extern "C" fn js_async_local_storage_disable(receiver: Handle) { - if let Some(handle) = resolve_async_local_storage_handle(receiver) { +pub extern "C" fn js_async_local_storage_disable(handle: Handle) { + if get_handle_mut::(handle).is_some() { unsafe { js_async_context_als_clear(handle) }; } } diff --git a/crates/perry-stdlib/src/common/dispatch.rs b/crates/perry-stdlib/src/common/dispatch.rs index 97abfc2795..800e1ace1e 100644 --- a/crates/perry-stdlib/src/common/dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch.rs @@ -26,7 +26,6 @@ pub use property_dispatch::js_handle_property_dispatch; pub(crate) use emitter_als::{ dispatch_async_local_storage_method, dispatch_async_local_storage_property, - unbound_async_local_storage_method, }; #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] pub(crate) use emitter_als::{dispatch_event_emitter_method, dispatch_event_emitter_property}; diff --git a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs index 48605b8c3e..0685887b2e 100644 --- a/crates/perry-stdlib/src/common/dispatch/emitter_als.rs +++ b/crates/perry-stdlib/src/common/dispatch/emitter_als.rs @@ -1,3 +1,4 @@ +use super::super::handle::*; use super::*; /// `js_class_method_bind` retains the name pointer in the closure, so make a @@ -46,88 +47,6 @@ fn async_local_storage_method_name_static(property: &str) -> Option<&'static [u8 } } -extern "C" fn async_local_storage_unbound_method_thunk( - closure: *const perry_runtime::closure::ClosureHeader, - rest: f64, -) -> f64 { - unsafe { - let name_ptr = perry_runtime::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; - 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 receiver_raw = if receiver.to_bits() >> 48 == 0x7FFD { - (receiver.to_bits() & POINTER_MASK_BITS) as i64 - } else { - 0 - }; - - // Node deliberately makes these two operations harmless when their - // method value is invoked without an ALS receiver. - if matches!(name, b"enterWith" | b"disable") - && crate::async_local_storage::resolve_async_local_storage_handle(receiver_raw) - .is_none() - { - return TAG_UNDEFINED_F64; - } - - let args_array = - perry_runtime::value::js_nanbox_get_pointer(rest) as *const perry_runtime::ArrayHeader; - let args = if args_array.is_null() { - Vec::new() - } else { - let len = perry_runtime::array::js_array_length(args_array) as usize; - (0..len) - .map(|index| { - f64::from_bits( - perry_runtime::array::js_array_get(args_array, index as u32).bits(), - ) - }) - .collect::>() - }; - if let Some(value) = dispatch_async_local_storage_method(receiver_raw, name_str, &args) { - return value; - } - - // The remaining cases are invalid receivers. Brand-checked methods - // throw; enterWith/disable already returned the deliberate no-op above. - match name { - b"getStore" => { - crate::async_local_storage::js_async_local_storage_get_store(receiver_raw) - } - b"run" => crate::async_local_storage::js_async_local_storage_run( - receiver_raw, - args.first().copied().unwrap_or(TAG_UNDEFINED_F64), - args.get(1).copied().unwrap_or(TAG_UNDEFINED_F64), - 0, - ), - b"exit" => crate::async_local_storage::js_async_local_storage_exit( - receiver_raw, - args.first().copied().unwrap_or(TAG_UNDEFINED_F64), - 0, - ), - _ => TAG_UNDEFINED_F64, - } - } -} - -pub(crate) fn unbound_async_local_storage_method(method: &'static [u8]) -> f64 { - perry_runtime::closure::js_register_closure_rest( - async_local_storage_unbound_method_thunk as *const u8, - 0, - ); - let closure = perry_runtime::closure::js_closure_alloc( - async_local_storage_unbound_method_thunk as *const u8, - 2, - ); - if closure.is_null() { - return TAG_UNDEFINED_F64; - } - perry_runtime::closure::js_closure_set_capture_ptr(closure, 0, method.as_ptr() as i64); - perry_runtime::closure::js_closure_set_capture_ptr(closure, 1, method.len() as i64); - perry_runtime::value::js_nanbox_pointer(closure as i64) -} - /// Dynamic dispatch for `AsyncLocalStorage` receivers whose static type the /// codegen lost (`any`-typed bindings, closure captures). Gated on registry /// type membership so no other subsystem's handle is claimed (#788). @@ -142,7 +61,9 @@ pub(crate) unsafe fn dispatch_async_local_storage_method( ) { return None; } - let handle = crate::async_local_storage::resolve_async_local_storage_handle(handle)?; + if get_handle_mut::(handle).is_none() { + return None; + } Some(match method { "getStore" => crate::async_local_storage::js_async_local_storage_get_store(handle), "run" if args.len() >= 2 => { @@ -198,21 +119,31 @@ pub(crate) unsafe fn dispatch_event_emitter_method( f64::from_bits(POINTER_TAG_BITS | (ptr as u64 & POINTER_MASK_BITS)) }; - if perry_runtime::object::event_emitter_async_resource_handle_probe() - .is_some_and(|probe| probe(handle)) - { - let operation = match method { - "asyncId" => Some(0), - "triggerAsyncId" => Some(1), - "asyncResource" => Some(2), - "emitDestroy" => Some(3), - _ => None, - }; - if let (Some(operation), Some(dispatch)) = ( - operation, - perry_runtime::object::event_emitter_async_resource_dispatch(), - ) { - return Some(dispatch(handle, operation)); + // EventEmitterAsyncResource extras exist only in the bundled impl; + // perry-ext-events has no async-resource constructor, so its handles + // never satisfy this probe. + #[cfg(feature = "bundled-events")] + if crate::events::is_event_emitter_async_resource_handle(handle) { + match method { + "asyncId" => { + return Some(crate::events::js_event_emitter_async_resource_async_id( + handle, + )); + } + "triggerAsyncId" => { + return Some( + crate::events::js_event_emitter_async_resource_trigger_async_id(handle), + ); + } + "asyncResource" => { + return Some(crate::events::js_event_emitter_async_resource_async_resource(handle)); + } + "emitDestroy" => { + return Some(crate::events::js_event_emitter_async_resource_emit_destroy( + handle, + )); + } + _ => {} } } @@ -277,23 +208,24 @@ pub(crate) unsafe fn dispatch_event_emitter_property(handle: i64, property: &str return None; } - if perry_runtime::object::event_emitter_async_resource_handle_probe() - .is_some_and(|probe| probe(handle)) - { - if property == "emitDestroy" { - return Some(bind_static_handle_method(handle, b"emitDestroy")); - } - let operation = match property { - "asyncId" => Some(0), - "triggerAsyncId" => Some(1), - "asyncResource" => Some(2), - _ => None, - }; - if let (Some(operation), Some(dispatch)) = ( - operation, - perry_runtime::object::event_emitter_async_resource_dispatch(), - ) { - return Some(dispatch(handle, operation)); + #[cfg(feature = "bundled-events")] + if crate::events::is_event_emitter_async_resource_handle(handle) { + match property { + "asyncId" => { + return Some(crate::events::js_event_emitter_async_resource_async_id( + handle, + )); + } + "triggerAsyncId" => { + return Some( + crate::events::js_event_emitter_async_resource_trigger_async_id(handle), + ); + } + "asyncResource" => { + return Some(crate::events::js_event_emitter_async_resource_async_resource(handle)); + } + "emitDestroy" => return Some(bind_static_handle_method(handle, b"emitDestroy")), + _ => {} } } @@ -317,8 +249,10 @@ pub(crate) unsafe fn dispatch_async_local_storage_property( property: &str, ) -> Option { let method = async_local_storage_method_name_static(property)?; - crate::async_local_storage::resolve_async_local_storage_handle(handle)?; - Some(unbound_async_local_storage_method(method)) + if get_handle_mut::(handle).is_none() { + return None; + } + Some(bind_static_handle_method(handle, method)) } #[cfg(test)] diff --git a/crates/perry-stdlib/src/common/dispatch/init.rs b/crates/perry-stdlib/src/common/dispatch/init.rs index 1cf933d6e4..0ecf9253ef 100644 --- a/crates/perry-stdlib/src/common/dispatch/init.rs +++ b/crates/perry-stdlib/src/common/dispatch/init.rs @@ -454,9 +454,6 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { fn js_register_event_emitter_async_resource_handle_probe( f: unsafe extern "C" fn(i64) -> bool, ); - fn js_register_event_emitter_async_resource_dispatch( - f: unsafe extern "C" fn(i64, u32) -> f64, - ); fn js_register_event_emitter_on(f: EventEmitterOn); #[cfg(feature = "web-fetch")] fn js_register_global_fetch_with_options( @@ -580,18 +577,6 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { } #[cfg(feature = "bundled-events")] js_register_event_emitter_async_resource_handle_probe(event_emitter_async_resource_probe); - #[cfg(feature = "bundled-events")] - unsafe extern "C" fn event_emitter_async_resource_dispatch(handle: i64, operation: u32) -> f64 { - match operation { - 0 => crate::events::js_event_emitter_async_resource_async_id(handle), - 1 => crate::events::js_event_emitter_async_resource_trigger_async_id(handle), - 2 => crate::events::js_event_emitter_async_resource_async_resource(handle), - 3 => crate::events::js_event_emitter_async_resource_emit_destroy(handle), - _ => TAG_UNDEFINED_F64, - } - } - #[cfg(feature = "bundled-events")] - js_register_event_emitter_async_resource_dispatch(event_emitter_async_resource_dispatch); #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] unsafe extern "C" fn event_emitter_on_hook( handle: i64, diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs index 53d7ae7ef8..3f9c4cd092 100644 --- a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -1,5 +1,4 @@ -#[cfg(any(feature = "crypto", feature = "database-redis"))] -use super::super::handle::with_handle; +use super::super::handle::*; use super::*; /// Route external zlib stream methods before the generic dispatcher creates @@ -116,7 +115,7 @@ unsafe fn try_dispatch_external_http_client( }; if !matches!( method_name, - "setEncoding" | "on" | "once" | "addListener" | "pipe" | "pause" | "resume" + "setEncoding" | "on" | "addListener" | "pipe" | "pause" | "resume" ) { return None; } diff --git a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs index 99758d79cd..4885ea9aad 100644 --- a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs @@ -1,5 +1,4 @@ -#[cfg(any(feature = "crypto", feature = "http-client"))] -use super::super::handle::with_handle; +use super::super::handle::*; use super::*; /// Dispatch a property access on a handle-based object. @@ -28,12 +27,6 @@ pub unsafe extern "C" fn js_handle_property_dispatch( return v; } - if let Some(value) = - perry_runtime::async_hooks::try_async_resource_property_dispatch(handle, property_name) - { - return value; - } - #[cfg(any(feature = "bundled-events", feature = "external-events-construct"))] if let Some(value) = dispatch_event_emitter_property(handle, property_name) { return value; diff --git a/crates/perry-stdlib/src/common/dispatch_http.rs b/crates/perry-stdlib/src/common/dispatch_http.rs index c72f963591..50ed08efa1 100644 --- a/crates/perry-stdlib/src/common/dispatch_http.rs +++ b/crates/perry-stdlib/src/common/dispatch_http.rs @@ -145,10 +145,7 @@ pub(super) unsafe fn dispatch_client_incoming_method( method_name: &str, args: &[f64], ) -> Option { - if !matches!( - method_name, - "setEncoding" | "on" | "once" | "addListener" | "pipe" - ) { + if !matches!(method_name, "setEncoding" | "on" | "addListener" | "pipe") { return None; } @@ -179,7 +176,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 { diff --git a/crates/perry-stdlib/src/crypto/kdf.rs b/crates/perry-stdlib/src/crypto/kdf.rs index 9194ff7788..38390c41a2 100644 --- a/crates/perry-stdlib/src/crypto/kdf.rs +++ b/crates/perry-stdlib/src/crypto/kdf.rs @@ -64,12 +64,7 @@ pub unsafe extern "C" fn js_crypto_pbkdf2_async_alg( } else { f64::from_bits(JSValue::pointer(buf as *const u8).bits()) }; - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - value, - "PBKDF2REQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), value); f64::from_bits(JSValue::undefined().bits()) } @@ -135,12 +130,7 @@ pub unsafe extern "C" fn js_crypto_hkdf_async_alg( } else { f64::from_bits(JSValue::pointer(buf as *const u8).bits()) }; - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - value, - "DERIVEBITSREQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), value); f64::from_bits(JSValue::undefined().bits()) } @@ -159,12 +149,7 @@ pub unsafe extern "C" fn js_crypto_scrypt_async( } else { f64::from_bits(JSValue::pointer(buf as *const u8).bits()) }; - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - value, - "SCRYPTREQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), value); f64::from_bits(JSValue::undefined().bits()) } @@ -339,12 +324,7 @@ pub unsafe extern "C" fn js_crypto_argon2_async( } else { f64::from_bits(JSValue::pointer(buf as *const u8).bits()) }; - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - value, - "ARGON2REQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), value); f64::from_bits(JSValue::undefined().bits()) } diff --git a/crates/perry-stdlib/src/crypto/keys.rs b/crates/perry-stdlib/src/crypto/keys.rs index 8ffa4087e0..211bc6846c 100644 --- a/crates/perry-stdlib/src/crypto/keys.rs +++ b/crates/perry-stdlib/src/crypto/keys.rs @@ -102,31 +102,12 @@ pub(super) unsafe fn call_node_style_callback2(callback_bits: f64, err: f64, val ); } -pub(super) unsafe fn schedule_node_style_callback2( - callback_bits: f64, - err: f64, - value: f64, - provider_type: &'static str, -) { +pub(super) unsafe fn call_node_style_callback3(callback_bits: f64, err: f64, a: f64, b: f64) { let raw = callback_bits.to_bits() & 0x0000_FFFF_FFFF_FFFF; - if !perry_runtime::closure::is_closure_ptr(raw as usize) { - return; - } - perry_runtime::timer::schedule_native_callback(raw as i64, &[err, value], provider_type); -} - -pub(super) unsafe fn schedule_node_style_callback3( - callback_bits: f64, - err: f64, - a: f64, - b: f64, - provider_type: &'static str, -) { - let raw = callback_bits.to_bits() & 0x0000_FFFF_FFFF_FFFF; - if !perry_runtime::closure::is_closure_ptr(raw as usize) { + if raw < 0x1000 { return; } - perry_runtime::timer::schedule_native_callback(raw as i64, &[err, a, b], provider_type); + perry_runtime::closure::js_closure_call3(raw as *const perry_runtime::ClosureHeader, err, a, b); } #[no_mangle] @@ -141,12 +122,7 @@ pub unsafe extern "C" fn js_crypto_generate_key_async( } else { f64::from_bits(JSValue::pointer(key as *const u8).bits()) }; - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - value, - "KEYGENREQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), value); f64::from_bits(JSValue::undefined().bits()) } @@ -166,25 +142,18 @@ pub unsafe extern "C" fn js_crypto_generate_key_pair_async( let null = f64::from_bits(JSValue::null().bits()); let undefined = f64::from_bits(JSValue::undefined().bits()); if pair.is_null() { - schedule_node_style_callback3( - callback_bits, - null, - undefined, - undefined, - "KEYPAIRGENREQUEST", - ); + call_node_style_callback3(callback_bits, null, undefined, undefined); return undefined; } let public_key = js_object_get_field_by_name(pair, js_string_from_bytes(b"publicKey".as_ptr(), 9)); let private_key = js_object_get_field_by_name(pair, js_string_from_bytes(b"privateKey".as_ptr(), 10)); - schedule_node_style_callback3( + call_node_style_callback3( callback_bits, null, f64::from_bits(public_key.bits()), f64::from_bits(private_key.bits()), - "KEYPAIRGENREQUEST", ); undefined } diff --git a/crates/perry-stdlib/src/crypto/prime.rs b/crates/perry-stdlib/src/crypto/prime.rs index 7612a77f82..1e09eef506 100644 --- a/crates/perry-stdlib/src/crypto/prime.rs +++ b/crates/perry-stdlib/src/crypto/prime.rs @@ -40,11 +40,10 @@ pub unsafe extern "C" fn js_crypto_generate_prime_async( callback_bits: f64, ) -> f64 { let value = js_crypto_generate_prime_sync(size_bits, options_bits); - schedule_node_style_callback2( + call_node_style_callback2( callback_bits, f64::from_bits(JSValue::undefined().bits()), value, - "RANDOMPRIMEREQUEST", ); f64::from_bits(JSValue::undefined().bits()) } @@ -56,11 +55,10 @@ pub unsafe extern "C" fn js_crypto_check_prime_async( callback_bits: f64, ) -> f64 { let result = js_crypto_check_prime_sync(candidate_bits, options_bits); - schedule_node_style_callback2( + call_node_style_callback2( callback_bits, f64::from_bits(JSValue::null().bits()), result, - "CHECKPRIMEREQUEST", ); f64::from_bits(JSValue::undefined().bits()) } diff --git a/crates/perry-stdlib/src/crypto/random.rs b/crates/perry-stdlib/src/crypto/random.rs index 9fc5773c11..7aeeaf4081 100644 --- a/crates/perry-stdlib/src/crypto/random.rs +++ b/crates/perry-stdlib/src/crypto/random.rs @@ -103,7 +103,7 @@ pub unsafe extern "C" fn js_crypto_random_bytes_async(size: f64, callback_bits: if perry_runtime::closure::is_closure_ptr(cb_ptr as usize) { let err = f64::from_bits(JSValue::null().bits()); let args = [err, value]; - perry_runtime::timer::schedule_native_callback(cb_ptr, &args, "RANDOMBYTESREQUEST"); + perry_runtime::timer::js_set_immediate_callback_args(cb_ptr, args.as_ptr(), 2); } f64::from_bits(JSValue::undefined().bits()) } @@ -373,24 +373,8 @@ pub unsafe extern "C" fn js_crypto_native_dispatch( f64::from_bits(JSValue::string_ptr(js_crypto_create_public_key_value(arg(0))).bits()) } "generatePrime" if args_len >= 3 => js_crypto_generate_prime_async(arg(0), arg(1), arg(2)), - "generatePrime" - if args_len == 2 - && perry_runtime::closure::is_closure_ptr( - perry_runtime::value::js_nanbox_get_pointer(arg(1)) as usize, - ) => - { - js_crypto_generate_prime_async(arg(0), undefined, arg(1)) - } "generatePrime" | "generatePrimeSync" => js_crypto_generate_prime_sync(arg(0), arg(1)), "checkPrime" if args_len >= 3 => js_crypto_check_prime_async(arg(0), arg(1), arg(2)), - "checkPrime" - if args_len == 2 - && perry_runtime::closure::is_closure_ptr( - perry_runtime::value::js_nanbox_get_pointer(arg(1)) as usize, - ) => - { - js_crypto_check_prime_async(arg(0), undefined, arg(1)) - } "checkPrime" | "checkPrimeSync" => js_crypto_check_prime_sync(arg(0), arg(1)), "getFips" => 0.0, "setFips" => undefined, @@ -443,17 +427,6 @@ pub unsafe extern "C" fn js_crypto_native_dispatch( js_crypto_scrypt_bytes(bytes_ptr(0), bytes_ptr(1), arg(2), options_ptr) as *mut u8, ) } - // Node callback forms are randomInt(max, callback) and - // randomInt(min, max, callback); both complete asynchronously. - "randomInt" if args_len >= 3 => js_crypto_random_int_async(arg(0), arg(1), arg(2)), - "randomInt" - if args_len == 2 - && perry_runtime::closure::is_closure_ptr( - perry_runtime::value::js_nanbox_get_pointer(arg(1)) as usize, - ) => - { - js_crypto_random_int_async(0.0, arg(0), arg(1)) - } // Node: randomInt(max) → [0,max); randomInt(min,max) → [min,max). "randomInt" if args_len >= 2 => js_crypto_random_int(arg(0), arg(1)), "randomInt" => js_crypto_random_int(0.0, arg(0)), @@ -474,12 +447,7 @@ pub unsafe extern "C" fn js_crypto_random_int_async( callback_bits: f64, ) -> f64 { let n = js_crypto_random_int(min_bits, max_bits); - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - n, - "RANDOMBYTESREQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), n); f64::from_bits(JSValue::undefined().bits()) } @@ -627,12 +595,7 @@ pub unsafe extern "C" fn js_crypto_random_fill_async( callback_bits: f64, ) -> f64 { let value = js_crypto_random_fill_sync(buf_bits, offset_bits, size_bits); - schedule_node_style_callback2( - callback_bits, - f64::from_bits(JSValue::null().bits()), - value, - "RANDOMBYTESREQUEST", - ); + call_node_style_callback2(callback_bits, f64::from_bits(JSValue::null().bits()), value); f64::from_bits(JSValue::undefined().bits()) } diff --git a/crates/perry-stdlib/src/events/constructors.rs b/crates/perry-stdlib/src/events/constructors.rs index f95362821b..9303592b11 100644 --- a/crates/perry-stdlib/src/events/constructors.rs +++ b/crates/perry-stdlib/src/events/constructors.rs @@ -76,12 +76,7 @@ pub unsafe extern "C" fn js_event_emitter_async_resource_new(options: f64) -> Ha let mut emitter = EventEmitterHandle::new(); emitter.capture_rejections = event_emitter_options_capture_rejections(options); emitter.async_resource_handle = async_resource_handle; - let emitter_handle = register_handle(emitter); - perry_runtime::async_hooks::set_async_resource_event_emitter( - async_resource_handle, - emitter_handle, - ); - emitter_handle + register_handle(emitter) } #[no_mangle] @@ -161,15 +156,3 @@ static KEEP_JS_EVENT_EMITTER_NEW_WITH_OPTIONS: unsafe extern "C" fn(f64) -> Hand #[used] static KEEP_JS_EVENT_EMITTER_ASYNC_RESOURCE_NEW: unsafe extern "C" fn(f64) -> Handle = js_event_emitter_async_resource_new; -#[used] -static KEEP_JS_EVENT_EMITTER_ASYNC_RESOURCE_ASYNC_ID: unsafe extern "C" fn(Handle) -> f64 = - js_event_emitter_async_resource_async_id; -#[used] -static KEEP_JS_EVENT_EMITTER_ASYNC_RESOURCE_TRIGGER_ASYNC_ID: unsafe extern "C" fn(Handle) -> f64 = - js_event_emitter_async_resource_trigger_async_id; -#[used] -static KEEP_JS_EVENT_EMITTER_ASYNC_RESOURCE_ASYNC_RESOURCE: unsafe extern "C" fn(Handle) -> f64 = - js_event_emitter_async_resource_async_resource; -#[used] -static KEEP_JS_EVENT_EMITTER_ASYNC_RESOURCE_EMIT_DESTROY: unsafe extern "C" fn(Handle) -> f64 = - js_event_emitter_async_resource_emit_destroy; diff --git a/crates/perry-stdlib/src/fetch/mod.rs b/crates/perry-stdlib/src/fetch/mod.rs index 95058914aa..d03d4c0a31 100644 --- a/crates/perry-stdlib/src/fetch/mod.rs +++ b/crates/perry-stdlib/src/fetch/mod.rs @@ -1391,13 +1391,6 @@ pub unsafe extern "C" fn js_blob_type(handle: f64) -> *mut StringHeader { /// property dispatch in `value.rs`. Resolved synchronously. #[no_mangle] pub unsafe extern "C" fn js_blob_array_buffer(handle: f64) -> *mut perry_runtime::Promise { - let result = perry_runtime::async_hooks::run_provider_completion("BLOBREADER", || { - perry_runtime::value::js_nanbox_pointer(blob_array_buffer_impl(handle) as i64) - }); - perry_runtime::value::js_nanbox_get_pointer(result) as *mut perry_runtime::Promise -} - -unsafe fn blob_array_buffer_impl(handle: f64) -> *mut perry_runtime::Promise { let promise = perry_runtime::js_promise_new(); let id = handle_id(handle); let body: Vec = BLOB_REGISTRY @@ -1425,10 +1418,7 @@ unsafe fn blob_array_buffer_impl(handle: f64) -> *mut perry_runtime::Promise { /// hits the `is_registered_buffer` path from #227). #[no_mangle] pub unsafe extern "C" fn js_blob_bytes(handle: f64) -> *mut perry_runtime::Promise { - let result = perry_runtime::async_hooks::run_provider_completion("BLOBREADER", || { - perry_runtime::value::js_nanbox_pointer(blob_array_buffer_impl(handle) as i64) - }); - perry_runtime::value::js_nanbox_get_pointer(result) as *mut perry_runtime::Promise + js_blob_array_buffer(handle) } /// blob.text() — UTF-8-decodes the body bytes into a `StringHeader` and @@ -1437,13 +1427,6 @@ pub unsafe extern "C" fn js_blob_bytes(handle: f64) -> *mut perry_runtime::Promi /// characters; lossy_utf8 produces U+FFFD identically). #[no_mangle] pub unsafe extern "C" fn js_blob_text(handle: f64) -> *mut perry_runtime::Promise { - let result = perry_runtime::async_hooks::run_provider_completion("BLOBREADER", || { - perry_runtime::value::js_nanbox_pointer(blob_text_impl(handle) as i64) - }); - perry_runtime::value::js_nanbox_get_pointer(result) as *mut perry_runtime::Promise -} - -unsafe fn blob_text_impl(handle: f64) -> *mut perry_runtime::Promise { let promise = perry_runtime::js_promise_new(); let id = handle_id(handle); let body: Vec = BLOB_REGISTRY diff --git a/crates/perry-stdlib/src/readline/mod.rs b/crates/perry-stdlib/src/readline/mod.rs index ca7bdeee27..4b93af6a9c 100644 --- a/crates/perry-stdlib/src/readline/mod.rs +++ b/crates/perry-stdlib/src/readline/mod.rs @@ -615,18 +615,16 @@ fn close_custom_interface(handle: i64) { .flatten(); if let Some(cb_i64) = cb { js_closure_call0(cb_i64 as *const ClosureHeader); - // Release the slot once the close notification has an observer. If the - // custom stream completed before user code could attach `rl.on` - // listeners, retain the closed state temporarily; `js_readline_on` - // replays the buffered lines and close below. This compensates for - // Perry's native-call checkpoint draining Readable.from microtasks - // between adjacent JS statements (#6764). - READLINE_INTERFACES.with(|interfaces| { - if let Some(slot) = interfaces.borrow_mut().get_mut(handle as usize) { - *slot = None; - } - }); } + // Release the slot so the GC scanner stops rooting the closed + // interface's input/output/callbacks. Handles are NOT reused: a stale + // handle to a closed interface must hit the `None` slot (a no-op, like + // Node's ERR_USE_AFTER_CLOSE), not alias a newer interface. + READLINE_INTERFACES.with(|interfaces| { + if let Some(slot) = interfaces.borrow_mut().get_mut(handle as usize) { + *slot = None; + } + }); } fn append_custom_input(handle: i64, chunk: f64) { @@ -667,10 +665,6 @@ fn append_custom_input(handle: i64, chunk: f64) { .flatten(); if let Some(cb_i64) = cb { js_closure_call1(cb_i64 as *const ClosureHeader, callback_arg(&line)); - } else { - let _ = with_interface_mut(handle, |state| { - state.buffered_lines.push_back(line); - }); } } } @@ -869,13 +863,6 @@ fn attach_custom_input(handle: i64, input: f64) { if raw_ptr_from_value(input).is_none() { return; } - // These are native listener closures, so the generic stream emitter needs - // their public arities before it can select call0/call1 correctly. Without - // registration Readable.from-backed interfaces retained the callbacks but - // never delivered `data`/`end`, leaving the top-level readline Promise - // unsettled. - perry_runtime::closure::js_register_closure_arity(custom_input_data as *const u8, 1); - perry_runtime::closure::js_register_closure_arity(custom_input_close as *const u8, 0); // Root every value built here: each later closure/string allocation (and // the JS `.on` calls below) can trigger a moving minor GC, leaving an // unrooted listener pointer in from-space. Re-read handles at each use. @@ -1312,51 +1299,19 @@ pub extern "C" fn js_readline_on( return undefined(); } let event = string_header_to_string(event_ptr); - let mut replay_lines = false; - let mut replay_close = false; if with_interface_mut(handle, |state| { if !state.uses_custom_stream { return false; } match event.as_str() { - "line" => { - state.line_callback = Some(callback); - replay_lines = !state.buffered_lines.is_empty(); - } - "close" => { - state.close_callback = Some(callback); - replay_close = state.closed; - } + "line" => state.line_callback = Some(callback), + "close" => state.close_callback = Some(callback), _ => {} } true }) .unwrap_or(false) { - if replay_lines { - loop { - let next = - with_interface_mut(handle, |state| state.buffered_lines.pop_front()).flatten(); - let Some(line) = next else { - break; - }; - let cb = with_interface(handle, |state| state.line_callback).flatten(); - if let Some(cb_i64) = cb { - js_closure_call1(cb_i64 as *const ClosureHeader, callback_arg(&line)); - } - } - } - if replay_close { - let cb = with_interface(handle, |state| state.close_callback).flatten(); - if let Some(cb_i64) = cb { - js_closure_call0(cb_i64 as *const ClosureHeader); - } - READLINE_INTERFACES.with(|interfaces| { - if let Some(slot) = interfaces.borrow_mut().get_mut(handle as usize) { - *slot = None; - } - }); - } return undefined(); } match event.as_str() { @@ -1724,13 +1679,113 @@ pub use pump::{js_readline_has_active, js_readline_process_pending}; // Tests // --------------------------------------------------------------------------- +/// Test-only helper: bypass the stdin reader and inject a line into the +/// queue. +#[doc(hidden)] #[cfg(test)] -mod test_support; +fn test_inject_line(line: &str) { + PENDING_LINES.lock().unwrap().push(line.to_string()); +} + +#[doc(hidden)] +#[cfg(test)] +fn test_inject_chunk(chunk: &[u8]) { + PENDING_DATA.lock().unwrap().push(chunk.to_vec()); +} #[cfg(test)] mod tests { - use super::test_support::*; use super::*; + use std::sync::{Mutex, MutexGuard}; + + /// All readline tests share PENDING_LINES / PENDING_DATA / EOF_REACHED + /// / CLOSE_FIRED / RAW_MODE / the thread_local callback cells, so + /// `cargo test`'s default parallel runner races them — most visibly, + /// `has_active_reflects_state`'s `test_inject_line("x")` → + /// `assert has_active == 1` window can be observed mid-flight by + /// `injected_line_drains_via_test_helper`'s `reset()` (which clears + /// PENDING_LINES) and flake. Serialize every state-touching test + /// through one process-global lock acquired by `reset()`. Pure- + /// function tests (`parse_keypress_*`) don't call `reset()` and + /// continue running in parallel. + static TEST_LOCK: Mutex<()> = Mutex::new(()); + thread_local! { + static DATA_COUNT: RefCell = const { RefCell::new(0) }; + static KEYPRESS_NAMES: RefCell> = const { RefCell::new(Vec::new()) }; + } + + extern "C" fn count_data_callback(_closure: *const ClosureHeader, _chunk: f64) -> f64 { + DATA_COUNT.with(|count| *count.borrow_mut() += 1); + undefined() + } + + fn data_counter_callback() -> i64 { + js_closure_alloc(count_data_callback as *const u8, 0) as i64 + } + + extern "C" fn count_readable_callback(_closure: *const ClosureHeader) -> f64 { + DATA_COUNT.with(|count| *count.borrow_mut() += 1); + undefined() + } + + fn readable_counter_callback() -> i64 { + js_closure_alloc(count_readable_callback as *const u8, 0) as i64 + } + + extern "C" fn record_keypress_callback( + _closure: *const ClosureHeader, + _seq: f64, + key_obj: f64, + ) -> f64 { + let name = object_field(key_obj, b"name") + .map(value_to_string) + .unwrap_or_default(); + KEYPRESS_NAMES.with(|names| names.borrow_mut().push(name)); + undefined() + } + + fn keypress_recorder_callback() -> i64 { + js_closure_alloc(record_keypress_callback as *const u8, 0) as i64 + } + + fn event_name(name: &str) -> *mut StringHeader { + js_string_from_bytes(name.as_ptr(), name.len() as u32) + } + + fn reset() -> MutexGuard<'static, ()> { + let guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + DATA_COUNT.with(|count| *count.borrow_mut() = 0); + KEYPRESS_NAMES.with(|names| names.borrow_mut().clear()); + QUESTION_CALLBACK.with(|c| *c.borrow_mut() = None); + LINE_CALLBACK.with(|c| *c.borrow_mut() = None); + CLOSE_CALLBACK.with(|c| *c.borrow_mut() = None); + if let Ok(mut v) = DATA_CALLBACKS.lock() { + v.clear(); + } + if let Ok(mut v) = KEYPRESS_CALLBACKS.lock() { + v.clear(); + } + if let Ok(mut v) = READABLE_CALLBACKS.lock() { + v.clear(); + } + PENDING_LINES.lock().unwrap().clear(); + PENDING_DATA.lock().unwrap().clear(); + PENDING_ESCAPE.lock().unwrap().clear(); + EOF_REACHED.store(false, Ordering::Release); + READABLE_EOF_NOTIFIED.store(false, Ordering::Release); + STDIN_PAUSED.store(false, Ordering::Release); + STDIN_REFED.store(true, Ordering::Release); + STDIN_DESTROYED.store(false, Ordering::Release); + CLOSE_FIRED.with(|f| *f.borrow_mut() = false); + RAW_MODE.store(false, Ordering::Release); + STDIN_DATA_FLOWING.store(false, Ordering::Release); + READLINE_INTERFACES.with(|interfaces| interfaces.borrow_mut().clear()); + NEXT_READLINE_HANDLE.with(|next| *next.borrow_mut() = 2); + // ensure_reader_started never spawns a real thread under cfg(test), + // so the started flag is safe to clear between tests. + READER_STARTED.store(false, Ordering::Release); + guard + } #[test] fn close_without_callbacks_is_noop() { diff --git a/crates/perry-stdlib/src/readline/test_support.rs b/crates/perry-stdlib/src/readline/test_support.rs deleted file mode 100644 index 39295b7e76..0000000000 --- a/crates/perry-stdlib/src/readline/test_support.rs +++ /dev/null @@ -1,87 +0,0 @@ -use super::*; -use std::sync::{Mutex, MutexGuard}; - -static TEST_LOCK: Mutex<()> = Mutex::new(()); -thread_local! { - pub(super) static DATA_COUNT: RefCell = const { RefCell::new(0) }; - pub(super) static KEYPRESS_NAMES: RefCell> = const { RefCell::new(Vec::new()) }; -} - -pub(super) fn test_inject_line(line: &str) { - PENDING_LINES.lock().unwrap().push(line.to_string()); -} - -pub(super) fn test_inject_chunk(chunk: &[u8]) { - PENDING_DATA.lock().unwrap().push(chunk.to_vec()); -} - -extern "C" fn count_data_callback(_closure: *const ClosureHeader, _chunk: f64) -> f64 { - DATA_COUNT.with(|count| *count.borrow_mut() += 1); - undefined() -} - -pub(super) fn data_counter_callback() -> i64 { - js_closure_alloc(count_data_callback as *const u8, 0) as i64 -} - -extern "C" fn count_readable_callback(_closure: *const ClosureHeader) -> f64 { - DATA_COUNT.with(|count| *count.borrow_mut() += 1); - undefined() -} - -pub(super) fn readable_counter_callback() -> i64 { - js_closure_alloc(count_readable_callback as *const u8, 0) as i64 -} - -extern "C" fn record_keypress_callback( - _closure: *const ClosureHeader, - _seq: f64, - key_obj: f64, -) -> f64 { - let name = object_field(key_obj, b"name") - .map(value_to_string) - .unwrap_or_default(); - KEYPRESS_NAMES.with(|names| names.borrow_mut().push(name)); - undefined() -} - -pub(super) fn keypress_recorder_callback() -> i64 { - js_closure_alloc(record_keypress_callback as *const u8, 0) as i64 -} - -pub(super) fn event_name(name: &str) -> *mut StringHeader { - js_string_from_bytes(name.as_ptr(), name.len() as u32) -} - -pub(super) fn reset() -> MutexGuard<'static, ()> { - let guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - DATA_COUNT.with(|count| *count.borrow_mut() = 0); - KEYPRESS_NAMES.with(|names| names.borrow_mut().clear()); - QUESTION_CALLBACK.with(|c| *c.borrow_mut() = None); - LINE_CALLBACK.with(|c| *c.borrow_mut() = None); - CLOSE_CALLBACK.with(|c| *c.borrow_mut() = None); - if let Ok(mut v) = DATA_CALLBACKS.lock() { - v.clear(); - } - if let Ok(mut v) = KEYPRESS_CALLBACKS.lock() { - v.clear(); - } - if let Ok(mut v) = READABLE_CALLBACKS.lock() { - v.clear(); - } - PENDING_LINES.lock().unwrap().clear(); - PENDING_DATA.lock().unwrap().clear(); - PENDING_ESCAPE.lock().unwrap().clear(); - EOF_REACHED.store(false, Ordering::Release); - READABLE_EOF_NOTIFIED.store(false, Ordering::Release); - STDIN_PAUSED.store(false, Ordering::Release); - STDIN_REFED.store(true, Ordering::Release); - STDIN_DESTROYED.store(false, Ordering::Release); - CLOSE_FIRED.with(|f| *f.borrow_mut() = false); - RAW_MODE.store(false, Ordering::Release); - STDIN_DATA_FLOWING.store(false, Ordering::Release); - READLINE_INTERFACES.with(|interfaces| interfaces.borrow_mut().clear()); - NEXT_READLINE_HANDLE.with(|next| *next.borrow_mut() = 2); - READER_STARTED.store(false, Ordering::Release); - guard -} diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index 5361d60715..2b600de645 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -29,17 +29,12 @@ const TLS_DISPATCH_MISSING_BITS: u64 = TAG_UNDEFINED_BITS; mod client_verifier; mod dispatch; -mod event_pump; mod module_api; mod socket_api; // Re-export the handle-dispatch and module-level entry points so // `crate::tls::…` (and the `pub use tls::*` glob in `lib.rs`) keep resolving // them exactly as before the split. pub use dispatch::{dispatch_tls_handle, dispatch_tls_property, should_dispatch_tls_handle}; -pub use event_pump::{ - is_tls_server_handle, is_tls_socket_handle, js_tls_has_active_handles, js_tls_process_pending, - record_tls_client_handle, -}; pub use module_api::{ js_tls_check_server_identity, js_tls_convert_alpn_protocols, js_tls_create_secure_context, js_tls_get_ca_certificates, js_tls_get_ciphers, js_tls_native_dispatch, @@ -1843,38 +1838,162 @@ pub unsafe extern "C" fn js_tls_server_set_ticket_keys(handle: i64, value_bits: } } -// TLS methods are called only by symbols emitted into generated object files. -// Keep every public FFI entry point in the auto-optimized stdlib archive so -// whole-program LTO cannot discard them before the generated object is linked. -struct KeepTlsFfi( - #[allow(dead_code)] [*const (); N], // link-time keepalive anchor; field never read -); -// SAFETY: the pointers are retained for linking only and are never read or -// dereferenced, so sharing the static anchor between threads is sound. -unsafe impl Sync for KeepTlsFfi {} -#[used] -static KEEP_TLS_FFI: KeepTlsFfi<23> = KeepTlsFfi([ - js_tls_create_server as *const (), - js_tls_tlssocket_constructor as *const (), - js_tls_server_listen as *const (), - js_tls_server_close as *const (), - js_tls_server_address as *const (), - js_tls_server_on as *const (), - js_tls_server_once as *const (), - js_tls_server_remove_listener as *const (), - js_tls_server_remove_all_listeners as *const (), - js_tls_server_listener_count as *const (), - js_tls_server_event_names as *const (), - js_tls_server_set_secure_context as *const (), - js_tls_server_get_ticket_keys as *const (), - js_tls_server_set_ticket_keys as *const (), - js_tls_socket_get_protocol as *const (), - js_tls_socket_get_cipher as *const (), - js_tls_socket_get_peer_certificate as *const (), - js_tls_socket_get_certificate as *const (), - js_tls_socket_get_session as *const (), - js_tls_socket_is_session_reused as *const (), - js_tls_socket_export_keying_material as *const (), - js_tls_socket_set_max_send_fragment as *const (), - js_tls_process_pending as *const (), -]); +pub fn record_tls_client_handle(handle: i64) { + if handle <= 0 { + return; + } + crate::common::async_bridge::ensure_pump_registered(); + ensure_tls_gc_scanner_registered(); + if !perry_runtime::tls::is_tls_client_handle(handle) { + unsafe { + perry_runtime::tls::js_tls_client_record_start( + handle, + undefined(), + std::ptr::null(), + 0, + ); + } + } +} + +pub fn is_tls_server_handle(handle: i64) -> bool { + servers().lock().unwrap().contains_key(&handle) +} + +pub fn is_tls_socket_handle(handle: i64) -> bool { + sockets().lock().unwrap().contains_key(&handle) + || perry_runtime::tls::is_tls_client_handle(handle) +} + +#[no_mangle] +pub unsafe extern "C" fn js_tls_process_pending() -> i32 { + let mut events = { + let mut pending = pending_events().lock().unwrap(); + std::mem::take(&mut *pending) + }; + let count = events.len() as i32; + for event in events.drain(..) { + match event { + PendingTlsEvent::ServerListening(server_id) => { + let callbacks = { + let mut all = listeners().lock().unwrap(); + all.get_mut(&server_id) + .and_then(|per| per.remove("listening")) + .unwrap_or_default() + }; + for cb in callbacks { + if cb != 0 { + js_closure_call0(cb as *const ClosureHeader); + } + } + drain_once_listeners(server_id, "listening"); + } + 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); + } + } + drain_once_listeners(server_id, event_name); + } + } + PendingTlsEvent::ServerClose(server_id) => { + let callbacks = { + let mut all = listeners().lock().unwrap(); + all.get_mut(&server_id) + .and_then(|per| per.remove("close")) + .unwrap_or_default() + }; + for cb in callbacks { + if cb != 0 { + js_closure_call0(cb as *const ClosureHeader); + } + } + servers().lock().unwrap().remove(&server_id); + listeners().lock().unwrap().remove(&server_id); + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + sockets().lock().unwrap().remove(&socket_id); + listeners().lock().unwrap().remove(&socket_id); + 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); + } + } + drain_once_listeners(socket_id, "error"); + } + } + } + count +} + +pub fn js_tls_has_active_handles() -> i32 { + if !pending_events().lock().unwrap().is_empty() { + return 1; + } + if servers() + .lock() + .unwrap() + .values() + .any(|server| server.listening || (server.closing && server.active_connections > 0)) + { + return 1; + } + if sockets() + .lock() + .unwrap() + .values() + .any(|s| s.server_side && s.cmd_tx.is_some()) + { + return 1; + } + 0 +} diff --git a/crates/perry-stdlib/src/tls/event_pump.rs b/crates/perry-stdlib/src/tls/event_pump.rs deleted file mode 100644 index 114cddbf5f..0000000000 --- a/crates/perry-stdlib/src/tls/event_pump.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Main-thread TLS event delivery and active-handle accounting. - -use super::*; - -pub fn record_tls_client_handle(handle: i64) { - if handle <= 0 { - return; - } - crate::common::async_bridge::ensure_pump_registered(); - ensure_tls_gc_scanner_registered(); - if !perry_runtime::tls::is_tls_client_handle(handle) { - unsafe { - perry_runtime::tls::js_tls_client_record_start( - handle, - undefined(), - std::ptr::null(), - 0, - ); - } - } -} - -pub fn is_tls_server_handle(handle: i64) -> bool { - servers().lock().unwrap().contains_key(&handle) -} - -pub fn is_tls_socket_handle(handle: i64) -> bool { - sockets().lock().unwrap().contains_key(&handle) - || perry_runtime::tls::is_tls_client_handle(handle) -} - -#[no_mangle] -pub unsafe extern "C" fn js_tls_process_pending() -> i32 { - let mut events = { - let mut pending = pending_events().lock().unwrap(); - std::mem::take(&mut *pending) - }; - let count = events.len() as i32; - for event in events.drain(..) { - match event { - PendingTlsEvent::ServerListening(server_id) => { - let callbacks = { - let mut all = listeners().lock().unwrap(); - all.get_mut(&server_id) - .and_then(|per| per.remove("listening")) - .unwrap_or_default() - }; - for cb in callbacks { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); - } - } - drain_once_listeners(server_id, "listening"); - } - 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); - } - } - drain_once_listeners(server_id, event_name); - } - } - PendingTlsEvent::ServerClose(server_id) => { - let callbacks = { - let mut all = listeners().lock().unwrap(); - all.get_mut(&server_id) - .and_then(|per| per.remove("close")) - .unwrap_or_default() - }; - for cb in callbacks { - if cb != 0 { - js_closure_call0(cb as *const ClosureHeader); - } - } - servers().lock().unwrap().remove(&server_id); - listeners().lock().unwrap().remove(&server_id); - 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); - } - } - 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); - } - } - 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); - } - } - 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); - } - } - 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); - } - } - sockets().lock().unwrap().remove(&socket_id); - listeners().lock().unwrap().remove(&socket_id); - 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); - } - } - drain_once_listeners(socket_id, "error"); - } - } - } - count -} - -pub fn js_tls_has_active_handles() -> i32 { - if !pending_events().lock().unwrap().is_empty() { - return 1; - } - if servers() - .lock() - .unwrap() - .values() - .any(|server| server.listening || (server.closing && server.active_connections > 0)) - { - return 1; - } - if sockets() - .lock() - .unwrap() - .values() - .any(|s| s.server_side && s.cmd_tx.is_some()) - { - return 1; - } - 0 -} diff --git a/crates/perry-stdlib/src/webcrypto/aes.rs b/crates/perry-stdlib/src/webcrypto/aes.rs index 27af9d877a..290b4e002c 100644 --- a/crates/perry-stdlib/src/webcrypto/aes.rs +++ b/crates/perry-stdlib/src/webcrypto/aes.rs @@ -763,7 +763,7 @@ pub unsafe extern "C" fn js_webcrypto_encrypt( Some(c) => c, None => return reject_with_dom_exception("OperationError", "The operation failed"), }; - return resolve_with_bytes_provider(&ciphertext, "CIPHERREQUEST"); + return resolve_with_bytes(&ciphertext); } if algo_name.eq_ignore_ascii_case("AES-CBC") { let key_addr = strip_ptr(key_bits.to_bits()); @@ -801,7 +801,7 @@ pub unsafe extern "C" fn js_webcrypto_encrypt( Some(c) => c, None => return reject_with_dom_exception("OperationError", "The operation failed"), }; - return resolve_with_bytes_provider(&ciphertext, "CIPHERREQUEST"); + return resolve_with_bytes(&ciphertext); } if algo_name.eq_ignore_ascii_case("AES-CTR") { let key_addr = strip_ptr(key_bits.to_bits()); @@ -839,7 +839,7 @@ pub unsafe extern "C" fn js_webcrypto_encrypt( Some(c) => c, None => return reject_with_dom_exception("OperationError", "The operation failed"), }; - return resolve_with_bytes_provider(&ciphertext, "CIPHERREQUEST"); + return resolve_with_bytes(&ciphertext); } if algo_name.eq_ignore_ascii_case("ChaCha20-Poly1305") { let key_addr = strip_ptr(key_bits.to_bits()); @@ -877,7 +877,7 @@ pub unsafe extern "C" fn js_webcrypto_encrypt( Some(c) => c, None => return reject_with_dom_exception("OperationError", "The operation failed"), }; - return resolve_with_bytes_provider(&ciphertext, "CIPHERREQUEST"); + return resolve_with_bytes(&ciphertext); } if algo_name.eq_ignore_ascii_case("AES-OCB") { let key_addr = strip_ptr(key_bits.to_bits()); @@ -915,7 +915,7 @@ pub unsafe extern "C" fn js_webcrypto_encrypt( Some(c) => c, None => return reject_with_dom_exception("OperationError", "The operation failed"), }; - return resolve_with_bytes_provider(&ciphertext, "CIPHERREQUEST"); + return resolve_with_bytes(&ciphertext); } if !algo_name.eq_ignore_ascii_case("AES-GCM") { return reject_with_dom_exception("NotSupportedError", "Unrecognized algorithm name"); @@ -949,7 +949,7 @@ pub unsafe extern "C" fn js_webcrypto_encrypt( Some(c) => c, None => return reject_with_dom_exception("OperationError", "The operation failed"), }; - resolve_with_bytes_provider(&ciphertext, "CIPHERREQUEST") + resolve_with_bytes(&ciphertext) } /// `crypto.subtle.decrypt({ name: "AES-GCM", iv, additionalData? }, key, data)` diff --git a/crates/perry-stdlib/src/webcrypto/digest.rs b/crates/perry-stdlib/src/webcrypto/digest.rs index 235483ca7d..ddb549338c 100644 --- a/crates/perry-stdlib/src/webcrypto/digest.rs +++ b/crates/perry-stdlib/src/webcrypto/digest.rs @@ -60,6 +60,6 @@ pub unsafe extern "C" fn js_webcrypto_digest(algo_bits: f64, data_bits: f64) -> perry_runtime::closure::js_closure_set_capture_ptr(cl, 1, value.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"); + perry_runtime::timer::js_set_immediate_callback(cl as i64); promise } diff --git a/crates/perry-stdlib/src/webcrypto/hmac.rs b/crates/perry-stdlib/src/webcrypto/hmac.rs index ac7b6fc395..2cc75ec999 100644 --- a/crates/perry-stdlib/src/webcrypto/hmac.rs +++ b/crates/perry-stdlib/src/webcrypto/hmac.rs @@ -204,7 +204,7 @@ pub unsafe extern "C" fn js_webcrypto_sign( } else { return reject_with_dom_exception("NotSupportedError", "Unrecognized algorithm name"); }; - resolve_with_bytes_provider(&sig, "SIGNREQUEST") + resolve_with_bytes(&sig) } /// `crypto.subtle.verify(algorithm, key, signature, data)` → Promise @@ -431,7 +431,7 @@ pub unsafe extern "C" fn js_webcrypto_verify( } else { return reject_with_dom_exception("NotSupportedError", "Unrecognized algorithm name"); }; - resolve_with_bool_provider(ok, "SIGNREQUEST") + resolve_with_bool(ok) } /// Algorithm-arg coercion shared by sign / verify: accepts a string diff --git a/crates/perry-stdlib/src/webcrypto/util.rs b/crates/perry-stdlib/src/webcrypto/util.rs index 258a90bebb..95c440ccf3 100644 --- a/crates/perry-stdlib/src/webcrypto/util.rs +++ b/crates/perry-stdlib/src/webcrypto/util.rs @@ -916,16 +916,6 @@ pub(super) fn resolve_with_bits(bits: u64) -> *mut Promise { js_promise_resolved(f64::from_bits(bits)) } -pub(super) fn resolve_with_bits_provider(bits: u64, provider_type: &'static str) -> *mut Promise { - let scope = perry_runtime::gc::RuntimeHandleScope::new(); - let value = scope.root_nanbox_f64(f64::from_bits(bits)); - let result = perry_runtime::async_hooks::run_provider_completion(provider_type, || { - let promise = js_promise_resolved(value.get_nanbox_f64()); - f64::from_bits(JSValue::pointer(promise as *const u8).bits()) - }); - perry_runtime::value::js_nanbox_get_pointer(result) as *mut Promise -} - /// Construct a DOMException and return a rejected Promise carrying it. pub(super) unsafe fn reject_with_dom_exception(name: &str, message: &str) -> *mut Promise { let name_str = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32); @@ -950,35 +940,11 @@ pub(super) unsafe fn resolve_with_bytes(bytes: &[u8]) -> *mut Promise { resolve_with_bits(val) } -pub(super) unsafe fn resolve_with_bytes_provider( - bytes: &[u8], - provider_type: &'static str, -) -> *mut Promise { - let buf = alloc_uint8array_from_slice(bytes); - if buf.is_null() { - return reject_with_dom_exception("OperationError", "The operation failed"); - } - let val = JSValue::pointer(buf as *const u8).bits(); - 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, -) -> *mut Promise { - let bits = if b { - JSValue::bool(true).bits() - } else { - JSValue::bool(false).bits() - }; - resolve_with_bits_provider(bits, provider_type) -} - pub(super) fn compute_digest(algo: HashAlgo, data: &[u8]) -> Vec { match algo { HashAlgo::Sha1 => Sha1::digest(data).to_vec(), diff --git a/crates/perry-stdlib/src/worker_threads.rs b/crates/perry-stdlib/src/worker_threads.rs index d434249e9b..3b94aff3fb 100644 --- a/crates/perry-stdlib/src/worker_threads.rs +++ b/crates/perry-stdlib/src/worker_threads.rs @@ -208,8 +208,6 @@ struct WorkerRecord { alive: bool, refed: bool, terminate_promise: Option, - async_resources: [perry_runtime::async_hooks::AsyncResourceIds; 3], - async_resource_bits: [u64; 3], } struct WorkerListener { @@ -303,9 +301,6 @@ fn scan_worker_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_> if let Some(promise) = worker.terminate_promise.as_mut() { visitor.visit_usize_slot(promise); } - for resource in &mut worker.async_resource_bits { - visitor.visit_nanbox_u64_slot(resource); - } } } } @@ -1198,31 +1193,6 @@ pub extern "C" fn js_worker_threads_worker_new(entry_ptr: i64, options: f64) -> }; let (tx, rx) = mpsc::channel::(); let worker_obj = worker_object(worker_id, &options_state); - let resource_scope = perry_runtime::gc::RuntimeHandleScope::new(); - let mut async_resources = [perry_runtime::async_hooks::AsyncResourceIds { - async_id: 0, - trigger_async_id: 0, - }; 3]; - let mut resource_handles = Vec::with_capacity(3); - for (index, type_name) in ["WORKER", "MESSAGEPORT", "MESSAGEPORT"] - .into_iter() - .enumerate() - { - let resource = perry_runtime::object::js_object_alloc_null_proto(0, 0); - let resource_handle = resource_scope - .root_nanbox_f64(perry_runtime::value::js_nanbox_pointer(resource as i64)); - async_resources[index] = perry_runtime::async_hooks::init_resource( - type_name, - resource_handle.get_nanbox_f64(), - true, - ); - resource_handles.push(resource_handle); - } - let async_resource_bits = [ - resource_handles[0].get_nanbox_f64().to_bits(), - resource_handles[1].get_nanbox_f64().to_bits(), - resource_handles[2].get_nanbox_f64().to_bits(), - ]; WORKERS.lock().unwrap().insert( worker_id, WorkerRecord { @@ -1232,8 +1202,6 @@ pub extern "C" fn js_worker_threads_worker_new(entry_ptr: i64, options: f64) -> alive: true, refed: true, terminate_promise: None, - async_resources, - async_resource_bits, }, ); diff --git a/crates/perry-stdlib/src/worker_threads/worker_pump.rs b/crates/perry-stdlib/src/worker_threads/worker_pump.rs index 814b3407a2..994ac9613b 100644 --- a/crates/perry-stdlib/src/worker_threads/worker_pump.rs +++ b/crates/perry-stdlib/src/worker_threads/worker_pump.rs @@ -127,22 +127,14 @@ pub extern "C" fn js_worker_threads_process_pending() -> i32 { processed += 1; } WorkerEvent::Exit(worker_id, code) => { - let (terminate_promise, async_resources) = + let terminate_promise = if let Some(worker) = WORKERS.lock().unwrap().get_mut(&worker_id) { worker.alive = false; - ( - worker.terminate_promise.take(), - Some(worker.async_resources), - ) + worker.terminate_promise.take() } else { - (None, None) + None }; dispatch_worker_event(worker_id, "exit", Some(code as f64)); - if let Some(async_resources) = async_resources { - for resource in async_resources { - perry_runtime::async_hooks::destroy(resource.async_id); - } - } if let Some(promise) = terminate_promise { super::async_shim::queue_promise_resolution( promise, @@ -213,11 +205,7 @@ pub extern "C" fn js_worker_threads_has_pending() -> i32 { fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { // Collect (callback, web_event) pairs, then invoke OUTSIDE the WORKERS lock — // a listener may re-enter postMessage / terminate, which needs the lock again. - let (object_bits, callbacks, async_resources): ( - u64, - Vec<(u64, bool)>, - [perry_runtime::async_hooks::AsyncResourceIds; 3], - ) = { + let (object_bits, callbacks): (u64, Vec<(u64, bool)>) = { let mut workers = WORKERS.lock().unwrap(); let Some(worker) = workers.get_mut(&worker_id) else { return; @@ -234,13 +222,9 @@ fn dispatch_worker_event(worker_id: u64, event: &str, arg: Option) { callbacks }) .unwrap_or_default(); - (worker.object_bits, callbacks, worker.async_resources) + (worker.object_bits, callbacks) }; - 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. @@ -305,7 +289,4 @@ 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); - } } diff --git a/crates/perry-stdlib/src/zlib.rs b/crates/perry-stdlib/src/zlib.rs index 415b037903..ccade48bd6 100644 --- a/crates/perry-stdlib/src/zlib.rs +++ b/crates/perry-stdlib/src/zlib.rs @@ -439,16 +439,10 @@ unsafe fn queue_zlib_callback(codec: Codec, data_value: f64, callback_value: f64 let result = run_one_shot_codec(codec, &data).map_err(|e| e.to_string()); crate::common::async_bridge::ensure_pump_registered(); ensure_zlib_gc_scanner(); - let resource = perry_runtime::js_object_alloc_null_proto(0, 0); - let async_ids = perry_runtime::async_hooks::init_resource( - "ZLIB", - perry_runtime::js_nanbox_pointer(resource as i64), - true, - ); ZLIB_PENDING_EVENTS .lock() .unwrap() - .push(ZlibEvent::OneShotCallback(callback, result, async_ids)); + .push(ZlibEvent::OneShotCallback(callback, result)); perry_runtime::event_pump::js_notify_main_thread(); } @@ -631,7 +625,6 @@ pub unsafe extern "C" fn js_zlib_zstd_decompress(data_value: f64, callback_value // ============================================================================ struct ZlibStreamState { - async_ids: perry_runtime::async_hooks::AsyncResourceIds, codec: Codec, /// Compression level resolved from the factory's `{ level }` option /// (#4917) — kept so `.reset()` rebuilds the codec at the same level. @@ -656,11 +649,7 @@ enum ZlibEvent { /// `.flush(cb)` completion callback — invoked after its flushed 'data'. Callback(i64), /// One-shot `zlib.gzip(data, cb)` style completion callback. - OneShotCallback( - i64, - Result, String>, - perry_runtime::async_hooks::AsyncResourceIds, - ), + OneShotCallback(i64, Result, String>), } lazy_static::lazy_static! { @@ -710,7 +699,7 @@ fn scan_zlib_roots(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_>) { if let Ok(mut pending) = ZLIB_PENDING_EVENTS.lock() { for ev in pending.iter_mut() { match ev { - ZlibEvent::Callback(cb) | ZlibEvent::OneShotCallback(cb, _, _) => { + ZlibEvent::Callback(cb) | ZlibEvent::OneShotCallback(cb, _) => { visitor.visit_i64_slot(cb); } _ => {} @@ -729,23 +718,12 @@ fn next_zlib_id() -> i64 { id } -fn init_zlib_resource() -> perry_runtime::async_hooks::AsyncResourceIds { - let resource = perry_runtime::js_object_alloc_null_proto(0, 0); - perry_runtime::async_hooks::init_resource( - "ZLIB", - perry_runtime::js_nanbox_pointer(resource as i64), - true, - ) -} - fn create_zlib_stream(codec: Codec, level: Compression) -> i64 { ensure_zlib_gc_scanner(); let id = next_zlib_id(); - let async_ids = init_zlib_resource(); ZLIB_STREAMS.lock().unwrap().insert( id, ZlibStreamState { - async_ids, codec, level, codec_state: make_codec_state(codec, level), @@ -1344,16 +1322,6 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { }; let count = events.len() as i32; for ev in events { - let event_ids = match &ev { - ZlibEvent::Data(id, _) | ZlibEvent::End(id) | ZlibEvent::Error(id, _) => ZLIB_STREAMS - .lock() - .ok() - .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); - } match ev { ZlibEvent::Data(id, bytes) => { publish_zlib_bytes_written(id); @@ -1401,9 +1369,7 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { js_closure_call0(cb as *const ClosureHeader); } } - ZlibEvent::OneShotCallback(cb, result, ids) => { - perry_runtime::async_hooks::run_resource_scope(ids, || {}); - perry_runtime::async_hooks::enter_resource_scope(ids); + ZlibEvent::OneShotCallback(cb, result) => { if cb != 0 { match result { Ok(bytes) => { @@ -1432,7 +1398,6 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { } } } - perry_runtime::async_hooks::leave_resource_scope(ids.async_id); } ZlibEvent::Error(id, msg) => { let err_f64 = build_zlib_error(&msg); @@ -1445,9 +1410,6 @@ pub unsafe extern "C" fn js_zlib_process_pending() -> i32 { ZLIB_STREAMS.lock().unwrap().remove(&id); } } - if let Some(ids) = event_ids { - perry_runtime::async_hooks::leave_resource_scope(ids.async_id); - } } count } diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index ffa282300a..5ca8b4c2db 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -49,9 +49,6 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[ // to a shadow frame. It changes which functions carry statepoints, so it // changes the generated code and must be a cache input. "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", @@ -142,8 +139,6 @@ const BUILD_CACHE_ENV_EXCLUSIONS: &[&str] = &[ "PERRY_LLVM_DIFF_DIR", "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/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index ddaeaab8f9..2c6ed3d854 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -738,9 +738,6 @@ fn collect_module_one( return; } for p in &set { - if p.starts_with("data:text/javascript,") { - ctx.uses_data_url_dynamic_import = true; - } if !new_dyn_imports.contains(p) { new_dyn_imports.push(p.clone()); } diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index e341b40a4e..c0d9d1a9f5 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -380,13 +380,6 @@ pub(crate) fn build_optimized_libs( if original_features.contains(&"bundled-net") { features.insert("external-net-pump"); } - // perry-ext-net owns client sockets after the well-known flip, - // but TLS servers and TLSSocket introspection still live in - // perry-stdlib. Retain that surface without re-enabling - // `bundled-net` (and its colliding socket symbols). - if original_features.contains(&"tls") { - features.insert("external-tls-server"); - } // #1843 — when the flip strips the compression base feature and // routes `node:zlib` to perry-ext-zlib, activate // `external-zlib-pump` so perry-stdlib's main-thread pump + @@ -460,13 +453,6 @@ pub(crate) fn build_optimized_libs( // them would drop unresolved externs at link time. if matches!(module_normalized, "http" | "https" | "http2") { features.insert("external-http-server-pump"); - // perry-ext-http depends on perry-ext-net, whose TLS client - // object calls back into perry-stdlib's main-thread - // preflight hook. Retain that provider even for plain HTTP: - // the static archive can pull the shared socket object before - // a TLS-specific API is used, and otherwise leaves - // `js_tls_client_preflight` undefined at link time. - features.insert("external-tls-server"); } // Issue #769 — when `node:http` / `node:https` routes to // perry-ext-http, also activate the client-side pump so the diff --git a/crates/perry/src/commands/compile/optimized_libs/freshness.rs b/crates/perry/src/commands/compile/optimized_libs/freshness.rs index 4cb562c009..543f0280e9 100644 --- a/crates/perry/src/commands/compile/optimized_libs/freshness.rs +++ b/crates/perry/src/commands/compile/optimized_libs/freshness.rs @@ -146,9 +146,7 @@ pub(crate) fn auto_optimized_cache_key( needs_node_test(ctx), // #6559: dyn-eval presence changes the built archive, so it must // key the freshness stamp like every other runtime feature toggle. - perry_hir::has_deferred_dynamic_code_sites() - || ctx.native_module_imports.contains("vm") - || ctx.uses_data_url_dynamic_import, + perry_hir::has_deferred_dynamic_code_sites(), format!( "{}{}{}", size_opt_level().unwrap_or("off"), @@ -217,16 +215,13 @@ pub(crate) fn auto_optimized_cross_features( // `Intl.*` namespace surface — see perry-runtime's `intl-namespace`. // A deferred dynamic-code site can construct `Intl.…` from a runtime // string, so force it on there too (mirrors the dyn-eval regex rule). - let needs_dyn_eval = perry_hir::has_deferred_dynamic_code_sites() - || ctx.native_module_imports.contains("vm") - || ctx.uses_data_url_dynamic_import; - if ctx.uses_intl_namespace || needs_dyn_eval { + if ctx.uses_intl_namespace || perry_hir::has_deferred_dynamic_code_sites() { cross_features.push("perry-runtime/intl-namespace".to_string()); } // Per-namespace globalThis member tables — see perry-runtime's `global-*`. // A deferred dynamic-code site can reach any namespace by runtime string, // so force all four on there (mirrors the intl-namespace rule). - let dynamic_code = needs_dyn_eval; + let dynamic_code = perry_hir::has_deferred_dynamic_code_sites(); for (used, feat) in [ (ctx.uses_global_math, "global-math"), (ctx.uses_global_json, "global-json"), @@ -278,7 +273,7 @@ pub(crate) fn auto_optimized_cross_features( // interpreter. The generated code of the schema-codegen ecosystem (ajv) // also leans on regex literals (`key.replace(/~/g, …)`), so the regex // engine rides along even when the program's own source never uses one. - if needs_dyn_eval { + if perry_hir::has_deferred_dynamic_code_sites() { cross_features.push("perry-runtime/dyn-eval".to_string()); if !ctx.uses_regex { cross_features.push("perry-runtime/regex-engine".to_string()); diff --git a/crates/perry/src/commands/compile/optimized_libs/tests.rs b/crates/perry/src/commands/compile/optimized_libs/tests.rs index b47d77718f..ac6da186c3 100644 --- a/crates/perry/src/commands/compile/optimized_libs/tests.rs +++ b/crates/perry/src/commands/compile/optimized_libs/tests.rs @@ -1099,26 +1099,6 @@ fn auto_optimize_always_includes_keepalive_anchors() { ); } -#[test] -fn data_url_dynamic_import_enables_dyn_eval_and_changes_cache_key() { - let dir = tempfile::tempdir().expect("tempdir"); - let empty_features = std::collections::BTreeSet::new(); - let without = CompilationContext::new(dir.path().to_path_buf()); - let mut with_data_url = CompilationContext::new(dir.path().to_path_buf()); - with_data_url.uses_data_url_dynamic_import = true; - - let cross = auto_optimized_cross_features(&with_data_url, &empty_features, &[]); - assert!( - cross.iter().any(|f| f == "perry-runtime/dyn-eval"), - "data URL modules require the dyn-eval runtime, got {cross:?}" - ); - assert_ne!( - auto_optimized_cache_key("", true, false, None, &with_data_url), - auto_optimized_cache_key("", true, false, None, &without), - "a runtime without dyn-eval must not be reused for data URL imports" - ); -} - /// The `keepalive-anchors` feature must NOT be conditional on /// `PERRY_LLVM_BITCODE_LINK` — the classic link path needs it too. #[test] diff --git a/crates/perry/src/commands/compile/types.rs b/crates/perry/src/commands/compile/types.rs index eafa7ac4fe..2c2e756791 100644 --- a/crates/perry/src/commands/compile/types.rs +++ b/crates/perry/src/commands/compile/types.rs @@ -739,10 +739,6 @@ pub struct CompilationContext { /// (e.g. "mysql2", "fastify", "ws"). Used by `--minimal-stdlib` to /// compute the smallest perry-stdlib feature set that satisfies them. pub native_module_imports: BTreeSet, - /// Whether a dynamic import can resolve to a JavaScript `data:` URL. - /// The runtime evaluates these modules through the dyn-eval interpreter, - /// so auto-optimized archives must retain that otherwise optional feature. - pub uses_data_url_dynamic_import: bool, /// Whether any TS module calls global `fetch()` (which routes to /// reqwest in perry-stdlib's http-client feature). pub uses_fetch: bool, @@ -1186,7 +1182,6 @@ impl CompilationContext { needs_geisterhand: false, geisterhand_port: 7676, native_module_imports: BTreeSet::new(), - uses_data_url_dynamic_import: false, uses_fetch: false, uses_crypto_builtins: false, uses_zlib_brotli: false, diff --git a/docs/src/api/reference.md b/docs/src/api/reference.md index e8704a9ead..2f74be5bf8 100644 --- a/docs/src/api/reference.md +++ b/docs/src/api/reference.md @@ -2,7 +2,7 @@ This page is auto-generated from Perry's compile-time API manifest (`perry-api-manifest::API_MANIFEST`). It is the source of truth for what `perry compile` accepts; references to symbols not listed here produce `R005 UnimplementedApi` (issue #463). Stubs (#464) are flagged ⚠ — they link cleanly but no-op at runtime on the chosen target. -Total: 3017 entries across 137 modules. +Total: 3015 entries across 137 modules. ## Modules @@ -1752,8 +1752,6 @@ Total: 3017 entries across 137 modules. - `on` — instance *(class: `HttpServer`)* - `on` — instance *(class: `IncomingMessage`)* - `on` — instance *(class: `ServerResponse`)* -- `once` — instance *(class: `IncomingMessage`)* -- `once` — instance *(class: `ClientRequest`)* - `pause` — instance *(class: `IncomingMessage`)* - `protocol` — instance *(class: `Agent`)* - `read` — instance *(class: `IncomingMessage`)* diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index e4aebe2a68..e39071bd95 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -253,12 +253,6 @@ "verdict": "test_only", "why": "#[cfg(test)] diagnostic trace for the bound-method moving-GC regression: records the (before, after) addresses a test-forced minor produced so the test can assert the relocation happened. The addresses are compared as integers, never dereferenced, and the cell is dead in a shipped binary." }, - { - "file": "crates/perry-runtime/src/os/signal.rs", - "name": "SIGNAL_ASYNC_IDS", - "verdict": "not_a_gc_pointer", - "why": "Maps owned Rust signal-name strings to async_hooks async IDs. Async IDs are scalar u64 identifiers, not JS heap addresses; the AsyncResource objects themselves live in async_hooks RESOURCES, whose registered scanner visits them." - }, { "file": "crates/perry-runtime/src/perf_hooks.rs", "name": "LOOP_START_MS", @@ -343,12 +337,6 @@ "verdict": "not_a_gc_pointer", "why": "Keyed by decoder handle id; DecoderState is Rust-owned (encoding enum, label, flags), no JS values." }, - { - "file": "crates/perry-runtime/src/timer.rs", - "name": "TIMER_HANDLE_KINDS", - "verdict": "not_a_gc_pointer", - "why": "Maps scalar timer handle IDs to the CallbackTimerKind enum so clearTimeout/clearInterval can destroy the matching async_hooks resource. Neither the i64 keys nor the enum values contain a JS heap address." - }, { "file": "crates/perry-runtime/src/thread.rs", "name": "ACTIVE_THREAD_JOBS", @@ -448,24 +436,12 @@ "verdict": "not_a_gc_pointer", "why": "Transform writable id -> COUNT of queued write jobs (#6607). No address." }, - { - "file": "crates/perry-stdlib/src/worker_threads.rs", - "name": "CURRENT_WORKER_ID", - "verdict": "not_a_gc_pointer", - "why": "Thread-local scalar ID of the currently executing in-process Worker. It is an index into the Rust-owned WORKERS registry, not a JS heap address." - }, { "file": "crates/perry-stdlib/src/worker_threads.rs", "name": "NEXT_PORT_ID", "verdict": "not_a_gc_pointer", "why": "Monotonic MessagePort id counter." }, - { - "file": "crates/perry-stdlib/src/worker_threads.rs", - "name": "NEXT_WORKER_ID", - "verdict": "not_a_gc_pointer", - "why": "Monotonic u64 counter used to allocate in-process Worker registry IDs. It contains no address." - }, { "file": "crates/perry-stdlib/src/ws.rs", "name": "NEXT_WS_ID", diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index 7b29eaa595..3825d7b805 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -123,7 +123,7 @@ 9 crates/perry-runtime/src/promise/microtasks.rs 1 crates/perry-runtime/src/promise/native_async.rs 3 crates/perry-runtime/src/promise/rejection.rs -3 crates/perry-runtime/src/promise/then.rs +8 crates/perry-runtime/src/promise/then.rs 8 crates/perry-runtime/src/proxy.rs 6 crates/perry-runtime/src/regex.rs 19 crates/perry-runtime/src/regex/exec_array.rs diff --git a/scripts/string_payload_access_baseline.txt b/scripts/string_payload_access_baseline.txt index 27bcefbbfb..84320b2308 100644 --- a/scripts/string_payload_access_baseline.txt +++ b/scripts/string_payload_access_baseline.txt @@ -12,7 +12,7 @@ inline-offset | perry-ext-nodemailer | 1 inline-offset | perry-ext-pg | 2 inline-offset | perry-ext-zlib | 3 inline-offset | perry-ffi | 3 -inline-offset | perry-runtime | 364 +inline-offset | perry-runtime | 365 inline-offset | perry-stdlib | 48 inline-offset | perry-updater | 5 reader-helper | perry-ext-ethers | 1 diff --git a/scripts/thread_local_cold_allowlist.json b/scripts/thread_local_cold_allowlist.json index 566ab8a28c..3b945f66f6 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": 227, "files": { "crates/perry-runtime/src/agent.rs": 1, "crates/perry-runtime/src/arena/block.rs": 2, @@ -24,6 +24,7 @@ "crates/perry-runtime/src/eh_walker.rs": 1, "crates/perry-runtime/src/error.rs": 2, "crates/perry-runtime/src/event_pump.rs": 1, + "crates/perry-runtime/src/fs/callbacks.rs": 1, "crates/perry-runtime/src/fs/dir_glob_watch/watch.rs": 1, "crates/perry-runtime/src/fs/filehandle.rs": 1, "crates/perry-runtime/src/fs/mod.rs": 1,