ipc_call and ipc_reply_wait grow a `send_cap` argument (r9) and a `received_cap` return (r8): an endpoint travels alongside a message, installed into the receiver's handle table. The transfer is a share, not a move — the endpoint's refcount is bumped and the sender keeps its handle. If the receiver's table is full the call fails -ENOSPC and the message is NOT delivered (a half-delivered capability is worse than a failed send); a bad handle fails -EBADF. Both directions carry a cap: a client's call hands one to the server (seen in the server's replyWait), and the server's reply hands one back (seen in the client's call return). This is the "open" primitive the driver model was blocked on: a bus driver mints a per-device endpoint and hands it to a class driver, giving it a private channel to one device without the 8-slot global name registry. Kernel: shareCapability in ipc-synchronous.zig at both copy points; new setSystemCallResult3 (r8, saved/restored by the syscall stub); Task gains ipc_send_cap / ipc_received_cap. Runtime: callCap + Reply, replyWait gains send_cap and Received.cap; plain call/replyWait delegate with no_cap. New abi.no_cap. New ipc-cap test (two kernel tasks exercise both directions, each verifying the endpoint it received is the same object shared, refcount bumped to 2). No class driver consumes callCap yet — it lands with the first one. Suite 37/37 plus host tests.
128 lines
5.5 KiB
Zig
128 lines
5.5 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.zig");
|
|
|
|
/// 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 createEndpoint() ?Handle {
|
|
const r = sc.systemCall0(.create_endpoint);
|
|
return if (failed(r)) null else r;
|
|
}
|
|
|
|
/// Publish endpoint `h` under a well-known service id so other processes find it.
|
|
pub fn register(id: abi.ServiceId, h: Handle) bool {
|
|
return !failed(sc.systemCall2(.ipc_register, @intFromEnum(id), h));
|
|
}
|
|
|
|
/// Find the endpoint published under `id`, installing a handle to it in this
|
|
/// process.
|
|
pub fn lookup(id: abi.ServiceId) ?Handle {
|
|
const r = sc.systemCall1(.ipc_lookup, @intFromEnum(id));
|
|
return if (failed(r)) null else r;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// 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;
|
|
|
|
/// 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 a device interrupt, not a client request. A driver's
|
|
/// 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;
|
|
}
|
|
|
|
/// The interrupt source (a GSI), meaningful only when `isNotification`.
|
|
pub fn source(self: Received) u64 {
|
|
return self.badge & ~notify_badge_bit;
|
|
}
|
|
};
|
|
|
|
/// 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 };
|
|
}
|