Skip to content
Closed
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
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 6 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"crates/perry-dispatch",
"crates/perry-runtime",
"crates/perry-ffi",
"crates/perry-native-registration",
"crates/perry-ext-dotenv",
"crates/perry-ext-nanoid",
"crates/perry-ext-uuid",
Expand Down Expand Up @@ -458,15 +459,10 @@ perry-hir = { path = "crates/perry-hir" }
perry-transform = { path = "crates/perry-transform" }
perry-codegen = { path = "crates/perry-codegen" }
perry-dispatch = { path = "crates/perry-dispatch" }
# #1112: `perry-ffi` is published to crates.io so npm-packaged native
# wrappers (per `perry.nativeLibrary` in package.json) can resolve it
# without a sibling Perry checkout. perry-ffi's only direct reference
# to perry-runtime is a `#[cfg(test)]` layout-assertion test which
# moves perry-runtime to a path-only `[dev-dependencies]` entry —
# `cargo publish -p perry-ffi` therefore strips it from the published
# Cargo.toml, and external users never need perry-runtime in their
# Cargo graph (Perry's compiler driver links `libperry_runtime.a`
# into the final binary directly).
# #1112: `perry-ffi` is published for external native wrappers. Its optional
# `runtime-link` edge supplies runtime symbols to in-tree adapter tests.
# The dependency-free registration core sits below both crates so future
# runtime wrappers can share identities without depending on perry-ffi.
# `default-features = false` so dependency EDGES (perry-stdlib, ext crates, the
# perry binary) don't force perry-runtime's heavy default features (`full`,
# `regex-engine`, `temporal`, `url-engine`, `string-normalize`, `intl-segmenter`)
Expand All @@ -479,6 +475,7 @@ perry-dispatch = { path = "crates/perry-dispatch" }
# `wasm-host` must stay out of `default`, generalized to all heavy features.
perry-runtime = { path = "crates/perry-runtime", version = "0.5.1011", default-features = false }
perry-ffi = { path = "crates/perry-ffi", version = "0.5.1011" }
perry-native-registration = { path = "crates/perry-native-registration", version = "0.5.1522" }
perry-ext-dotenv = { path = "crates/perry-ext-dotenv" }
perry-ext-nanoid = { path = "crates/perry-ext-nanoid" }
perry-ext-uuid = { path = "crates/perry-ext-uuid" }
Expand Down
12 changes: 12 additions & 0 deletions changelog.d/10000-native-registration-leases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Add native registry-instance identities and monotonically issued registration
serials while preserving the numeric provider ABI. Coordinate wrapper and
operation leases with pending insertion, retirement, both quarantine tiers,
and bounded id reuse across FFI, Common, and External Events registries. Net
reservations retain their private registry domain while sharing the FFI id pool.
Explicit Common ids reject occupied or retained slots. JavaScript publication
and receiver representation remain unchanged in this preparatory phase.

Place the dependency-free identity and lease state machine in
`perry-native-registration`, with the existing public types re-exported by
`perry-ffi`. This lets future runtime wrappers share registration state without
reversing FFI's optional `runtime-link` dependency on `perry-runtime`.
82 changes: 12 additions & 70 deletions crates/perry-ext-events/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use perry_ffi::{
};
use std::collections::{HashMap, HashSet};
use std::ffi::c_void;
use std::sync::{Mutex, MutexGuard, Once, OnceLock};
use std::sync::Once;

mod error_monitor;
use error_monitor::dispatch_error_monitor;
Expand Down Expand Up @@ -415,82 +415,24 @@ impl EventEmitterHandle {
}
}

type EventEmitterRegistry = Vec<Option<Box<EventEmitterHandle>>>;
mod registry;
#[cfg(test)]
use registry::drop_event_emitter_handle;
pub use registry::{
acquire_event_emitter_registration, drain_quarantined_event_emitter_handles,
event_emitter_registration, event_emitter_registry_domain,
};
use registry::{
get_event_emitter_mut, is_local_event_emitter_handle, lock_event_emitters,
register_event_emitter_handle,
};

static EVENT_EMITTERS: OnceLock<Mutex<EventEmitterRegistry>> = OnceLock::new();
static EVENTS_RUNTIME_HOOKS_REGISTERED: Once = Once::new();

thread_local! {
static EVENTS_GC_REGISTERED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}

fn event_emitters() -> &'static Mutex<EventEmitterRegistry> {
EVENT_EMITTERS.get_or_init(|| Mutex::new(Vec::new()))
}

fn lock_event_emitters() -> MutexGuard<'static, EventEmitterRegistry> {
event_emitters()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}

fn handle_index(handle: Handle) -> Option<usize> {
if !(EVENT_EMITTER_HANDLE_ID_START..EVENT_EMITTER_HANDLE_ID_END).contains(&handle) {
return None;
}
Some((handle - EVENT_EMITTER_HANDLE_ID_START) as usize)
}

