danos/library/kernel/ipc.zig

246 lines
11 KiB
Zig

//! User-space IPC helpers over the kernel's synchronous IPC syscalls. A client
//! `call`s an endpoint (send + block for reply); the VFS server and drivers are
//! reached this way. The server side (`replyWait`, which returns two values) is
//! added with the first server binary.
const abi = @import("abi");
const sc = @import("system-call");
/// A small-int handle into the calling process's handle table.
pub const Handle = usize;
/// A fixed-size, register-friendly message payload. Server protocols (VFS, driver)
/// layer their own wire format on top of the bytes a call carries.
pub const Message = extern struct {
tag: u64 = 0,
a: u64 = 0,
b: u64 = 0,
c: u64 = 0,
};
/// Whether a system_call return value is a wrapped -errno (lands in the top page).
inline fn failed(r: usize) bool {
return r > ~@as(usize, 0) - 4095;
}
/// Create a new endpoint owned by this process; returns its handle.
pub fn createIpcEndpoint() ?Handle {
const r = sc.systemCall0(.create_ipc_endpoint);
return if (failed(r)) null else r;
}
// `register`/`lookup` lived here — the two wrappers over the flat ServiceId
// registry. Naming is not a system call any more: a provider binds its contract
// name at the registry and a client resolves and opens `/protocol/<name>`, both
// through `channel` (docs/os-development/protocol-namespace.md).
/// Drop a capability handle (endpoint, shared-memory, or DMA-region) and free its table
/// slot. A forwarding hop closes a cap it passed on; a binder closes a DMA-region cap
/// once the binding holds its own reference — the 32-slot table is otherwise consumed by
/// repeated cap-passing.
pub fn close(h: Handle) bool {
return !failed(sc.systemCall1(.handle_close, h));
}
pub const CallError = error{Failed};
/// The result of a capability-passing `callCap`: the reply length, and the handle of
/// an endpoint the server sent back (e.g. a per-device channel), or null.
pub const Reply = struct {
len: usize,
cap: ?Handle,
};
/// Send `message` to endpoint `h` and block until the server replies into `reply`,
/// optionally handing the server a capability (`send_cap`) and receiving one back.
/// This is the class-driver "open" primitive: call a bus with `send_cap = null`, get a
/// private per-device endpoint back in `.cap`. Two return values (reply length in rax,
/// received handle in r8) need a hand-written stub — r8 is read-write (in: reply
/// capacity, arg #4; out: the received handle).
pub fn callCap(h: Handle, message: []const u8, reply: []u8, send_cap: ?Handle) CallError!Reply {
var rax: usize = undefined;
var r8: usize = reply.len; // in: reply capacity (arg #4); out: received capability handle
asm volatile ("syscall"
: [rax] "={rax}" (rax),
[r8] "+{r8}" (r8),
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_call)),
[a0] "{rdi}" (h),
[a1] "{rsi}" (@intFromPtr(message.ptr)),
[a2] "{rdx}" (message.len),
[a3] "{r10}" (@intFromPtr(reply.ptr)),
[a5] "{r9}" (send_cap orelse abi.no_cap),
: .{ .rcx = true, .r11 = true, .memory = true });
if (failed(rax)) return error.Failed;
return .{ .len = rax, .cap = if (r8 == abi.no_cap) null else r8 };
}
/// Send `message` to endpoint `h` and block until the server replies into `reply`.
/// Returns the reply length. The common case: no capability passed either way.
pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
return (try callCap(h, message, reply, null)).len;
}
/// Post `message` to endpoint `h`'s asynchronous queue and return immediately — no
/// rendezvous, no reply, no blocking. The receiver picks it up through `replyWait` as a
/// buffered message (`Received.isMessage`). Unlike `call`, this **cannot hang on a dead
/// or slow peer**, which is why a broadcaster (the input service) delivers events this
/// way. The payload must fit an endpoint slot (64 bytes); a full queue drops the oldest
/// message. Returns false on failure (bad handle, oversized payload, bad buffer).
pub fn send(h: Handle, message: []const u8) bool {
return !failed(sc.systemCall3(.ipc_send, h, @intFromPtr(message.ptr), message.len));
}
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
/// bound device interrupt — rather than a client's message. The low bits carry the
/// GSI. See `isNotification`.
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
/// Set alongside `notify_badge_bit` when the notification is a **signal** — the
/// lifecycle vocabulary of docs/process-lifecycle.md, delivered to the endpoint
/// nominated with `process.bindSignals`. Decode with `process.signalsFrom`.
pub const notify_signal_bit: u64 = abi.notify_signal_bit;
/// Set alongside `notify_badge_bit` when the notification is a **one-shot timer**
/// landing (`system.timerOnce`).
pub const notify_timer_bit: u64 = abi.notify_timer_bit;
/// Set alongside `notify_badge_bit` when the notification is a **child-exit
/// notice** — a process this one spawned (with an exit endpoint) has ended —
/// rather than a device interrupt. The low bits carry the child's process id.
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
/// Set alongside `notify_badge_bit` when the wake-up is a **buffered message** — a payload
/// posted with `send` (`ipc_send`) — rather than a bare device interrupt or child-exit
/// notice. The payload is in the `replyWait` receive buffer (`Received.len` bytes); the
/// low bits of the badge carry the sender's task id. See `Received.isMessage`.
pub const notify_message_bit: u64 = abi.notify_message_bit;
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
/// an IRQ notification if the high bit is set), and any capability the request carried.
pub const Received = struct {
len: usize,
badge: u64,
cap: ?Handle,
/// True if this wake-up was an asynchronous notification (a device interrupt
/// or a child-exit notice), not a client request. An event loop branches on
/// this; there is no reply owed on the notification path.
pub fn isNotification(self: Received) bool {
return self.badge & notify_badge_bit != 0;
}
/// True if this wake-up tells of a supervised child's end — the notification
/// requested by passing an exit endpoint to `system.spawnSupervised`.
pub fn isChildExit(self: Received) bool {
return self.isNotification() and self.badge & notify_exit_bit != 0;
}
/// True if this wake-up is a **buffered message** posted with `send` (`ipc_send`):
/// there is a payload in the receive buffer (`self.len` bytes) and no reply is owed.
/// The subscriber side of a broadcast branches on this.
pub fn isMessage(self: Received) bool {
return self.isNotification() and self.badge & notify_message_bit != 0;
}
/// The task id of whoever posted a buffered message, meaningful only when
/// Whether this arrival is a signal notification — decode the set with
/// `process.signalsFrom(badge)`.
pub fn isSignal(self: Received) bool {
return self.isNotification() and self.badge & notify_signal_bit != 0;
}
/// Whether this arrival is a one-shot timer landing (`system.timerOnce`).
pub fn isTimer(self: Received) bool {
return self.isNotification() and self.badge & notify_timer_bit != 0;
}
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
pub fn senderTaskId(self: Received) u32 {
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
}
/// The interrupt source (a GSI), meaningful only when `isNotification` and
/// not `isChildExit`.
pub fn source(self: Received) u64 {
return self.badge & ~notify_badge_bit;
}
/// The ended child's process id, meaningful only when `isChildExit`.
pub fn childProcessId(self: Received) u32 {
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit));
}
};
/// A capability that arrived with one turn of a receive loop, and the ownership
/// rule for it: **the turn owns it until a handler takes it, and closes whatever
/// is left.**
///
/// The kernel installs a sent capability in the receiver's handle table whenever
/// the caller attached one, *independent of the message's length or kind*
/// (system/kernel/ipc-synchronous.zig `replyWait`), so every path out of a loop
/// has to dispose of one — including the paths that never look at the message.
/// The table is thirty-two slots, and `ipc_call` does not dedupe, so a client
/// looping on `callCap(server, &.{}, endpoint)` spends one slot per call: about
/// thirty-two zero-length pings and the service can never accept another
/// capability, which means no subscribe and no shared-memory handover, for the
/// rest of the boot. It is unauthenticated and it is two lines to write.
///
/// So ownership is structural rather than a close per branch — the per-branch
/// version has already failed twice in this tree, in PID 1's ping path and in
/// every `service.run` callback that simply ignored its capability argument.
/// Written this way, forgetting **closes**, and *keeping* a capability is the
/// thing a handler has to say out loud:
///
/// ```zig
/// var arrived: ipc.Arrival = .{ .handle = got.cap };
/// defer arrived.release(); // every exit path, including `continue`
/// ...
/// const kept = arrived.take().?; // claimed: mine to hold or close
/// ```
pub const Arrival = struct {
handle: ?Handle = null,
/// Look without claiming — a handler that may still refuse wants no close of
/// its own on the refusal paths.
pub fn peek(self: *const Arrival) ?Handle {
return self.handle;
}
/// Claim ownership: from here the capability is the taker's to keep or close,
/// and the turn will not touch it.
pub fn take(self: *Arrival) ?Handle {
defer self.handle = null;
return self.handle;
}
/// Close whatever nobody claimed. Idempotent, so it is safe as a `defer` next
/// to any number of `take`s.
pub fn release(self: *Arrival) void {
if (self.handle) |handle| _ = close(handle);
self.handle = null;
}
};
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if any,
/// optionally handing it `send_cap`), then block until the next request arrives in
/// `receive`. Returns its length, the sender badge, and any capability the request
/// carried (in `.cap`). Three return values — length in rax, badge in rdx, received
/// handle in r8 — so it needs a hand-written stub: rdx is read-write (in: reply length,
/// arg #3; out: badge) and r8 is read-write (in: receive capacity, arg #4; out: handle).
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8, send_cap: ?Handle) Received {
var rax: usize = undefined;
var rdx: usize = reply.len; // in: reply_len (arg #3); out: badge
var r8: usize = receive.len; // in: receive capacity (arg #4); out: received capability handle
asm volatile ("syscall"
: [rax] "={rax}" (rax),
[rdx] "+{rdx}" (rdx),
[r8] "+{r8}" (r8),
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_reply_wait)),
[a0] "{rdi}" (h),
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
[a3] "{r10}" (@intFromPtr(receive.ptr)),
[a5] "{r9}" (send_cap orelse abi.no_cap),
: .{ .rcx = true, .r11 = true, .memory = true });
return .{ .len = rax, .badge = rdx, .cap = if (r8 == abi.no_cap) null else r8 };
}