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
17 changes: 16 additions & 1 deletion .github/workflows/release-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,21 @@ jobs:
cargo build --release --target ${{ matrix.target }} -p perry-runtime
cargo build --release --target ${{ matrix.target }} -p perry-stdlib

- name: Build panic=abort runtime variant (Unix)
# Out-of-tree installs can't rebuild the runtime, so ship the
# panic=abort profile prebuilt: `perry compile` links it for
# runtime-only apps with no catch_unwind callers (games, CLIs),
# dropping unwind tables/landing pads (~12-18% of the binary).
# See optimized_libs.rs (find_runtime_abort_library selection).
# Separate CARGO_TARGET_DIR so the profile override doesn't
# invalidate the main build's incremental cache.
if: runner.os != 'Windows'
run: |
CARGO_TARGET_DIR=target-abort CARGO_PROFILE_RELEASE_PANIC=abort \
cargo build --release --target ${{ matrix.target }} -p perry-runtime
cp "target-abort/${{ matrix.target }}/release/libperry_runtime.a" \
"target/${{ matrix.target }}/release/libperry_runtime_abort.a"

- name: Build native ext libraries (Unix)
# #2532 — the perry-ext-* wrapper crates ship the host functions for
# node:http (server), ws, net, zlib, fastify, the db drivers, etc.
Expand Down Expand Up @@ -430,7 +445,7 @@ jobs:
mkdir -p staging
cp target/${{ matrix.target }}/release/perry staging/
# Include static libraries for linking (runtime, stdlib, UI)
for lib in libperry_runtime.a libperry_stdlib.a libperry_ui_macos.a libperry_ui_gtk4.a; do
for lib in libperry_runtime.a libperry_runtime_abort.a libperry_stdlib.a libperry_ui_macos.a libperry_ui_gtk4.a; do
if [ -f "target/${{ matrix.target }}/release/$lib" ]; then
cp "target/${{ matrix.target }}/release/$lib" staging/
fi
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/node_v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ pub extern "C" fn js_v8_write_heap_snapshot(filename: f64, options: f64) -> f64
/// Build a 2-field native-module namespace object: field[0] = `module` tag,
/// field[1] = registry `id`. NOT cached (every `new` must be a fresh instance).
unsafe fn build_v8_instance(module: &str, id: usize) -> f64 {
crate::object::install_native_module_vtable();
let obj = crate::object::js_object_alloc(crate::object::NATIVE_MODULE_CLASS_ID, 2);
let mname = js_string_from_bytes(module.as_ptr(), module.len() as u32);
crate::object::js_object_set_field(obj, 0, JSValue::string_ptr(mname));
Expand Down Expand Up @@ -617,6 +618,7 @@ pub extern "C" fn js_v8_promise_hook_register() -> f64 {
pub extern "C" fn js_v8_gc_profiler_new() -> f64 {
unsafe {
let module = "v8.GCProfiler";
crate::object::install_native_module_vtable();
let obj = crate::object::js_object_alloc(crate::object::NATIVE_MODULE_CLASS_ID, 2);
let module_name = js_string_from_bytes(module.as_ptr(), module.len() as u32);
crate::object::js_object_set_field(obj, 0, JSValue::string_ptr(module_name));
Expand Down
8 changes: 6 additions & 2 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5178,7 +5178,9 @@ unsafe fn try_native_static_method_in_proto_chain(
let module = b"buffer.Buffer";
let ns = js_create_native_module_namespace(module.as_ptr(), module.len());
let ns_obj = JSValue::from_bits(ns.to_bits()).as_pointer::<ObjectHeader>();
let result = dispatch_native_module_method(ns_obj, name, args_ptr, args_len);
let result = crate::object::native_module::call_native_module_dispatch_hook(
ns_obj, name, args_ptr, args_len,
);
if !JSValue::from_bits(result.to_bits()).is_undefined() {
return Some(result);
}
Expand All @@ -5189,7 +5191,9 @@ unsafe fn try_native_static_method_in_proto_chain(
if read_native_module_name(proto_obj as *const ObjectHeader).as_deref()
== Some("buffer.Buffer")
{
let result = dispatch_native_module_method(proto_obj, name, args_ptr, args_len);
let result = crate::object::native_module::call_native_module_dispatch_hook(
proto_obj, name, args_ptr, args_len,
);
if !JSValue::from_bits(result.to_bits()).is_undefined() {
return Some(result);
}
Expand Down
112 changes: 21 additions & 91 deletions crates/perry-runtime/src/object/field_get_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1775,28 +1775,11 @@ pub extern "C" fn js_object_keys(obj: *const ObjectHeader) -> *mut ArrayHeader {
}
unsafe {
if (*obj).class_id == NATIVE_MODULE_CLASS_ID {
if let Some(module_name) = read_native_module_name(obj) {
if let Some(keys) = native_module_enumerable_keys(&module_name) {
let include_permission = matches!(
module_name.as_str(),
"process" | "process.namespace" | "process.default"
) && crate::process::process_permission_enabled();
let out =
crate::array::js_array_alloc(keys.len() as u32 + include_permission as u32);
for key_bytes in keys {
let key_str = crate::string::js_string_from_bytes(
key_bytes.as_ptr(),
key_bytes.len() as u32,
);
crate::array::js_array_push(out, JSValue::string_ptr(key_str));
}
if include_permission {
let key_str = crate::string::js_string_from_bytes(
b"permission".as_ptr(),
b"permission".len() as u32,
);
crate::array::js_array_push(out, JSValue::string_ptr(key_str));
}
// Relocated to native_module.rs::vt_own_keys_array so the
// module key tables are reachable only through the vtable
// (linker-strippable when no namespace object exists).
if let Some(vt) = super::native_module::native_module_vtable() {
if let Some(out) = (vt.own_keys_array)(obj) {
return out;
}
}
Expand Down Expand Up @@ -2329,7 +2312,8 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 {
.as_deref()
.zip(super::has_own_helpers::str_from_string_header(key_ptr))
.map(|(module, key)| {
super::native_module::native_module_has_enumerable_key(module, key)
super::native_module::native_module_vtable()
.is_some_and(|vt| (vt.has_enumerable_key)(module, key))
})
.unwrap_or(false);
return if present { nanbox_true } else { nanbox_false };
Expand Down Expand Up @@ -2401,7 +2385,10 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 {
};
let present = unsafe { read_native_module_name(obj_ptr) }
.as_deref()
.is_some_and(|module_name| native_module_has_enumerable_key(module_name, key_name));
.is_some_and(|module_name| {
super::native_module::native_module_vtable()
.is_some_and(|vt| (vt.has_enumerable_key)(module_name, key_name))
});
return if present { nanbox_true } else { nanbox_false };
}

Expand Down Expand Up @@ -2658,7 +2645,7 @@ fn reified_function_method_name(name: &str) -> Option<&'static [u8]> {
}
}

unsafe fn native_module_own_field_by_key(
pub(super) unsafe fn native_module_own_field_by_key(
obj: *const ObjectHeader,
key: *const crate::StringHeader,
) -> Option<JSValue> {
Expand Down Expand Up @@ -4616,74 +4603,17 @@ pub extern "C" fn js_object_get_field_by_name(
// lookup fell through to the field-bag scan (which only stores
// `__module__`) and returned undefined. Now we route through
// `get_native_module_constant` directly.
// Issue #649 / #3687 / #894: native-module own-field reads
// (sub-namespaces, process IPC props, callable exports). Body
// relocated to native_module.rs::vt_get_own_field so the
// (module, method) tables are reachable only through the vtable.
// `None` (no module name / vtable uninstalled) falls through to
// the generic scans below, matching the pre-relocation flow.
if (*obj).class_id == NATIVE_MODULE_CLASS_ID && !key.is_null() {
let key_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
let key_len = (*key).byte_len as usize;
let nb_ptr = crate::value::js_nanbox_pointer(obj as i64);
let module_name = get_module_name_from_namespace(nb_ptr);
if !module_name.is_empty() {
let property_name =
std::str::from_utf8(std::slice::from_raw_parts(key_ptr, key_len)).unwrap_or("");
if matches!(
module_name,
"process" | "process.namespace" | "process.default"
) {
if let Some(value) = crate::process::process_ipc_property(property_name) {
return JSValue::from_bits(value.to_bits());
}
}
if let Some(value) = native_module_own_field_by_key(obj, key) {
return value;
}
// #3687: node:cluster default-import EventEmitter methods on the
// distinct `cluster.default` namespace. Mirror the
// NativeModuleRef fast path (`js_native_module_property_by_name`)
// — this dynamic `obj[key]` read must resolve `on`/`emit`/… to
// bound methods *before* `get_native_module_constant` (which
// normalizes to `cluster` and returns `undefined` for `on`).
if module_name == "cluster.default"
&& super::is_cluster_emitter_method(property_name)
{
return JSValue::from_bits(
super::bound_native_callable_export_value(module_name, property_name)
.to_bits(),
);
}
if let Some(val) = get_native_module_constant(module_name, property_name, nb_ptr) {
return JSValue::from_bits(val.to_bits());
}
if module_name == "crypto.webcrypto" {
if let Some(value) = super::global_this::webcrypto_method_value(property_name) {
return JSValue::from_bits(value.to_bits());
}
}
if module_name == "crypto.subtle" {
if let Some(value) =
super::global_this::subtle_crypto_method_value(property_name)
{
return JSValue::from_bits(value.to_bits());
}
}
// Issue #894: parity with the direct-NativeModuleRef
// fast path (`js_native_module_property_by_name`). For
// (module, prop) pairs whose property-read should
// produce a callable handle — e.g.
// `("events", "EventEmitter")` — synthesize the same
// BOUND_METHOD_FUNC_PTR closure so the require-then-
// member-access shape (`const { EventEmitter } =
// require("node:events")`) matches the direct
// namespace-import shape (`import { EventEmitter } from
// "node:events"`). Pre-fix the slow path returned
// undefined here, and the downstream
// `EventEmitter.prototype` read tripped the spec
// "Cannot read properties of undefined" throw.
if is_native_module_callable_export(module_name, property_name) {
return JSValue::from_bits(
super::bound_native_callable_export_value(module_name, property_name)
.to_bits(),
);
if let Some(vt) = super::native_module::native_module_vtable() {
if let Some(v) = (vt.get_own_field)(obj, key) {
return v;
}
return JSValue::undefined();
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) mod iterator_prototypes;
mod namespace_create;
mod native_call_method;
mod native_module;
pub(crate) use native_module::install_native_module_vtable;
mod native_module_crypto_key_object;
mod native_module_crypto_random;
mod native_module_dispatch;
Expand Down
14 changes: 12 additions & 2 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3453,7 +3453,12 @@ pub unsafe extern "C" fn js_native_call_method(
// #853: the `is_valid_obj_ptr` guard that used to live after
// this return was dead — the early return claims the path
// unconditionally. Removed.
return dispatch_native_module_method(obj, method_name, args_ptr, args_len);
return crate::object::native_module::call_native_module_dispatch_hook(
obj,
method_name,
args_ptr,
args_len,
);
}
// Issue #1206: Buffer iterators returned from `buf.values()` etc.
// have a dedicated class id so `.next()` lands here and dispatches
Expand Down Expand Up @@ -3977,7 +3982,12 @@ pub unsafe extern "C" fn js_native_call_method(
// Check for native module namespace
if (*obj).class_id == NATIVE_MODULE_CLASS_ID {
// #853: same dead-after-return as the first arm above.
return dispatch_native_module_method(obj, method_name, args_ptr, args_len);
return crate::object::native_module::call_native_module_dispatch_hook(
obj,
method_name,
args_ptr,
args_len,
);
}
// Issue #1206: same class-id check as the NaN-boxed path above
// so a raw-pointer iterator value (uncommon, but possible after
Expand Down
Loading