fn register_event_emitter_handle(value: EventEmitterHandle) -> Handle {
let mut registry = lock_event_emitters();
if let Some((idx, slot)) = registry
.iter_mut()
.enumerate()
.find(|(_, slot)| slot.is_none())
{
*slot = Some(Box::new(value));
return EVENT_EMITTER_HANDLE_ID_START + idx as Handle;
}
let handle = EVENT_EMITTER_HANDLE_ID_START + registry.len() as Handle;
if handle >= EVENT_EMITTER_HANDLE_ID_END {
panic!("perry-ext-events handle id range exhausted");
}
registry.push(Some(Box::new(value)));
handle
}

fn event_emitter_ptr(handle: Handle) -> Option<*mut EventEmitterHandle> {
let idx = handle_index(handle)?;
let mut registry = lock_event_emitters();
let slot = registry.get_mut(idx)?.as_mut()?;
Some(&mut **slot as *mut EventEmitterHandle)
}

fn get_event_emitter_mut(handle: Handle) -> Option<&'static mut EventEmitterHandle> {
let ptr = event_emitter_ptr(handle)?;
Some(unsafe { &mut *ptr })
}

fn is_local_event_emitter_handle(handle: Handle) -> bool {
let Some(idx) = handle_index(handle) else {
return false;
};
let registry = lock_event_emitters();
registry.get(idx).is_some_and(|slot| slot.is_some())
}

#[cfg(test)]
fn drop_event_emitter_handle(handle: Handle) -> bool {
let Some(idx) = handle_index(handle) else {
return false;
};
let mut registry = lock_event_emitters();
let Some(slot) = registry.get_mut(idx) else {
return false;
};
slot.take().is_some()
}

unsafe extern "C" fn event_emitter_handle_probe(handle: i64) -> bool {
is_local_event_emitter_handle(handle)
}
Expand Down
139 changes: 139 additions & 0 deletions crates/perry-ext-events/src/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! External Events owns a domain independently of FFI's numeric allocator.
//! Registry-state and payload-vector locks are never held together.

use super::{
EventEmitterHandle, Handle, EVENT_EMITTER_HANDLE_ID_END, EVENT_EMITTER_HANDLE_ID_START,
};
#[cfg(test)]
use perry_ffi::NativeQuarantine;
use perry_ffi::{
NativeLeaseKind, NativeRegistrationIdentity, NativeRegistrationKind, NativeRegistrationLease,
NativeRegistrationRegistry, NativeRegistryDomain,
};
use std::sync::{LazyLock, Mutex, MutexGuard};

type EventEmitterRegistry = Vec<Option<Box<EventEmitterHandle>>>;
static EVENT_EMITTERS: Mutex<EventEmitterRegistry> = Mutex::new(Vec::new());
static REGISTRATIONS: LazyLock<NativeRegistrationRegistry> = LazyLock::new(|| {
NativeRegistrationRegistry::new(
EVENT_EMITTER_HANDLE_ID_START,
EVENT_EMITTER_HANDLE_ID_END,
32 * 1024,
)
});

pub fn event_emitter_registry_domain() -> NativeRegistryDomain {
REGISTRATIONS.domain()
}
pub fn event_emitter_registration(id: Handle) -> Option<NativeRegistrationIdentity> {
REGISTRATIONS.identity(id)
}
pub fn acquire_event_emitter_registration(
identity: NativeRegistrationIdentity,
kind: NativeLeaseKind,
) -> Option<NativeRegistrationLease> {
REGISTRATIONS.acquire(identity, kind)
}

/// Native preparation API; production has no payload retirement path yet.
pub fn drain_quarantined_event_emitter_handles() -> usize {
REGISTRATIONS.drain(std::time::Instant::now())
}

pub(super) fn lock_event_emitters() -> MutexGuard<'static, EventEmitterRegistry> {
EVENT_EMITTERS
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}

fn handle_index(handle: Handle) -> Option<usize> {
if !(EVENT_EMITTER_HANDLE_ID_START..EVENT_EMITTER_HANDLE_ID_END).contains(&handle) {
return None;
}
Some((handle - EVENT_EMITTER_HANDLE_ID_START) as usize)
}

pub(super) fn register_event_emitter_handle(value: EventEmitterHandle) -> Handle {
let identity = REGISTRATIONS
.begin_registration(NativeRegistrationKind::Payload)
.expect("perry-ext-events handle registration exhausted");
let handle = identity.numeric_id();
let idx = handle_index(handle).expect("allocated EventEmitter id must be in range");
{
let mut registry = lock_event_emitters();
if idx >= registry.len() {
registry.resize_with(idx + 1, || None);
}
assert!(
registry[idx].is_none(),
"pending EventEmitter id must have an empty slot"
);
registry[idx] = Some(Box::new(value));
}
assert!(REGISTRATIONS.publish(identity));
handle
}

