From fcb831be187a19912cd3d89d158ec50e7cae25fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 21:45:17 +0200 Subject: [PATCH 1/4] fix(async_hooks): expose constructor prototypes (#6764) --- .../perry-runtime/src/object/native_module.rs | 1 + .../native_module/async_hooks_exports.rs | 130 ++++++++++++++++++ .../object/native_module/callable_exports.rs | 6 +- ...sue_6764_async_hooks_prototype_metadata.rs | 120 ++++++++++++++++ 4 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 crates/perry-runtime/src/object/native_module/async_hooks_exports.rs create mode 100644 crates/perry/tests/issue_6764_async_hooks_prototype_metadata.rs diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 71a60986c1..ddc8e5eb16 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -12,6 +12,7 @@ use std::cell::{Cell, RefCell}; use std::ptr::null_mut; use std::sync::atomic::{AtomicPtr, Ordering}; +mod async_hooks_exports; mod callable_export_check; pub(crate) mod callable_exports; mod constants; 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 new file mode 100644 index 0000000000..58686b372e --- /dev/null +++ b/crates/perry-runtime/src/object/native_module/async_hooks_exports.rs @@ -0,0 +1,130 @@ +//! Reflective constructor/prototype surface for `node:async_hooks`. +//! +//! Direct calls on AsyncLocalStorage/AsyncResource are native-dispatched +//! elsewhere. This module supplies the ordinary JS prototype objects so +//! reflection and method-as-value reads see the same functions Node exposes. + +use super::callable_exports::{set_bound_native_closure_name, set_builtin_closure_length}; +use super::*; + +const ASYNC_LOCAL_STORAGE_METHODS: &[(&str, u32)] = &[ + ("run", 2), + ("getStore", 0), + ("enterWith", 1), + ("exit", 1), + ("disable", 0), +]; + +const ASYNC_RESOURCE_METHODS: &[(&str, u32)] = &[ + ("asyncId", 0), + ("triggerAsyncId", 0), + ("emitDestroy", 0), + ("runInAsyncScope", 2), + ("bind", 2), +]; + +/// Forward a prototype method call through the existing dynamic receiver +/// dispatcher. The rest array preserves every variadic argument for +/// `run`, `exit`, and `runInAsyncScope`. +extern "C" fn async_hooks_prototype_method_thunk( + closure: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + unsafe { + let name_ptr = crate::closure::js_closure_get_capture_ptr(closure, 0) as *const i8; + let name_len = crate::closure::js_closure_get_capture_ptr(closure, 1) as usize; + let receiver = crate::object::js_implicit_this_get(); + let name = std::slice::from_raw_parts(name_ptr as *const u8, name_len); + + // Node's enterWith/disable implementations do not brand-check an + // arbitrary object receiver; they simply have no observable storage + // state to mutate there. Preserve that no-op behavior instead of + // asking the generic object dispatcher to call a missing method. + if matches!(name, b"enterWith" | b"disable") { + let receiver_value = JSValue::from_bits(receiver.to_bits()); + if receiver_value.is_pointer() + && !crate::value::addr_class::is_handle_band( + receiver_value.as_pointer::() as usize + ) + { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + } + + let args_array = crate::value::js_nanbox_get_pointer(rest); + crate::object::js_native_call_method_apply(receiver, name_ptr, name_len, args_array) + } +} + +fn attach_prototype(constructor_value: f64, methods: &[(&str, u32)]) { + let constructor_js = JSValue::from_bits(constructor_value.to_bits()); + if !constructor_js.is_pointer() { + return; + } + let constructor = constructor_js.as_pointer::() as usize; + if constructor == 0 { + return; + } + + let prototype = js_object_alloc(0, 0); + if prototype.is_null() { + return; + } + + let constructor_name = "constructor"; + let constructor_key = crate::string::js_string_from_bytes( + constructor_name.as_ptr(), + constructor_name.len() as u32, + ); + js_object_set_field_by_name(prototype, constructor_key, constructor_value); + super::super::set_builtin_property_attrs( + prototype as usize, + constructor_name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + + let thunk = async_hooks_prototype_method_thunk as *const u8; + crate::closure::js_register_closure_rest(thunk, 0); + for &(name, length) in methods { + let leaked: &'static [u8] = name.as_bytes().to_vec().leak(); + let method = crate::closure::js_closure_alloc(thunk, 2); + if method.is_null() { + continue; + } + crate::closure::js_closure_set_capture_ptr(method, 0, leaked.as_ptr() as i64); + crate::closure::js_closure_set_capture_ptr(method, 1, leaked.len() as i64); + set_bound_native_closure_name(method, name); + set_builtin_closure_length(method as usize, length); + + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + js_object_set_field_by_name( + prototype, + key, + crate::value::js_nanbox_pointer(method as i64), + ); + super::super::set_builtin_property_attrs( + prototype as usize, + name.to_string(), + super::super::PropertyAttrs::new(true, false, true), + ); + } + + crate::closure::closure_set_dynamic_prop( + constructor, + "prototype", + crate::value::js_nanbox_pointer(prototype as i64), + ); + super::super::set_builtin_property_attrs( + constructor, + "prototype".to_string(), + super::super::PropertyAttrs::new(false, false, false), + ); +} + +pub(super) fn attach_async_local_storage_prototype(constructor_value: f64) { + attach_prototype(constructor_value, ASYNC_LOCAL_STORAGE_METHODS); +} + +pub(super) fn attach_async_resource_prototype(constructor_value: f64) { + attach_prototype(constructor_value, ASYNC_RESOURCE_METHODS); +} diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index 3607051746..ad2966a345 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -246,7 +246,7 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option Some(1), ("querystring", "stringify" | "parse") => Some(4), ("async_hooks", "AsyncLocalStorage") => Some(0), - ("async_hooks", "AsyncResource") => Some(2), + ("async_hooks", "AsyncResource") => Some(1), ("async_hooks", "createHook") => Some(1), ("async_hooks", "executionAsyncId") => Some(0), ("async_hooks", "triggerAsyncId") => Some(0), @@ -1686,6 +1686,7 @@ pub(crate) unsafe fn nm_attach_async_hooks( closure_addr: usize, ) -> f64 { if property_name == "AsyncLocalStorage" { + super::async_hooks_exports::attach_async_local_storage_prototype(value); crate::closure::closure_set_dynamic_prop( closure_addr, "bind", @@ -1709,6 +1710,7 @@ pub(crate) unsafe fn nm_attach_async_hooks( } if property_name == "AsyncResource" { + super::async_hooks_exports::attach_async_resource_prototype(value); crate::closure::closure_set_dynamic_prop( closure_addr, "bind", @@ -1817,7 +1819,7 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[ "async_hooks", &[ ("AsyncLocalStorage", 0), - ("AsyncResource", 2), + ("AsyncResource", 1), ("createHook", 1), ("executionAsyncId", 0), ("executionAsyncResource", 0), diff --git a/crates/perry/tests/issue_6764_async_hooks_prototype_metadata.rs b/crates/perry/tests/issue_6764_async_hooks_prototype_metadata.rs new file mode 100644 index 0000000000..d4beef6ea6 --- /dev/null +++ b/crates/perry/tests/issue_6764_async_hooks_prototype_metadata.rs @@ -0,0 +1,120 @@ +//! Regression coverage for the first #6764 async_hooks parity increment: +//! constructor/prototype metadata and reflective prototype calls. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +#[test] +fn async_hooks_constructors_expose_real_prototype_methods() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write( + &entry, + r#" +import { AsyncLocalStorage, AsyncResource } from "node:async_hooks"; + +function metadata(entries: Array<[string, unknown]>) { + return entries + .map(([name, value]) => + typeof value === "function" + ? `${name}:${(value as Function).name}/${(value as Function).length}` + : `${name}:missing`, + ) + .join("|"); +} + +console.log( + "storage:", + metadata([ + ["constructor", AsyncLocalStorage], + ["run", AsyncLocalStorage.prototype.run], + ["getStore", AsyncLocalStorage.prototype.getStore], + ["enterWith", AsyncLocalStorage.prototype.enterWith], + ["exit", AsyncLocalStorage.prototype.exit], + ["disable", AsyncLocalStorage.prototype.disable], + ]), +); +console.log( + "resource:", + metadata([ + ["constructor", AsyncResource], + ["asyncId", AsyncResource.prototype.asyncId], + ["triggerAsyncId", AsyncResource.prototype.triggerAsyncId], + ["emitDestroy", AsyncResource.prototype.emitDestroy], + ["runInAsyncScope", AsyncResource.prototype.runInAsyncScope], + ["bind", AsyncResource.prototype.bind], + ]), +); + +const storage = new AsyncLocalStorage(); +const storageResult = AsyncLocalStorage.prototype.run.call( + storage, + "ctx", + (a: number, b: number) => `${storage.getStore()}:${a + b}`, + 2, + 3, +); +console.log("storage call:", storageResult); + +const resource = new AsyncResource("fixture"); +const resourceResult = AsyncResource.prototype.runInAsyncScope.call( + resource, + (a: number, b: number) => a + b, + null, + 4, + 5, +); +console.log("resource call:", resourceResult); +console.log( + "foreign no-op:", + AsyncLocalStorage.prototype.enterWith.call({}, "value"), + AsyncLocalStorage.prototype.disable.call({}), +); +"#, + ) + .expect("write fixture"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .output() + .expect("run compiled fixture"); + assert!( + run.status.success(), + "compiled fixture failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + concat!( + "storage: constructor:AsyncLocalStorage/0|run:run/2|getStore:getStore/0|", + "enterWith:enterWith/1|exit:exit/1|disable:disable/0\n", + "resource: constructor:AsyncResource/1|asyncId:asyncId/0|", + "triggerAsyncId:triggerAsyncId/0|emitDestroy:emitDestroy/0|", + "runInAsyncScope:runInAsyncScope/2|bind:bind/2\n", + "storage call: ctx:5\n", + "resource call: 9\n", + "foreign no-op: undefined undefined\n", + ) + ); +} From b76e286f22b6fd5db9766884fee0307768872a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 22:09:55 +0200 Subject: [PATCH 2/4] docs: add changelog for #7093 --- changelog.d/7093-async-hooks-prototypes.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7093-async-hooks-prototypes.md diff --git a/changelog.d/7093-async-hooks-prototypes.md b/changelog.d/7093-async-hooks-prototypes.md new file mode 100644 index 0000000000..88e7555728 --- /dev/null +++ b/changelog.d/7093-async-hooks-prototypes.md @@ -0,0 +1 @@ +Fixed `node:async_hooks` constructor prototype metadata and reflective prototype method calls. From 12e9180d68a743b5fdb68bbfb814b57bece5e646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 22:53:21 +0200 Subject: [PATCH 3/4] fix(async_hooks): root prototype metadata construction (#6764) --- .../native_module/async_hooks_exports.rs | 95 ++++++++++++++----- .../object/native_module/callable_exports.rs | 86 +++++++++++------ 2 files changed, 128 insertions(+), 53 deletions(-) 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 58686b372e..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 @@ -4,7 +4,7 @@ //! elsewhere. This module supplies the ordinary JS prototype objects so //! reflection and method-as-value reads see the same functions Node exposes. -use super::callable_exports::{set_bound_native_closure_name, set_builtin_closure_length}; +use super::callable_exports::set_builtin_closure_length; use super::*; const ASYNC_LOCAL_STORAGE_METHODS: &[(&str, u32)] = &[ @@ -43,8 +43,8 @@ extern "C" fn async_hooks_prototype_method_thunk( if matches!(name, b"enterWith" | b"disable") { let receiver_value = JSValue::from_bits(receiver.to_bits()); if receiver_value.is_pointer() - && !crate::value::addr_class::is_handle_band( - receiver_value.as_pointer::() as usize + && crate::value::addr_class::is_plausible_heap_addr( + receiver_value.as_pointer::() as usize, ) { return f64::from_bits(crate::value::TAG_UNDEFINED); @@ -56,29 +56,45 @@ extern "C" fn async_hooks_prototype_method_thunk( } } -fn attach_prototype(constructor_value: f64, methods: &[(&str, u32)]) { +fn attach_prototype(constructor_value: f64, methods: &[(&str, u32)]) -> f64 { let constructor_js = JSValue::from_bits(constructor_value.to_bits()); if !constructor_js.is_pointer() { - return; + return constructor_value; } let constructor = constructor_js.as_pointer::() as usize; if constructor == 0 { - return; + return constructor_value; } + // Every allocation below can evacuate the constructor, prototype, method + // closures, and strings. Keep raw pointers only in updateable roots and + // reload them immediately before each use. + let scope = crate::gc::RuntimeHandleScope::new(); + let constructor_handle = + scope.root_raw_mut_ptr(constructor as *mut crate::closure::ClosureHeader); let prototype = js_object_alloc(0, 0); if prototype.is_null() { - return; + return crate::value::js_nanbox_pointer( + constructor_handle.get_raw_mut_ptr::() as i64, + ); } + let prototype_handle = scope.root_raw_mut_ptr(prototype); let constructor_name = "constructor"; let constructor_key = crate::string::js_string_from_bytes( constructor_name.as_ptr(), constructor_name.len() as u32, ); - js_object_set_field_by_name(prototype, constructor_key, constructor_value); + let constructor_key_handle = scope.root_string_ptr(constructor_key); + js_object_set_field_by_name( + prototype_handle.get_raw_mut_ptr(), + constructor_key_handle.get_raw_mut_ptr(), + crate::value::js_nanbox_pointer( + constructor_handle.get_raw_mut_ptr::() as i64, + ), + ); super::super::set_builtin_property_attrs( - prototype as usize, + prototype_handle.get_raw_mut_ptr::() as usize, constructor_name.to_string(), super::super::PropertyAttrs::new(true, false, true), ); @@ -86,45 +102,72 @@ fn attach_prototype(constructor_value: f64, methods: &[(&str, u32)]) { let thunk = async_hooks_prototype_method_thunk as *const u8; crate::closure::js_register_closure_rest(thunk, 0); for &(name, length) in methods { - let leaked: &'static [u8] = name.as_bytes().to_vec().leak(); let method = crate::closure::js_closure_alloc(thunk, 2); if method.is_null() { continue; } - crate::closure::js_closure_set_capture_ptr(method, 0, leaked.as_ptr() as i64); - crate::closure::js_closure_set_capture_ptr(method, 1, leaked.len() as i64); - set_bound_native_closure_name(method, name); - set_builtin_closure_length(method as usize, length); + let method_handle = scope.root_raw_mut_ptr(method); + crate::closure::js_closure_set_capture_ptr( + method_handle.get_raw_mut_ptr(), + 0, + name.as_ptr() as i64, + ); + crate::closure::js_closure_set_capture_ptr( + method_handle.get_raw_mut_ptr(), + 1, + name.len() as i64, + ); + + let name_string = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name_handle = scope.root_string_ptr(name_string); + crate::closure::closure_set_dynamic_prop( + method_handle.get_raw_mut_ptr::() as usize, + "name", + f64::from_bits(JSValue::string_ptr(name_handle.get_raw_mut_ptr()).bits()), + ); + super::super::set_builtin_property_attrs( + method_handle.get_raw_mut_ptr::() as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + set_builtin_closure_length( + method_handle.get_raw_mut_ptr::() as usize, + length, + ); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); js_object_set_field_by_name( - prototype, - key, - crate::value::js_nanbox_pointer(method as i64), + prototype_handle.get_raw_mut_ptr(), + name_handle.get_raw_mut_ptr(), + crate::value::js_nanbox_pointer( + method_handle.get_raw_mut_ptr::() as i64, + ), ); super::super::set_builtin_property_attrs( - prototype as usize, + prototype_handle.get_raw_mut_ptr::() as usize, name.to_string(), super::super::PropertyAttrs::new(true, false, true), ); } crate::closure::closure_set_dynamic_prop( - constructor, + constructor_handle.get_raw_mut_ptr::() as usize, "prototype", - crate::value::js_nanbox_pointer(prototype as i64), + crate::value::js_nanbox_pointer(prototype_handle.get_raw_mut_ptr::() as i64), ); super::super::set_builtin_property_attrs( - constructor, + constructor_handle.get_raw_mut_ptr::() as usize, "prototype".to_string(), super::super::PropertyAttrs::new(false, false, false), ); + crate::value::js_nanbox_pointer( + constructor_handle.get_raw_mut_ptr::() as i64, + ) } -pub(super) fn attach_async_local_storage_prototype(constructor_value: f64) { - attach_prototype(constructor_value, ASYNC_LOCAL_STORAGE_METHODS); +pub(super) fn attach_async_local_storage_prototype(constructor_value: f64) -> f64 { + attach_prototype(constructor_value, ASYNC_LOCAL_STORAGE_METHODS) } -pub(super) fn attach_async_resource_prototype(constructor_value: f64) { - attach_prototype(constructor_value, ASYNC_RESOURCE_METHODS); +pub(super) fn attach_async_resource_prototype(constructor_value: f64) -> f64 { + attach_prototype(constructor_value, ASYNC_RESOURCE_METHODS) } diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index ad2966a345..faead7945e 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -114,13 +114,31 @@ fn async_hooks_static_method_value( length: u32, ) -> f64 { crate::closure::js_register_closure_rest(func_ptr, fixed_arity); + let scope = crate::gc::RuntimeHandleScope::new(); + let name_string = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name_handle = scope.root_string_ptr(name_string); let closure = crate::closure::js_closure_alloc(func_ptr, 0); if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - set_bound_native_closure_name(closure, name); - set_builtin_closure_length(closure as usize, length); - crate::value::js_nanbox_pointer(closure as i64) + let closure_handle = scope.root_raw_mut_ptr(closure); + crate::closure::closure_set_dynamic_prop( + closure_handle.get_raw_mut_ptr::() as usize, + "name", + f64::from_bits(JSValue::string_ptr(name_handle.get_raw_mut_ptr()).bits()), + ); + super::super::set_builtin_property_attrs( + closure_handle.get_raw_mut_ptr::() as usize, + "name".to_string(), + super::super::PropertyAttrs::new(false, false, true), + ); + set_builtin_closure_length( + closure_handle.get_raw_mut_ptr::() as usize, + length, + ); + crate::value::js_nanbox_pointer( + closure_handle.get_raw_mut_ptr::() as i64, + ) } extern "C" fn fs_namespace_descriptor_getter_thunk( @@ -1683,45 +1701,59 @@ pub(crate) unsafe fn nm_attach_perf_hooks( pub(crate) unsafe fn nm_attach_async_hooks( property_name: &str, mut value: f64, - closure_addr: usize, + _closure_addr: usize, ) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let constructor_handle = scope.root_nanbox_f64(value); if property_name == "AsyncLocalStorage" { - super::async_hooks_exports::attach_async_local_storage_prototype(value); + constructor_handle.set_nanbox_f64( + super::async_hooks_exports::attach_async_local_storage_prototype( + constructor_handle.get_nanbox_f64(), + ), + ); + let bind = scope.root_nanbox_f64(async_hooks_static_method_value( + crate::async_hooks::js_async_local_storage_static_bind_method as *const u8, + "bind", + 1, + 1, + )); crate::closure::closure_set_dynamic_prop( - closure_addr, + crate::value::js_nanbox_get_pointer(constructor_handle.get_nanbox_f64()) as usize, "bind", - async_hooks_static_method_value( - crate::async_hooks::js_async_local_storage_static_bind_method as *const u8, - "bind", - 1, - 1, - ), + bind.get_nanbox_f64(), ); + let snapshot = scope.root_nanbox_f64(async_hooks_static_method_value( + crate::async_hooks::js_async_local_storage_static_snapshot_method as *const u8, + "snapshot", + 0, + 0, + )); crate::closure::closure_set_dynamic_prop( - closure_addr, + crate::value::js_nanbox_get_pointer(constructor_handle.get_nanbox_f64()) as usize, "snapshot", - async_hooks_static_method_value( - crate::async_hooks::js_async_local_storage_static_snapshot_method as *const u8, - "snapshot", - 0, - 0, - ), + snapshot.get_nanbox_f64(), ); } if property_name == "AsyncResource" { - super::async_hooks_exports::attach_async_resource_prototype(value); + constructor_handle.set_nanbox_f64( + super::async_hooks_exports::attach_async_resource_prototype( + constructor_handle.get_nanbox_f64(), + ), + ); + let bind = scope.root_nanbox_f64(async_hooks_static_method_value( + crate::async_hooks::js_async_resource_static_bind_method as *const u8, + "bind", + 3, + 3, + )); crate::closure::closure_set_dynamic_prop( - closure_addr, + crate::value::js_nanbox_get_pointer(constructor_handle.get_nanbox_f64()) as usize, "bind", - async_hooks_static_method_value( - crate::async_hooks::js_async_resource_static_bind_method as *const u8, - "bind", - 3, - 3, - ), + bind.get_nanbox_f64(), ); } + value = constructor_handle.get_nanbox_f64(); value } From c3bb2c44f29d5636a51ebd7bedad8940ff5e7c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 23:28:06 +0200 Subject: [PATCH 4/4] refactor(runtime): reuse rooted callable naming helper --- .../object/native_module/callable_exports.rs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/perry-runtime/src/object/native_module/callable_exports.rs b/crates/perry-runtime/src/object/native_module/callable_exports.rs index faead7945e..7200ce999b 100644 --- a/crates/perry-runtime/src/object/native_module/callable_exports.rs +++ b/crates/perry-runtime/src/object/native_module/callable_exports.rs @@ -115,22 +115,14 @@ fn async_hooks_static_method_value( ) -> f64 { crate::closure::js_register_closure_rest(func_ptr, fixed_arity); let scope = crate::gc::RuntimeHandleScope::new(); - let name_string = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let name_handle = scope.root_string_ptr(name_string); let closure = crate::closure::js_closure_alloc(func_ptr, 0); if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } let closure_handle = scope.root_raw_mut_ptr(closure); - crate::closure::closure_set_dynamic_prop( - closure_handle.get_raw_mut_ptr::() as usize, - "name", - f64::from_bits(JSValue::string_ptr(name_handle.get_raw_mut_ptr()).bits()), - ); - super::super::set_builtin_property_attrs( - closure_handle.get_raw_mut_ptr::() as usize, - "name".to_string(), - super::super::PropertyAttrs::new(false, false, true), + set_bound_native_closure_name( + closure_handle.get_raw_mut_ptr::(), + name, ); set_builtin_closure_length( closure_handle.get_raw_mut_ptr::() as usize, @@ -1465,9 +1457,16 @@ pub(crate) fn set_bound_native_closure_name( closure: *mut crate::closure::ClosureHeader, name: &str, ) { + let scope = crate::gc::RuntimeHandleScope::new(); + let closure_handle = scope.root_raw_mut_ptr(closure); let ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let name_value = f64::from_bits(JSValue::string_ptr(ptr).bits()); - crate::closure::closure_set_dynamic_prop(closure as usize, "name", name_value); + let name_handle = scope.root_string_ptr(ptr); + let name_value = f64::from_bits(JSValue::string_ptr(name_handle.get_raw_mut_ptr()).bits()); + crate::closure::closure_set_dynamic_prop( + closure_handle.get_raw_mut_ptr::() as usize, + "name", + name_value, + ); // Spec: a function's `name` property is { writable:false, enumerable:false, // configurable:true }. Storing it as a plain dynamic prop left it ENUMERABLE // by default, so `for (k in Buffer)` yielded "name" — even though @@ -1490,7 +1489,7 @@ pub(crate) fn set_bound_native_closure_name( // table unconditionally, so the builtin variant preserves the // safe-buffer semantics above. crate::object::set_builtin_property_attrs( - closure as usize, + closure_handle.get_raw_mut_ptr::() as usize, "name".to_string(), crate::object::PropertyAttrs::new(false, false, true), );