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/6834-http-ffi-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
refactor(http): route production HTTP bindings through `perry-ffi` and remove the normal `perry-ext-http` dependency on `perry-runtime` while preserving the selected HTTP archive ABI.
29 changes: 2 additions & 27 deletions crates/perry-ext-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,6 @@ tokio-rustls.workspace = true
rustls = { workspace = true, features = ["std", "ring", "tls12"] }
rustls-pemfile.workspace = true
tokio-tungstenite = { workspace = true }
# #2154: Agent argument validation throws `RangeError [ERR_OUT_OF_RANGE]`
# via `js_throw` + `register_error_code_pub`, which are perry-runtime's
# Rust-ABI helpers (not perry-ffi's). Stays consistent with perry-stdlib's
# parallel `http.rs` AgentHandle, which already calls into perry-runtime
# directly. Cargo feature unification keeps the stdlib feature on when
# both stdlib and this crate link the same perry-runtime — no duplicate
# symbols, no behaviour change for the default `full` build.
# #6303: perry-runtime MUST be built here with the same feature set the shipped
# `libperry_runtime.a` / `libperry_stdlib.a` carry (i.e. its `default`). This crate
# is a `staticlib`, so it BUNDLES the perry-runtime rlib objects into
# `libperry_ext_*.a` — and perry links the ext archives BEFORE stdlib/runtime
# (`prefer_well_known_before_stdlib`), so those bundled objects WIN the link for
# every symbol they define. The workspace dep is `default-features = false`, so
# without `"default"` here a per-crate `cargo build -p perry-ext-<x>` (exactly what
# release-packages.yml does in its per-crate loop) bundles a runtime with
# `regex-engine`/`temporal`/... compiled OUT. The dispatchers those features gate
# are exported UNCONDITIONALLY (`js_string_replace_search_dyn`,
# `js_native_call_method`, ...) with the feature-gated logic `#[cfg]`-ed out of the
# BODY — so the degraded copy silently ToString-coerces a RegExp argument and
# searches for it literally instead of matching it (str.replace(re, fn) never fires
# its callback). Keep `"default"` in lock-step with perry-runtime's default feature
# list; the `ext_crates_bundle_a_full_featured_perry_runtime` test (well_known.rs) guards it.
# #6314: `stdlib` drops the bundled no-op `stdlib_stubs` (js_stdlib_init_dispatch,
# ...) from this staticlib's perry-runtime copy. Linked before stdlib, the no-op
# `js_stdlib_init_dispatch` otherwise wins first-definition and never registers
# the tokio reactor — every node:http server dies on its first accept.
perry-runtime = { workspace = true, features = ["default", "external-ws-symbols", "stdlib"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "http2"], default-features = false }
tokio = { workspace = true }
# Zero-copy body chunks: reqwest::Response::chunk() yields a refcounted
Expand All @@ -66,4 +39,6 @@ lazy_static.workspace = true
socket2.workspace = true

[dev-dependencies]
# GC and async test shims call runtime internals; production code uses only perry-ffi.
perry-runtime.workspace = true
perry-ffi = { workspace = true, features = ["runtime-link"] }
73 changes: 22 additions & 51 deletions crates/perry-ext-http/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
use crate::ensure_gc_scanner_registered;
use lazy_static::lazy_static;
use perry_ffi::{
alloc_string, get_handle, get_handle_mut, iter_handles_of_mut, register_handle, GcRootVisitor,
Handle, JsClosure, JsString, JsValue, RawClosureHeader, StringHeader,
alloc_string, get_handle, get_handle_mut, iter_handles_of_mut, register_handle, ErrorKind,
GcRootVisitor, Handle, JsClosure, JsString, JsValue, RawClosureHeader, StringHeader,
};
use std::collections::HashMap;
use std::sync::Mutex;
Expand Down Expand Up @@ -253,10 +253,7 @@ fn throw_out_of_range(name: &str, bound: &str, received: f64) -> ! {
"The value of \"{}\" is out of range. It must be {}. Received {}",
name, bound, received_str
);
let msg_ptr = perry_runtime::js_string_from_bytes(message.as_ptr(), message.len() as u32);
perry_runtime::node_submodules::register_error_code_pub(msg_ptr, "ERR_OUT_OF_RANGE");
let err = perry_runtime::error::js_rangeerror_new(msg_ptr);
perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(err as i64))
perry_ffi::throw_with_code(&message, "ERR_OUT_OF_RANGE", ErrorKind::RangeError)
}

