diff --git a/flake.nix b/flake.nix index a2bf5619a..f0ed3c93a 100644 --- a/flake.nix +++ b/flake.nix @@ -260,10 +260,13 @@ # thread's send wakes it, and its continuation completes via the # current_thread_syscall_return hook. run blocking_msg_demo BLOCKING_MSG_OK + # The persistent-thread doWork loop: one long-lived guest thread serves + # multiple RPC calls via dispatch, parking between them (state preserved). + run thread_call_loop_demo THREAD_LOOP_OK # daemon_demo / epoll_demo bind a filesystem unix socket; validated # locally + by the reproducible build, but skipped here since the nix # build sandbox restricts socket paths. - echo "darlingserver-rs: demos OK (link, scheduler both paths, wire codec, dispatch, routing, guest memory, mach traps, persistent threads, mach port ops, mach_msg send/recv, blocking recv)" + echo "darlingserver-rs: demos OK (link, scheduler both paths, wire codec, dispatch, routing, guest memory, mach traps, persistent threads, mach port ops, mach_msg send/recv, blocking recv, doWork loop)" touch "$out" ''; diff --git a/plan/rust-rewrite-eval.md b/plan/rust-rewrite-eval.md index 1dc86cd38..77310475a 100644 --- a/plan/rust-rewrite-eval.md +++ b/plan/rust-rewrite-eval.md @@ -242,6 +242,12 @@ gate), built reproducibly via `nix build '.?submodules=1#darlingserver-rs'`: queue); its continuation resumes, completes the receive (copyout), and delivers the result via `current_thread_syscall_return` (now wired). The message id round-trips (0xcafebabe) -> BLOCKING_MSG_OK. This is how real Darwin IPC (XPC, libdispatch) blocks. +- **Persistent-thread doWork loop (bucket B.2, multi-call)** -- one long-lived guest + thread serves MANY RPC calls over its lifetime, parking between them and resuming on + the same stack, rather than a fresh microthread per call. `thread_call_loop_demo` runs + one thread through 3 task_self_trap calls via the generated dispatch, parking between + each; a per-thread counter reaches 3 and every reply names the same task self port -> + THREAD_LOOP_OK. The real darlingserver Thread model. So every load-bearing **mechanism** is proven in running code. What remains is breadth + infrastructure + cutover, none of it research. @@ -270,11 +276,11 @@ template); the memory-touching ones depend on bucket B. Still open: `allocate_pages`/`free_pages`/`map_file`/`change_protection`, which are S2C calls (the daemon asks the guest to mmap on its own behalf), so they wait on the s2c path (item 5). -2. **Persistent per-guest Threads** -- park/resume DONE: a blocked call's microthread - persists addressable by tid and the daemon resumes the SAME thread on the awaited - event, state preserved (`persistent_threads_demo`; registry run_thread/wake_thread). - Still open: the multi-call loop (one long-lived thread serving many calls) and the - checkin/checkout lifecycle (item 3). +2. **Persistent per-guest Threads** -- DONE: a blocked call's microthread persists + addressable by tid and the daemon resumes the SAME thread on the awaited event, state + preserved (`persistent_threads_demo`); and one long-lived thread serves many calls via + the doWork loop, parking between them (`thread_call_loop_demo`). Registry + run_thread/wake_thread. Still open: the checkin/checkout lifecycle (item 3). 3. **Process/Thread lifecycle** -- checkin/checkout, fork/exec, death monitoring + reaping (pidfd/waitpid), port death notifications. 4. **The interrupt mechanism** -- signals delivered *during* a blocked call (nested diff --git a/src/external/darlingserver-rs/src/bin/thread_call_loop_demo.rs b/src/external/darlingserver-rs/src/bin/thread_call_loop_demo.rs new file mode 100644 index 000000000..80b849d60 --- /dev/null +++ b/src/external/darlingserver-rs/src/bin/thread_call_loop_demo.rs @@ -0,0 +1,115 @@ +//! The persistent-thread doWork loop (bucket B.2, the multi-call half): a single +//! long-lived guest thread serves MANY RPC calls over its lifetime, parking between +//! them and resuming on the same stack -- the real darlingserver Thread model, versus a +//! fresh microthread per call. Composes persistent threads (park/wake) with the +//! generated dispatch: the loop waits for a call, dispatches it, posts the reply, then +//! parks for the next; a per-thread stack counter proves the SAME thread served them +//! all. See plan/rust-rewrite-eval.md (bucket B.2). + +use darlingserver_rs::mach; +use darlingserver_rs::registry::Registry; +use darlingserver_rs::rpc_io::Message; +use darlingserver_rs::rpc_wire::{self, callnum, DserverRpcCallhdr, ReplyTaskSelfTrap}; +use darlingserver_rs::sched; +use std::cell::RefCell; +use std::os::fd::RawFd; +use std::rc::Rc; + +/// The daemon's handler (just task_self_trap here, to keep the loop's focus on the +/// thread model rather than breadth). +struct H; +impl rpc_wire::RpcHandler for H { + fn task_self_trap(&mut self, _fds: &[RawFd]) -> Result { + Ok(ReplyTaskSelfTrap { port_name: unsafe { mach::task_self_trap() } }) + } +} + +/// Mailbox between the daemon and the long-lived guest thread. +#[derive(Default)] +struct Mailbox { + pending: Option, + reply: Option>, + stop: bool, + served: u32, +} + +fn loop_body(mb: Rc>) -> Box { + Box::new(move || { + let mut h = H; + let mut served: u32 = 0; // per-thread state, lives on the microthread's stack + loop { + // Wait for the next call (or a stop signal), parking while idle. + loop { + let ready = { + let m = mb.borrow(); + m.pending.is_some() || m.stop + }; + if ready { + break; + } + unsafe { sched::suspend_current(None, std::ptr::null_mut(), std::ptr::null_mut()) }; + } + if mb.borrow().stop { + break; + } + let msg = mb.borrow_mut().pending.take().unwrap(); + served += 1; + let reply = rpc_wire::dispatch(&mut h, &msg); + let mut m = mb.borrow_mut(); + m.reply = reply; + m.served = served; + } + mb.borrow_mut().served = served; + }) +} + +fn task_self_trap_request(pid: u32, tid: u64) -> Message { + let hdr = DserverRpcCallhdr { number: callnum::TASK_SELF_TRAP, pid: pid as i32, tid: tid as i32, architecture: 2 }; + let data = unsafe { + std::slice::from_raw_parts(&hdr as *const _ as *const u8, std::mem::size_of::()).to_vec() + }; + Message { data, fds: vec![] } +} + +fn main() { + unsafe { run() } +} + +unsafe fn run() { + let kt = sched::init(); + let mut reg = Registry::new(kt); + let pid: u32 = 9000; + let tid: u64 = 7; + let arch: u32 = 2; + + let mb = Rc::new(RefCell::new(Mailbox::default())); + + // Spawn the long-lived thread; it parks waiting for its first call. + let parked = reg.run_thread(pid, tid, arch, loop_body(mb.clone())); + assert!(parked, "the doWork loop thread should park waiting for its first call"); + + // Serve several calls on the SAME thread; collect each reply's port name. + let mut ports = Vec::new(); + for i in 0..3 { + mb.borrow_mut().pending = Some(task_self_trap_request(pid, tid)); + let still_parked = reg.wake_thread(pid, tid); + assert!(still_parked, "thread should park again after serving call {i}"); + let reply = mb.borrow_mut().reply.take().expect("handler produced a reply"); + let port = u32::from_ne_bytes(reply[8..12].try_into().unwrap()); + ports.push(port); + } + + // Stop the loop; the thread finishes. + mb.borrow_mut().stop = true; + let still_parked = reg.wake_thread(pid, tid); + assert!(!still_parked, "thread should finish after the stop signal"); + + let served = mb.borrow().served; + eprintln!("[loop] one thread served {served} calls; task-self ports = {ports:x?}"); + + assert_eq!(served, 3, "the SAME long-lived thread must have served all 3 calls"); + assert!(ports.iter().all(|&p| p != 0), "every reply carried a valid port name"); + assert!(ports.windows(2).all(|w| w[0] == w[1]), "all replies name the same task self port (same task)"); + + println!("THREAD_LOOP_OK: one persistent guest thread served 3 RPC calls via dispatch, parking between them with per-thread state preserved (the doWork loop)"); +}