Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7093-async-hooks-prototypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed `node:async_hooks` constructor prototype metadata and reflective prototype method calls.
1 change: 1 addition & 0 deletions crates/perry-runtime/src/object/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
173 changes: 173 additions & 0 deletions crates/perry-runtime/src/object/native_module/async_hooks_exports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//! 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_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_plausible_heap_addr(
receiver_value.as_pointer::<u8>() as usize,
)
{
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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)]) -> f64 {
let constructor_js = JSValue::from_bits(constructor_value.to_bits());
if !constructor_js.is_pointer() {
return constructor_value;
}
let constructor = constructor_js.as_pointer::<crate::closure::ClosureHeader>() as usize;
if constructor == 0 {
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 crate::value::js_nanbox_pointer(
constructor_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() 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,
);
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::<crate::closure::ClosureHeader>() as i64,
),
);
super::super::set_builtin_property_attrs(
prototype_handle.get_raw_mut_ptr::<ObjectHeader>() 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 method = crate::closure::js_closure_alloc(thunk, 2);
if method.is_null() {
continue;
}
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::<crate::closure::ClosureHeader>() 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::<crate::closure::ClosureHeader>() as usize,
"name".to_string(),
super::super::PropertyAttrs::new(false, false, true),
);
set_builtin_closure_length(
method_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as usize,
length,
);

js_object_set_field_by_name(
prototype_handle.get_raw_mut_ptr(),
name_handle.get_raw_mut_ptr(),
crate::value::js_nanbox_pointer(
method_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as i64,
),
);
super::super::set_builtin_property_attrs(
prototype_handle.get_raw_mut_ptr::<ObjectHeader>() as usize,
name.to_string(),
super::super::PropertyAttrs::new(true, false, true),
);
}

crate::closure::closure_set_dynamic_prop(
constructor_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as usize,
"prototype",
crate::value::js_nanbox_pointer(prototype_handle.get_raw_mut_ptr::<ObjectHeader>() as i64),
);
super::super::set_builtin_property_attrs(
constructor_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as usize,
"prototype".to_string(),
super::super::PropertyAttrs::new(false, false, false),
);
crate::value::js_nanbox_pointer(
constructor_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as i64,
)
}

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) -> f64 {
attach_prototype(constructor_value, ASYNC_RESOURCE_METHODS)
}
93 changes: 63 additions & 30 deletions crates/perry-runtime/src/object/native_module/callable_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,23 @@ 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 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);
set_bound_native_closure_name(
closure_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>(),
name,
);
set_builtin_closure_length(
closure_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as usize,
length,
);
crate::value::js_nanbox_pointer(
closure_handle.get_raw_mut_ptr::<crate::closure::ClosureHeader>() as i64,
)
}

extern "C" fn fs_namespace_descriptor_getter_thunk(
Expand Down Expand Up @@ -246,7 +256,7 @@ fn native_callable_export_arity_reference(module: &str, prop: &str) -> Option<u3
("querystring", "escape") => 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),
Expand Down Expand Up @@ -1447,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::<crate::closure::ClosureHeader>() 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
Expand All @@ -1472,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::<crate::closure::ClosureHeader>() as usize,
"name".to_string(),
crate::object::PropertyAttrs::new(false, false, true),
);
Expand Down Expand Up @@ -1683,43 +1700,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" {
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" {
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
}

Expand Down Expand Up @@ -1817,7 +1850,7 @@ static CALLABLE_EXPORT_ARITY_TABLE: &[(&str, &[(&str, u32)])] = &[
"async_hooks",
&[
("AsyncLocalStorage", 0),
("AsyncResource", 2),
("AsyncResource", 1),
("createHook", 1),
("executionAsyncId", 0),
("executionAsyncResource", 0),
Expand Down
Loading
Loading