diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-03 01:05:57 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-03 01:05:57 +0900 |
| commit | 2f28d8112970960dbb9b6b582a3c6cd259337d21 (patch) | |
| tree | af8bc223b3af9098e6925cbf6c47e198459ac818 /crates/shirabe-php-rpc/src/session.rs | |
| parent | bf2c6fa58ae51f44fa0ec65c615f8e62de87812c (diff) | |
| download | php-shirabe-2f28d8112970960dbb9b6b582a3c6cd259337d21.tar.gz php-shirabe-2f28d8112970960dbb9b6b582a3c6cd259337d21.tar.zst php-shirabe-2f28d8112970960dbb9b6b582a3c6cd259337d21.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-rpc/src/session.rs')
| -rw-r--r-- | crates/shirabe-php-rpc/src/session.rs | 110 |
1 files changed, 110 insertions, 0 deletions
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<Option<(std::thread::ThreadId, u32)>>, + 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<SessionLock> = 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(); + } + } +} |
