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
27 changes: 27 additions & 0 deletions crates/perry-ffi/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ extern "C" {
scanner_id: usize,
scanner: PerryFfiNamedMutableRootScanner,
);
/// perry-runtime hook: register a probe the runtime's generic method
/// dispatcher consults to tell a `register_handle` id apart from a Node
/// timer id (both occupy the pointer-tagged small-integer band). Resolved
/// at final link (perry-runtime is always linked into the binary).
fn js_register_ffi_handle_exists_probe(probe: extern "C" fn(handle: i64) -> bool);
}

/// Probe handed to perry-runtime: is `handle` a live entry in this registry?
/// Used to disambiguate a `POINTER_TAG | id` value that names both a live
/// handle and a live timer (e.g. HTTP/2 server handle 1 vs `setTimeout` id 1),
/// so the runtime routes `server.close()` to the handle rather than swallowing
/// it as `clearTimeout`. See `class_handles::ffi_handle_exists`.
extern "C" fn ffi_handle_exists_probe(handle: Handle) -> bool {
HANDLES.contains_key(&handle)
}

/// Register [`ffi_handle_exists_probe`] with perry-runtime exactly once, the
/// first time any handle is created. Done lazily (rather than at an init entry
/// point perry-ffi doesn't own) so it is wired up before any handle value can
/// reach the runtime's generic dispatcher.
fn ensure_handle_exists_probe_registered() {
use std::sync::Once;
static REGISTER: Once = Once::new();
REGISTER.call_once(|| unsafe {
js_register_ffi_handle_exists_probe(ffi_handle_exists_probe);
});
}

/// Function pointer type for native wrappers that expose mutable GC root slots.
Expand Down Expand Up @@ -193,6 +219,7 @@ impl<'a> GcRootVisitor<'a> {
/// across threads (tokio workers may resolve promises that touch
/// handle data while the main thread is also touching it).
pub fn register_handle<T: 'static + Send + Sync>(value: T) -> Handle {
ensure_handle_exists_probe_registered();
let handle = next_handle_id();
HANDLES.insert(handle, Box::new(value));
handle
Expand Down
36 changes: 36 additions & 0 deletions crates/perry-runtime/src/object/class_handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ pub type EventEmitterSetDomainFn = unsafe extern "C" fn(handle: i64, domain: i64
/// pointer-tagged small integer handles, not heap objects with class ids.
pub type NetSocketHandleProbeFn = unsafe extern "C" fn(handle: i64) -> bool;

/// Probe for live `perry-ffi` registry handles. `register_handle`-issued ids
/// and Node timer ids both occupy the pointer-tagged small-integer band and
/// both count from 1, so a bare id is ambiguous between (say) an HTTP/2 server
/// handle and a `setTimeout` id. The timer-method dispatch arm consults this
/// probe to yield to an authoritative registered handle when the id is one,
/// so `server.close()` (handle 1) is not swallowed by `clearTimeout(1)` when a
/// timer with the colliding id also happens to be alive.
pub type FfiHandleExistsProbeFn = unsafe extern "C" fn(handle: i64) -> bool;

/// Narrow registration hook for runtime code that needs to attach an
/// EventEmitter listener without routing through the generic handle dispatcher.
pub type EventEmitterOnFn =
Expand Down Expand Up @@ -143,6 +152,7 @@ static EVENT_EMITTER_ASYNC_RESOURCE_HANDLE_PROBE_PTR: AtomicPtr<()> =
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());
static FFI_HANDLE_EXISTS_PROBE_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());
static EVENT_EMITTER_ON_PTR: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());

const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001;
Expand Down Expand Up @@ -487,6 +497,32 @@ pub unsafe extern "C" fn js_register_net_socket_handle_probe(f: NetSocketHandleP
NET_SOCKET_HANDLE_PROBE_PTR.store(f as *mut (), Ordering::Release);
}

#[inline]
pub fn ffi_handle_exists_probe() -> Option<FfiHandleExistsProbeFn> {
let p = FFI_HANDLE_EXISTS_PROBE_PTR.load(Ordering::Acquire);
if p.is_null() {
None
} else {
Some(unsafe { std::mem::transmute::<*mut (), FfiHandleExistsProbeFn>(p) })
}
}

/// Returns `true` only when a probe is registered AND it confirms `id` is a
/// live `perry-ffi` registry handle. Absent a probe (no native wrapper linked)
/// this is `false`, preserving the prior behavior.
#[inline]
pub fn ffi_handle_exists(id: i64) -> bool {
match ffi_handle_exists_probe() {
Some(probe) => unsafe { probe(id) },
None => false,
}
}

#[no_mangle]
pub unsafe extern "C" fn js_register_ffi_handle_exists_probe(f: FfiHandleExistsProbeFn) {
FFI_HANDLE_EXISTS_PROBE_PTR.store(f as *mut (), Ordering::Release);
}

#[inline]
pub fn event_emitter_on() -> Option<EventEmitterOnFn> {
let p = EVENT_EMITTER_ON_PTR.load(Ordering::Acquire);
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2009,7 +2009,16 @@ pub unsafe extern "C" fn js_native_call_method(
let top16 = bits >> 48;
if top16 == 0x7FFD {
let id = (bits & 0x0000_FFFF_FFFF_FFFF) as i64;
if crate::timer::is_known_timer_id(id) {
// Timer ids and `perry-ffi` registry handles share the pointer-tagged
// small-integer band and both count from 1, so a bare id can be
// ambiguous (e.g. an HTTP/2 server handle 1 vs a `setTimeout` id 1
// alive at the same time). A live registered handle is the
// authoritative interpretation — it owns a real Rust object and its
// method surface (`close`/`ref`/`unref`/…) — so yield to the handle
// dispatch below rather than swallow `server.close()` as
// `clearTimeout`. A genuine timer whose id does not also name a live
// handle still resolves here.
if crate::timer::is_known_timer_id(id) && !super::class_handles::ffi_handle_exists(id) {
match method_name {
"ref" => {
crate::timer::js_timer_ref(id);
Expand Down
Loading