From 2f28d8112970960dbb9b6b582a3c6cd259337d21 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 3 Aug 2026 01:05:57 +0900 Subject: feat(php-rpc): rework the RPC channel into the tagged plugin protocol Replace the name\0arg framing with the plugin wire protocol: tagged frames with corr_id multiplexing, a MAX_FRAME_LEN bound, a thread-ID based reentrant SessionLock, and the PluginValue codec (encoder plus the first recursive decoder, iterative with a 512-level depth cap). Float formatting is ported from php-src into shirabe-php-src so the encoder is byte-compatible with serialize() under serialize_precision=-1, which the spawned worker now pins. The worker gains a standing dispatch loop, CallRustMethod reentrancy, hand-written Event proxy stubs, and explicit-error answers for everything not implemented yet. The public query API (get_php_version and friends) is unchanged and now rides the new protocol; the codec is verified against real PHP by roundtrip oracle tests covering floats, non-UTF-8 bytes and deep nesting. Co-Authored-By: Claude Fable 5 --- crates/shirabe-php-rpc/src/session.rs | 110 ++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 crates/shirabe-php-rpc/src/session.rs (limited to 'crates/shirabe-php-rpc/src/session.rs') diff --git a/crates/shirabe-php-rpc/src/session.rs b/crates/shirabe-php-rpc/src/session.rs new file mode 100644 index 00000000..29bd89f3 --- /dev/null +++ b/crates/shirabe-php-rpc/src/session.rs @@ -0,0 +1,110 @@ +//! Thread-ID based reentrant session lock. +//! +//! Guarantees at most one logical RPC "call session" is in flight against the shared worker at +//! any time, while allowing the owning thread to recurse into it freely (a handler may itself +//! call back into the other side). A different thread attempting to start a session blocks until +//! the entire outer session (including all of its nested calls) completes — it is never rejected +//! or panicked on, only serialized. This keeps the "exactly one child process" invariant intact +//! even when multiple OS threads use this crate concurrently, which happens today only under +//! `cargo test`'s parallel test harness but is not assumed to be forbidden in the future. + +use std::sync::{Condvar, LazyLock, Mutex}; + +struct SessionLock { + owner: Mutex>, + cvar: Condvar, +} + +impl SessionLock { + fn acquire(&self) { + let mut owner = self.owner.lock().unwrap(); + let me = std::thread::current().id(); + loop { + match *owner { + None => { + *owner = Some((me, 1)); + return; + } + Some((tid, depth)) if tid == me => { + *owner = Some((me, depth + 1)); + return; + } + Some(_) => { + owner = self.cvar.wait(owner).unwrap(); + } + } + } + } + + fn release(&self) { + let mut owner = self.owner.lock().unwrap(); + let me = std::thread::current().id(); + match *owner { + Some((tid, depth)) if tid == me => { + if depth == 1 { + *owner = None; + self.cvar.notify_all(); + } else { + *owner = Some((me, depth - 1)); + } + } + _ => unreachable!("SessionLock::release without a matching acquire on this thread"), + } + } +} + +static SESSION: LazyLock = LazyLock::new(|| SessionLock { + owner: Mutex::new(None), + cvar: Condvar::new(), +}); + +/// RAII guard; acquired once at the outermost `rpc_call`, re-entered (depth += 1, no blocking) +/// by nested calls from the same thread. +pub struct SessionGuard; + +impl SessionGuard { + pub fn enter() -> Self { + SESSION.acquire(); + SessionGuard + } +} + +impl Drop for SessionGuard { + fn drop(&mut self) { + SESSION.release(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn same_thread_reenters_without_blocking() { + let _outer = SessionGuard::enter(); + let _inner = SessionGuard::enter(); + } + + #[test] + fn other_threads_are_serialized() { + use std::sync::Arc; + use std::sync::atomic::{AtomicU32, Ordering}; + + let concurrent = Arc::new(AtomicU32::new(0)); + let mut handles = Vec::new(); + for _ in 0..4 { + let concurrent = Arc::clone(&concurrent); + handles.push(std::thread::spawn(move || { + for _ in 0..50 { + let _guard = SessionGuard::enter(); + let now = concurrent.fetch_add(1, Ordering::SeqCst); + assert_eq!(now, 0, "two sessions were in flight at once"); + concurrent.fetch_sub(1, Ordering::SeqCst); + } + })); + } + for handle in handles { + handle.join().unwrap(); + } + } +} -- cgit v1.3.1