fn format_received_number(n: f64) -> String {
Expand Down Expand Up @@ -301,66 +298,47 @@ fn validate_positive(name: &str, value: f64) {
/// universe so we can't mix them on the `js_object_get_field_by_name`
/// boundary.
unsafe fn read_field_bits(obj_f64: f64, field: &str) -> Option<u64> {
let bits = obj_f64.to_bits();
let upper = bits >> 48;
let obj_ptr: *const perry_runtime::ObjectHeader = if upper >= 0x7FF8 {
(bits & PTR_MASK) as *const perry_runtime::ObjectHeader
} else if upper == 0 && bits >= 0x10000 {
bits as *const perry_runtime::ObjectHeader
} else {
return None;
};
if obj_ptr.is_null() {
return None;
}
let key = perry_runtime::js_string_from_bytes(field.as_ptr(), field.len() as u32);
let val = perry_runtime::js_object_get_field_by_name(obj_ptr, key);
if val.is_undefined() || val.is_null() {
let value = perry_ffi::object_field_by_name(JsValue::from_bits(obj_f64.to_bits()), field);
if value.is_undefined() || value.is_null() {
None
} else {
Some(val.bits())
Some(value.bits())
}
}

unsafe fn raw_object_ptr_is_null(val_f64: f64) -> bool {
let bits = val_f64.to_bits();
let upper = bits >> 48;
if upper >= 0x7FF8 {
(bits & PTR_MASK) == 0
} else {
!(upper == 0 && bits >= 0x10000)
}
unsafe fn raw_object_ptr_is_null(value: f64) -> bool {
!JsValue::from_bits(value.to_bits()).is_pointer_or_raw()
}

unsafe fn read_number_field(obj_f64: f64, field: &str) -> Option<f64> {
let bits = read_field_bits(obj_f64, field)?;
let val = perry_runtime::JSValue::from_bits(bits);
let val = JsValue::from_bits(bits);
if val.is_number() {
Some(val.to_number())
} else if val.is_int32() {
Some(val.as_int32() as f64)
Some(val.to_int32() as f64)
} else {
None
}
}

unsafe fn read_bool_field(obj_f64: f64, field: &str) -> Option<bool> {
let bits = read_field_bits(obj_f64, field)?;
let val = perry_runtime::JSValue::from_bits(bits);
let val = JsValue::from_bits(bits);
if val.is_bool() {
Some(val.as_bool())
Some(val.to_bool())
} else {
None
}
}

unsafe fn read_string_field(obj_f64: f64, field: &str) -> Option<String> {
let bits = read_field_bits(obj_f64, field)?;
let val = perry_runtime::JSValue::from_bits(bits);
let val = JsValue::from_bits(bits);
if !val.is_string() {
return None;
}
let ptr = val.as_string_ptr() as *mut perry_ffi::StringHeader;
let ptr = val.as_string_ptr();
if ptr.is_null() {
return None;
}
Expand Down Expand Up @@ -617,16 +595,7 @@ unsafe fn agent_new_with_protocol(options_f64: f64, default_protocol: &str) -> H
"The argument 'scheduling' must be one of: 'fifo', 'lifo'. Received {:?}",
s
);
let msg_ptr =
perry_runtime::js_string_from_bytes(message.as_ptr(), message.len() as u32);
perry_runtime::node_submodules::register_error_code_pub(
msg_ptr,
"ERR_INVALID_ARG_VALUE",
);
let err = perry_runtime::error::js_typeerror_new(msg_ptr);
perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(
err as i64,
))
perry_ffi::throw_with_code(&message, "ERR_INVALID_ARG_VALUE", ErrorKind::TypeError)
}
agent.scheduling = s;
}
Expand Down Expand Up @@ -857,11 +826,13 @@ fn json_value_to_string(v: &serde_json::Value) -> String {

#[no_mangle]
pub extern "C" fn js_http_agent_noop_self(handle: Handle) -> Handle {
perry_runtime::stub_diag::perry_stub_warn(
"http.Agent keepSocketAlive/reuseSocket",
"reqwest owns the keep-alive pool; per-socket hooks are no-ops",
Some("#4917"),
);
unsafe {
perry_ffi::warn_stub(
c"http.Agent keepSocketAlive/reuseSocket",
c"reqwest owns the keep-alive pool; per-socket hooks are no-ops",
Some(c"#4917"),
)
};
handle
}

Expand Down
61 changes: 28 additions & 33 deletions crates/perry-ext-http/src/client_request_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ fn null_value() -> f64 {
}

fn bool_value(value: bool) -> f64 {
f64::from_bits(perry_runtime::JSValue::bool(value).bits())
f64::from_bits(JsValue::from_bool(value).bits())
}

fn string_value(value: &str) -> f64 {
let ptr = perry_runtime::js_string_from_bytes(value.as_ptr(), value.len() as u32);
f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits())
f64::from_bits(JsValue::from_string_ptr(alloc_string(value).as_raw()).bits())
}

fn handle_value(handle: Handle) -> f64 {
Expand Down Expand Up @@ -117,12 +116,16 @@ fn remove_header_by_name(handle: Handle, name: &str) {

fn headers_array(handle: Handle, raw: bool) -> f64 {
let names = header_names(handle, raw);
let mut arr = perry_runtime::js_array_alloc(names.len() as u32);
let mut array = unsafe { perry_ffi::js_array_alloc(names.len() as u32) };
for name in names {
let ptr = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32);
arr = perry_runtime::js_array_push(arr, perry_runtime::JSValue::string_ptr(ptr));
array = unsafe {
perry_ffi::js_array_push(
array,
JsValue::from_string_ptr(alloc_string(&name).as_raw()),
)
};
}
f64::from_bits(perry_runtime::JSValue::array_ptr(arr).bits())
f64::from_bits(JsValue::from_object_ptr(array).bits())
}

fn headers_object(handle: Handle) -> f64 {
Expand All @@ -136,35 +139,29 @@ fn headers_object(handle: Handle) -> f64 {
.unwrap_or_default();
entries.sort_by(|a, b| a.0.cmp(&b.0));
entries.dedup_by(|a, b| a.0 == b.0);

let obj = perry_runtime::js_object_alloc_null_proto(0, entries.len() as u32);
let mut keys = perry_runtime::js_array_alloc(entries.len() as u32);
for (index, (key, value)) in entries.iter().enumerate() {
let key_ptr = perry_runtime::js_string_from_bytes(key.as_ptr(), key.len() as u32);
let value_ptr = perry_runtime::js_string_from_bytes(value.as_ptr(), value.len() as u32);
perry_runtime::js_object_set_field(
obj,
index as u32,
perry_runtime::JSValue::string_ptr(value_ptr),
);
keys = perry_runtime::js_array_push(keys, perry_runtime::JSValue::string_ptr(key_ptr));
}
perry_runtime::js_object_set_keys(obj, keys);
f64::from_bits(perry_runtime::JSValue::object_ptr(obj as *mut u8).bits())
let fields: Vec<(&str, JsValue)> = entries
.iter()
.map(|(key, value)| {
(
key.as_str(),
JsValue::from_string_ptr(alloc_string(value).as_raw()),
)
})
.collect();
f64::from_bits(perry_ffi::alloc_null_proto_object(&fields).bits())
}

/// `{ name: <class name> }` — stands in for `<handle>.constructor` so
/// `out.constructor.name` discriminates ClientRequest/ServerResponse the
/// way the corpus outgoing-message tests expect (#4909).
pub(crate) fn constructor_object(name: &str) -> f64 {
let obj = perry_runtime::js_object_alloc_null_proto(0, 1);
let key_ptr = perry_runtime::js_string_from_bytes("name".as_ptr(), 4);
let value_ptr = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32);
perry_runtime::js_object_set_field(obj, 0, perry_runtime::JSValue::string_ptr(value_ptr));
let mut keys = perry_runtime::js_array_alloc(1);
keys = perry_runtime::js_array_push(keys, perry_runtime::JSValue::string_ptr(key_ptr));
perry_runtime::js_object_set_keys(obj, keys);
f64::from_bits(perry_runtime::JSValue::object_ptr(obj as *mut u8).bits())
f64::from_bits(
perry_ffi::alloc_null_proto_object(&[(
"name",
JsValue::from_string_ptr(alloc_string(name).as_raw()),
)])
.bits(),
)
}

fn socket_value(handle: Handle) -> f64 {
Expand All @@ -173,9 +170,7 @@ fn socket_value(handle: Handle) -> f64 {
}
with_state_mut(handle, |state| {
if state.socket == 0.0 {
let obj = perry_runtime::js_object_alloc(0, 0);
state.socket =
f64::from_bits(perry_runtime::JSValue::object_ptr(obj as *mut u8).bits());
state.socket = f64::from_bits(perry_ffi::alloc_object().bits());
}
state.socket
})
Expand Down
22 changes: 6 additions & 16 deletions crates/perry-ext-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -851,18 +851,18 @@ unsafe fn invoke_create_socket(
// of reading an uninitialized register for the second parameter.
static REGISTER_ARITY: Once = Once::new();
REGISTER_ARITY.call_once(|| {
perry_runtime::closure::js_register_closure_arity(http_create_socket_cb as *const u8, 2);
perry_ffi::register_closure_arity(http_create_socket_cb as *const u8, 2);
});

let cb = perry_runtime::closure::js_closure_alloc(http_create_socket_cb as *const u8, 1);
let cb = perry_ffi::alloc_closure(http_create_socket_cb as *const u8, 1);
if cb.is_null() {
return;
}
// Capture the ClientRequest handle so the continuation can re-read the
// (still-stored) method/url/headers/body and resume dispatch. Stored as an
// f64 (a small registry id, not a heap pointer) — pointer-free, so it
// needs no GC layout fixup, matching `sqlite_tx_wrapper`'s db-handle slot.
perry_runtime::closure::js_closure_set_capture_f64(cb, 0, request_handle as f64);
perry_ffi::set_closure_capture_f64(cb, 0, request_handle as f64);

let cb_val = f64::from_bits(POINTER_TAG | (cb as usize as u64 & PTR_MASK));
let req_val = f64::from_bits(POINTER_TAG | (request_handle as u64 & PTR_MASK));
Expand All @@ -879,12 +879,11 @@ unsafe fn invoke_create_socket(
/// the override hands back a `net.Socket` (POINTER_TAG-boxed handle, or a bare
/// small handle on some codegen paths).
unsafe extern "C" fn http_create_socket_cb(
closure: *const perry_runtime::ClosureHeader,
closure: *const RawClosureHeader,
err: f64,
socket: f64,
) -> f64 {
let request_handle =
perry_runtime::closure::js_closure_get_capture_f64(closure, 0) as i64 as Handle;
let request_handle = perry_ffi::closure_capture_f64(closure, 0) as i64 as Handle;

// Node calls `cb(err)` on failure, `cb(null, socket)` on success.
let err_bits = err.to_bits();
Expand Down Expand Up @@ -1468,16 +1467,7 @@ unsafe fn emit_socket_timeout_overflow_warning(ms: f64) {
"{value_text} does not fit into a 32-bit signed integer.\n\
Timer duration was truncated to 2147483647."
);
let msg_ptr = perry_runtime::js_string_from_bytes(message.as_ptr(), message.len() as u32);
let label = "TimeoutOverflowWarning";
let label_ptr = perry_runtime::js_string_from_bytes(label.as_ptr(), label.len() as u32);
let msg_value = f64::from_bits(perry_runtime::JSValue::string_ptr(msg_ptr).bits());
let label_value = f64::from_bits(perry_runtime::JSValue::string_ptr(label_ptr).bits());
perry_runtime::process::js_process_emit_warning(
msg_value,
label_value,
f64::from_bits(TAG_UNDEFINED),
);
perry_ffi::emit_warning(&message, "TimeoutOverflowWarning");
}

/// `IncomingMessage.setEncoding(encoding)` for client responses. The same
Expand Down
13 changes: 6 additions & 7 deletions crates/perry-ext-http/src/response_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use std::collections::HashMap;

use perry_ffi::{alloc_string, JsValue, ObjectHeader};
use perry_ffi::{alloc_string, js_array_alloc, js_array_push, JsValue, ObjectHeader};

const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001;

Expand Down Expand Up @@ -95,14 +95,13 @@ pub(crate) fn build_response_headers_object(raw: &[(String, String)]) -> f64 {
if !obj.is_null() {
for (i, key) in order.iter().enumerate() {
let v = if key == "set-cookie" {
let mut arr = perry_runtime::js_array_alloc(set_cookie.len() as u32);
let mut arr = unsafe { js_array_alloc(set_cookie.len() as u32) };
for cookie in &set_cookie {
let ptr =
perry_runtime::js_string_from_bytes(cookie.as_ptr(), cookie.len() as u32);
arr =
perry_runtime::js_array_push(arr, perry_runtime::JSValue::string_ptr(ptr));
arr = unsafe {
js_array_push(arr, JsValue::from_string_ptr(alloc_string(cookie).as_raw()))
};
}
JsValue::from_bits(perry_runtime::JSValue::array_ptr(arr).bits())
JsValue::from_object_ptr(arr)
} else if let Some(val) = combined.get(key) {
let s = alloc_string(val);
JsValue::from_string_ptr(s.as_raw())
Expand Down
21 changes: 6 additions & 15 deletions crates/perry-ext-http/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,12 @@ static GC_REGISTERED: Once = Once::new();
pub(crate) fn ensure_gc_scanner_registered() {
GC_REGISTERED.call_once(|| {
gc_register_mutable_root_scanner_named("perry-ext-http", scan_http_server_roots);
// #2532 — register the server pump + has-active with perry-runtime
// directly. In a workspace build perry-stdlib drains these via its
// `external-http-server-pump` arm, but an out-of-tree install links
// the prebuilt full stdlib with that arm compiled OUT — so without
// this the accepted requests would never be dispatched and the
// program would hang. Registration is idempotent on the runtime
// side, so the in-tree double-drain is a harmless no-op.
extern "C" {
fn js_register_aux_pump(f: extern "C" fn() -> i32);
fn js_register_aux_has_active(f: extern "C" fn() -> i32);
}
unsafe {
js_register_aux_pump(crate::server::server::js_node_http_server_process_pending);
js_register_aux_has_active(crate::server::server::js_node_http_server_has_active);
}
// Register the extension pump for out-of-tree links where stdlib does
// not compile its HTTP pump arm. Runtime registration is idempotent.
perry_ffi::register_aux_event_pump(
crate::server::server::js_node_http_server_process_pending,
crate::server::server::js_node_http_server_has_active,
);
// Wall 10 — register the handle property/method/property-set dispatch
// extensions so erased-receiver `req.url` / `res.end(...)` etc. route to
// our handles even when the linked perry-stdlib was built WITHOUT
Expand Down
Loading