aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/src/session.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-03 01:05:57 +0900
committernsfisis <nsfisis@gmail.com>2026-08-03 01:05:57 +0900
commit2f28d8112970960dbb9b6b582a3c6cd259337d21 (patch)
treeaf8bc223b3af9098e6925cbf6c47e198459ac818 /crates/shirabe-php-rpc/src/session.rs
parentbf2c6fa58ae51f44fa0ec65c615f8e62de87812c (diff)
downloadphp-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.rs110
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();
+ }
+ }
+}