//! `Channel` — layer L1 of the communication stack //! (docs/os-development/communication.md) made concrete. A program holds a //! channel that speaks a protocol; it does not hold a raw handle and marshal //! bytes at one. The channel is the answer to "who am I talking to", decided //! once at establishment, so nothing after that ever routes a party again: //! every packet's `target` addresses an *object* within the peer already chosen. //! //! **Possession of the Channel is the connection.** There is no connect step, no //! session id, no reconnect handshake — the endpoint capability inside is the //! whole of the relationship, and it cannot be forged, only handed over. Which //! also means a channel is a resource: `close` it, or it occupies a handle-table //! slot for the life of the process. //! //! **A dead provider surfaces as `-EPEER`, and the recovery is to re-open.** //! When the process on the other end exits, the kernel fails calls on its //! endpoint rather than blocking forever; `call` returns null. The client does //! not repair the channel — it discards it and opens the name again, which //! reaches whatever instance the registry now points at. The restart story //! falls out of the naming layer for free; no protocol needs a reconnect verb. //! //! `open` resolves a `/protocol/` path through the kernel VFS router and //! takes the provider's endpoint from the open reply's capability. The registry //! answering it is init, PID 1, which mounts `/protocol` before it spawns anyone //! (docs/os-development/protocol-namespace.md); `bind` below is the other half — //! how a provider claims the name in the first place. const std = @import("std"); const ipc = @import("ipc"); const time = @import("time"); const file_system = @import("file-system"); const vfs_protocol = @import("vfs-protocol"); const envelope = @import("envelope"); /// Longest `/protocol/...` path this client marshals. The registry's names are /// short by construction (a contract leaf, not a file path), and the buffer is /// on the stack of whoever opens. pub const path_maximum: usize = 224; /// Where the protocol namespace is rooted — the one path prefix in the system /// that names contracts rather than files. Spelled once, here, so no caller /// builds it by hand (docs/file-system-development/file-system-hierarchy.md). pub const root: []const u8 = "/protocol"; /// Longest contract name — the part after `/protocol/`. Short by construction: /// a leaf like `display`, or a subtree leaf like `test/shared-memory`. pub const name_maximum: usize = 64; /// What a `call` came back with: the provider's status, the reply payload (the /// bytes after the `Status`, in the caller's own buffer), and any capability the /// reply carried. pub const Response = struct { status: envelope.Status, payload: []u8, capability: ?ipc.Handle, /// Whether the provider answered success. A negative status is its refusal /// (`-ENOSYS` for a verb it does not implement, and so on). pub fn succeeded(self: Response) bool { return self.status.status == 0; } }; /// An open conversation with one provider, speaking one protocol. pub const Channel = struct { /// The provider's endpoint. Sending into it is the only thing this handle /// can do — an endpoint is a mailbox owned by its creator, and that /// direction never reverses. endpoint: ipc.Handle, /// Adopt an endpoint that arrived some other way — a capability delivered /// in a reply, or one a supervisor wired in at spawn time (P5). The channel /// takes ownership of the handle. pub fn adopt(endpoint: ipc.Handle) Channel { return .{ .endpoint = endpoint }; } /// Establish a channel by name: resolve `/protocol/` to the registry /// backend, `open` the contract there, and take the provider's endpoint from /// the reply's capability. Null if the path does not resolve, the registry /// refuses (an ungranted name is refused *as* not-found), or the reply /// carries no capability. /// /// The path is spoken exactly once, here. Everything afterwards is integers /// in the packet header. pub fn open(path: []const u8) ?Channel { return .{ .endpoint = openPath(path) orelse return null }; } /// Establish a channel by contract name — `open` with `/protocol/` supplied, /// which is how every caller in the system spells it. pub fn connect(name: []const u8) ?Channel { return .{ .endpoint = openEndpoint(name) orelse return null }; } /// Send one request packet and block for the reply: `[Header][request]` out, /// `[Status][reply]` back. `request` is the bytes *after* the header — the /// protocol's fixed part plus any tail — because the header is this call's /// to lay down. The reply's payload lands in `into`. /// /// Null means the transport failed, which today means one of: a dead /// provider (`-EPEER` — discard this channel and `open` the name again), an /// oversized packet, or a bad handle. A provider that answered *and refused* /// is not a failure here: it comes back with a negative `Response.status`. pub fn call(self: Channel, header: envelope.Header, request: []const u8, into: []u8) ?Response { return self.callCapability(header, request, into, null); } /// As `call`, handing the provider a capability with the request — the only /// direction-crossing move kernel-ipc offers, and how `subscribe` delivers /// the subscriber's own endpoint. pub fn callCapability( self: Channel, header: envelope.Header, request: []const u8, into: []u8, capability: ?ipc.Handle, ) ?Response { var packet: [envelope.packet_maximum]u8 = undefined; const framed = frame(header, request, &packet) orelse return null; var reply: [envelope.packet_maximum]u8 = undefined; const answer = ipc.callCap(self.endpoint, framed, &reply, capability) catch return null; const status = envelope.statusOf(reply[0..answer.len]) orelse return null; const available = @min(answer.len - envelope.prefix_size, @as(usize, status.len)); const taken = @min(available, into.len); @memcpy(into[0..taken], reply[envelope.prefix_size..][0..taken]); return .{ .status = status, .payload = into[0..taken], .capability = answer.cap }; } /// Push one event packet and return immediately — no reply owed, and a slow /// or dead peer can never stall the sender. Bounded by `post_maximum`: an /// event that does not fit is refused here rather than split, because a /// packet is never fragmented. pub fn send(self: Channel, header: envelope.Header, payload: []const u8) bool { var packet: [envelope.post_maximum]u8 = undefined; const framed = frame(header, payload, &packet) orelse return false; return ipc.send(self.endpoint, framed); } /// Ask the provider what it is: the reserved `describe` verb, answered by /// every protocol built through `envelope.Define`. The name and version come /// back in `into`, which the returned `Described` borrows. pub fn describe(self: Channel, into: []u8) ?envelope.Described { var request: [envelope.packet_maximum]u8 = undefined; const packet = envelope.encodeDescribe(&request) orelse return null; var reply: [envelope.packet_maximum]u8 = undefined; const answer = ipc.callCap(self.endpoint, packet, &reply, null) catch return null; const taken = @min(answer.len, into.len); @memcpy(into[0..taken], reply[0..taken]); return envelope.decodeDescribe(into[0..taken]); } /// Drop the provider's endpoint and free the handle-table slot. The /// conversation is over the moment the capability is gone — there is nothing /// else holding it open. pub fn close(self: Channel) void { _ = ipc.close(self.endpoint); } }; // --- the namespace: resolving, opening, and claiming a contract name --------- /// Where a `/protocol/...` path routed: the registry's endpoint, plus the path /// rewritten mount-relative (`/display` for `/protocol/display`). The handle is /// deduplicated by the kernel across resolves and shared with every other user /// of that mount, so it is never ours to close. const Registry = struct { handle: ipc.Handle, relative: [path_maximum]u8, relative_len: usize, fn path(self: *const Registry) []const u8 { return self.relative[0..self.relative_len]; } }; /// Route `path` to whatever backend serves it. Null when nothing is mounted /// there — under `/protocol` that means the registry is not up yet, which is a /// *retry*, not a refusal. A kernel-served route (the read-only `/system` tree) /// is the wrong path, not a channel, and is refused here. fn reach(path: []const u8) ?Registry { var out: Registry = .{ .handle = 0, .relative = undefined, .relative_len = 0 }; const route = file_system.fsResolve(path, 0, &out.relative) orelse return null; switch (route) { .kernel => return null, .backend => |b| { out.handle = b.handle; out.relative_len = b.path_len; return out; }, } } /// One vfs-protocol round trip at a backend: the folded header, the verb's own /// fixed part, the name as the packet's tail, and an optional capability in each /// direction. Both verbs this file sends address the backend itself (target 0) — /// the name in the tail is what they are about. fn transact( comptime operation: vfs_protocol.Operation, handle: ipc.Handle, request: vfs_protocol.Protocol.RequestOf(operation), name: []const u8, send_capability: ?ipc.Handle, ) ?struct { status: envelope.Status, capability: ?ipc.Handle } { var packet: [vfs_protocol.message_maximum]u8 = undefined; const framed = vfs_protocol.Protocol.encodeRequest(operation, 0, request, name, &packet) orelse return null; var reply: [vfs_protocol.message_maximum]u8 = undefined; const answer = ipc.callCap(handle, framed, &reply, send_capability) catch return null; const status = envelope.statusOf(reply[0..answer.len]) orelse return null; return .{ .status = status, .capability = answer.cap }; } /// Resolve an absolute `/protocol/...` path and take the provider's endpoint out /// of the open reply's capability. fn openPath(path: []const u8) ?ipc.Handle { const registry = reach(path) orelse return null; const answered = transact(.open, registry.handle, .{ .flags = 0 }, registry.path(), null) orelse return null; if (answered.status.status != 0) { // A refusal carries no channel; anything that arrived anyway would be a // handle-table slot spent for nothing. if (answered.capability) |handle| _ = ipc.close(handle); return null; } // The capability *is* the channel — an open that succeeds without one was // answered by a file backend, which does not speak protocols. return answered.capability; } /// The provider's raw endpoint behind `/protocol/`. The transitional form, /// for the clients that still marshal their protocol's bytes by hand; P4 moves /// them onto `Channel` proper and this shrinks back to `connect`. /// /// Null covers both "no such contract" and "you may not have it" — deliberately /// the same answer (protocol-namespace.md: enforcement is absence), and also /// "the registry is not mounted yet", which is why every caller retries. pub fn openEndpoint(name: []const u8) ?ipc.Handle { var path: [path_maximum]u8 = undefined; const full = join(name, &path) orelse return null; return openPath(full); } /// Claim `/protocol/` for `endpoint`: the registry records the name /// against this process and hands the endpoint to whoever opens it afterwards. /// The endpoint rides the call as its capability, the one direction-crossing /// move kernel-ipc offers. /// /// Three-valued on purpose. **Null** is "the registry could not be reached" — /// it is not mounted yet, which happens when a provider starts before init has /// finished coming up, and the answer is to retry. A **value** is the registry's /// verdict and is final: 0 bound, `-EPERM` this binary is not granted that name, /// `-EBUSY` a live provider already holds it. pub fn bind(name: []const u8, endpoint: ipc.Handle) ?i32 { const registry = reach(root) orelse return null; const answered = transact(.bind, registry.handle, {}, name, endpoint) orelse return null; return answered.status.status; } /// How long a provider keeps offering itself before giving up. The registry is /// init, which mounts `/protocol` before it spawns anyone, so in a normal boot /// the first try lands; a provider the kernel test harness starts may well beat /// init to the mount, which is what the patience is for. Four seconds of 20 ms /// tries — the same cadence every client in the tree spends finding a service. const bind_attempts: u32 = 200; const bind_retry_ms: u64 = 20; /// `bind`, waiting out a registry that is not mounted yet. Only unreachability /// is retried: a registry that *answered* has decided, and asking again cannot /// change its mind. True when the name is ours. pub fn bindPatiently(name: []const u8, endpoint: ipc.Handle) bool { var attempt: u32 = 0; while (attempt < bind_attempts) : (attempt += 1) { if (bind(name, endpoint)) |status| return status == 0; time.sleepMillis(bind_retry_ms); } return false; } /// `/protocol/` + `name`, in the caller's buffer. Null if the name is empty or /// longer than the namespace admits. fn join(name: []const u8, buffer: []u8) ?[]u8 { if (name.len == 0 or name.len > name_maximum) return null; const total = root.len + 1 + name.len; if (total > buffer.len) return null; @memcpy(buffer[0..root.len], root); buffer[root.len] = '/'; @memcpy(buffer[root.len + 1 ..][0..name.len], name); return buffer[0..total]; } /// Lay a packet down: the folded header first, then the protocol's bytes. Null /// when it would not fit the buffer — the same rule as `envelope`'s framing, /// applied where the buffer is the transport's, not the protocol's. fn frame(header: envelope.Header, body: []const u8, buffer: []u8) ?[]u8 { const total = envelope.prefix_size + body.len; if (total > buffer.len) return null; @memcpy(buffer[0..envelope.prefix_size], std.mem.asBytes(&header)); @memcpy(buffer[envelope.prefix_size..][0..body.len], body); return buffer[0..total]; } // --- tests ------------------------------------------------------------------ // // The syscall half cannot run on the host, and there is no registry to reach // until P2 — so what is testable here is the framing, which is the part with // arithmetic in it. const testing = std.testing; test "a framed packet is the header followed by the protocol's bytes" { var buffer: [envelope.packet_maximum]u8 = undefined; const header = envelope.Header{ .operation = envelope.first_protocol_operation, .target = 9 }; const packet = frame(header, "body", &buffer).?; try testing.expectEqual(envelope.prefix_size + "body".len, packet.len); const decoded = envelope.headerOf(packet).?; try testing.expectEqual(envelope.first_protocol_operation, decoded.operation); try testing.expectEqual(@as(u64, 9), decoded.target); try testing.expectEqualStrings("body", packet[envelope.prefix_size..]); } test "a contract name joins the namespace root exactly once" { var buffer: [path_maximum]u8 = undefined; try testing.expectEqualStrings("/protocol/display", join("display", &buffer).?); try testing.expectEqualStrings("/protocol/test/shared-memory", join("test/shared-memory", &buffer).?); try testing.expect(join("", &buffer) == null); try testing.expect(join("x" ** (name_maximum + 1), &buffer) == null); } test "framing refuses a packet that would not fit rather than truncating it" { var post: [envelope.post_maximum]u8 = undefined; const header = envelope.Header{ .operation = envelope.first_protocol_operation }; const body = [_]u8{0} ** (envelope.post_maximum - envelope.prefix_size); const one_too_many = body ++ [_]u8{0}; try testing.expect(frame(header, &body, &post) != null); try testing.expect(frame(header, &one_too_many, &post) == null); }