pub(super) fn get_event_emitter_mut(handle: Handle) -> Option<&'static mut EventEmitterHandle> {
let idx = handle_index(handle)?;
let ptr = {
let mut registry = lock_event_emitters();
&mut **registry.get_mut(idx)?.as_mut()? as *mut EventEmitterHandle
};
// The existing provider contract orders payload removal after these borrows.
Some(unsafe { &mut *ptr })
}

pub(super) fn is_local_event_emitter_handle(handle: Handle) -> bool {
let Some(idx) = handle_index(handle) else {
return false;
};
lock_event_emitters()
.get(idx)
.is_some_and(|slot| slot.is_some())
}

#[cfg(test)]
pub(super) fn drop_event_emitter_handle(handle: Handle) -> bool {
let Some(identity) = REGISTRATIONS.begin_retirement(handle, NativeRegistrationKind::Payload)
else {
return false;
};
let removed = {
let mut registry = lock_event_emitters();
registry
.get_mut(handle_index(handle).unwrap())
.and_then(Option::take)
};
assert!(REGISTRATIONS.finish_retirement(identity, NativeQuarantine::NextDrain));
removed.is_some()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn retained_event_emitter_registration_blocks_empty_slot_reuse() {
let id = register_event_emitter_handle(EventEmitterHandle::new());
let identity = event_emitter_registration(id).unwrap();
let lease = acquire_event_emitter_registration(identity, NativeLeaseKind::Wrapper).unwrap();
assert!(drop_event_emitter_handle(id));
drain_quarantined_event_emitter_handles();
assert_eq!(
REGISTRATIONS.begin_registration_with_id(id, NativeRegistrationKind::Payload),
Err(perry_ffi::NativeRegistrationError::Occupied)
);
let other = register_event_emitter_handle(EventEmitterHandle::new());
assert_ne!(
other, id,
"retained Events registration must block empty-slot selection"
);
assert_ne!(
event_emitter_registry_domain(),
perry_ffi::handle_registry_domain()
);
drop(lease);
assert!(drop_event_emitter_handle(other));
}
}
33 changes: 32 additions & 1 deletion crates/perry-ext-net/src/handle_ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@
/// the `0` sentinel through [`next_id_or_throw`]; background callers must guard
/// it explicitly (never register an object under `0`).
pub(crate) fn next_id() -> i64 {
perry_ffi::reserve_handle_id()
perry_ffi::reserve_handle_id_in_domain(net_registry_domain())
}

/// One authoritative domain for net's private payload maps, sharing only the
/// numeric allocation pool with FFI's ordinary payload registry.
pub(crate) fn net_registry_domain() -> perry_ffi::NativeRegistryDomain {
static DOMAIN: std::sync::LazyLock<perry_ffi::NativeRegistryDomain> =
std::sync::LazyLock::new(|| {
perry_ffi::NativeRegistryDomain::new().expect("net native registry domains exhausted")
});
*DOMAIN
}

/// [`next_id`] for the synchronous FFI entry points (`new net.Socket()`,
Expand All @@ -54,3 +64,24 @@ pub(crate) fn next_id_or_throw() -> i64 {
}
id
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn reserved_net_id_names_the_private_domain() {
let id = next_id();
assert_ne!(id, perry_ffi::INVALID_HANDLE);
let identity = perry_ffi::handle_registration(id).unwrap();
assert_eq!(identity.domain(), net_registry_domain());
assert_ne!(identity.domain(), perry_ffi::handle_registry_domain());
let lease =
perry_ffi::acquire_handle_registration(identity, perry_ffi::NativeLeaseKind::Operation)
.unwrap();
perry_ffi::free_handle_id(id);
perry_ffi::drain_quarantined_handles();
assert!(perry_ffi::handle_registration(id).is_none());
drop(lease);
}
}
8 changes: 7 additions & 1 deletion crates/perry-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ readme = "README.md"
workspace = true

[dependencies]
perry-native-registration.workspace = true

# #1112: `perry-runtime` is restored as an OPTIONAL regular dep (not
# a dev-dep) so the `runtime-link` feature actually propagates the
# linkage into downstream test binaries (e.g.
Expand All @@ -26,7 +28,7 @@ workspace = true
# perry-runtime. The optional + path-only workspace dep means
# `cargo publish` will still error against the missing crates.io
# entry — see `scripts/publish_perry_ffi.sh` for the maintainer
# workflow (publish perry-runtime FIRST, then perry-ffi).
# workflow (publish perry-native-registration and perry-runtime before perry-ffi).
perry-runtime = { workspace = true, optional = true }

# Handle registry storage (#466 Phase 5 — `lru-cache`/db wrappers).
Expand All @@ -35,6 +37,10 @@ perry-runtime = { workspace = true, optional = true }
dashmap.workspace = true
once_cell.workspace = true

[dev-dependencies]
# Keep the existing allocator observations available only in test builds.
perry-native-registration = { workspace = true, features = ["test-support"] }

[features]
default = []
# Opt-in feature used by in-tree extension crates that need to link
Expand Down
Loading
Loading