library: five protocols speak the envelope

The folded header stops being a rule in a document and becomes the layout
on the wire. Verbs number from sixteen, leaving describe, enumerate,
subscribe and unsubscribe reserved and answered the same way by every
provider — none of them writes a line to do it. What each protocol used to
carry in a field of its own now travels in the header: a vfs node and a
display layer are the packet's target, and a reply opens with a status the
envelope stamps rather than one each protocol spelled for itself.

Display gains the most. One forty-byte request had served eleven verbs, so
attach_scanout smuggled stride through x, refresh through y and format
through colour, and every coordinate crossed as a bitcast. Per-operation
structs end all three: the fields have their own names and their own signs,
and the tile payload grows to 224 bytes because the prefix shrank. Scanout
loses a message maximum of 64 it had no business declaring — it answers
calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding
that number at its harness.

Two changes are semantic rather than notational. A directory now ends at an
entry with no name, because the fixed part of a reply always travels and a
zero-length reply no longer exists to mean anything. And input joins the
service harness, the last loop in the tree that answered no ping and heard
no terminate; its subscriber table, its pruning and its fan-out are the
same code, and a shutdown now asks it to stop instead of killing it.

A new conformance case reads the registry's own listing and asks every
protocol it finds for its name, its version and its verb count, then offers
a verb nobody defines and requires -ENOSYS — the envelope's promise,
checked against providers rather than against itself. What it cannot reach
in that boot it names on the serial line instead of passing quietly.

Suite 110/110.
This commit is contained in:
Daniel Samson
2026-08-01 06:15:25 +01:00
parent b004b9c3eb
commit d2dfbcabf8
40 changed files with 1882 additions and 1008 deletions
+6
View File
@@ -17,10 +17,15 @@ pub fn build(b: *std.Build) void {
// (docs/os-development/protocol-namespace.md).
const channel = kernel.module("channel");
// A client frames its own packets, so it needs the envelope alongside the
// protocol whose verbs it speaks.
const envelope = protocol.module("envelope");
_ = b.addModule("display-client", .{
.root_source_file = b.path("display/display-client.zig"),
.imports = &.{
.{ .name = "channel", .module = channel },
.{ .name = "envelope", .module = envelope },
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "display-protocol", .module = protocol.module("display-protocol") },
@@ -30,6 +35,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("input/input-client.zig"),
.imports = &.{
.{ .name = "channel", .module = channel },
.{ .name = "envelope", .module = envelope },
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "input-protocol", .module = protocol.module("input-protocol") },
+52 -86
View File
@@ -5,10 +5,13 @@
const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const display_protocol = @import("display-protocol");
const Protocol = display_protocol.Protocol;
/// The display's current mode, as `info()` reports it.
pub const Info = struct {
width: u32,
@@ -35,29 +38,45 @@ fn service() ?ipc.Handle {
return null;
}
/// Send one request, receive its reply; true on a zero status. `out` receives the reply
/// so callers can read `info`/`layer` fields on success.
fn transact(request: display_protocol.Request, out: *display_protocol.Reply) bool {
const h = service() orelse return false;
var req = request;
var reply: [display_protocol.reply_size]u8 = undefined;
const len = ipc.call(h, std.mem.asBytes(&req), &reply) catch return false;
if (len < display_protocol.reply_size) return false;
out.* = std.mem.bytesToValue(display_protocol.Reply, reply[0..display_protocol.reply_size]);
return out.status == 0;
/// A reply the compositor answered with, kept whole so the caller can decode the
/// verb's own fixed part out of it.
const Answered = struct {
packet: [display_protocol.message_maximum]u8,
len: usize,
fn bytes(self: *const Answered) []const u8 {
return self.packet[0..self.len];
}
};
/// Send one request (`target` addresses a layer, or 0 for the compositor itself)
/// and keep the reply. Null when the transport failed or the compositor refused.
fn transact(
comptime operation: Protocol.Operation,
target: u64,
request: Protocol.RequestOf(operation),
tail: []const u8,
) ?Answered {
const h = service() orelse return null;
var packet: [display_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
var answered: Answered = .{ .packet = undefined, .len = 0 };
answered.len = ipc.call(h, framed, &answered.packet) catch return null;
const status = envelope.statusOf(answered.bytes()) orelse return null;
if (status.status != 0) return null;
return answered;
}
/// The display's current mode, or null if the service never came up.
pub fn info() ?Info {
var reply: display_protocol.Reply = undefined;
if (!transact(.{ .operation = @intFromEnum(display_protocol.Operation.info) }, &reply)) return null;
const answered = transact(.info, 0, {}, &.{}) orelse return null;
const reply = Protocol.decodeReply(.info, answered.bytes()) orelse return null;
return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
}
/// Composite the dirty layers and flush the frame to the screen.
pub fn present() bool {
var reply: display_protocol.Reply = undefined;
return transact(.{ .operation = @intFromEnum(display_protocol.Operation.present) }, &reply);
return transact(.present, 0, {}, &.{}) != null;
}
/// One selectable display mode.
@@ -66,23 +85,17 @@ pub const Mode = display_protocol.Mode;
/// Fill `out` with the resolutions the display can switch to; returns how many were written
/// (zero on the GOP floor, or if the service never came up).
pub fn modes(out: []Mode) usize {
const h = service() orelse return 0;
var request = display_protocol.Request{ .operation = @intFromEnum(display_protocol.Operation.get_modes) };
var reply: [display_protocol.modes_reply_size]u8 = undefined;
const len = ipc.call(h, std.mem.asBytes(&request), &reply) catch return 0;
if (len < display_protocol.modes_reply_size) return 0;
const answer = std.mem.bytesToValue(display_protocol.ModesReply, reply[0..display_protocol.modes_reply_size]);
if (answer.status != 0) return 0;
const count = @min(@min(answer.count, display_protocol.max_modes), out.len);
for (0..count) |i| out[i] = answer.modes[i];
const answered = transact(.get_modes, 0, {}, &.{}) orelse return 0;
const offered = Protocol.decodeReply(.get_modes, answered.bytes()) orelse return 0;
const count = @min(@min(offered.count, display_protocol.max_modes), out.len);
for (0..count) |i| out[i] = offered.modes[i];
return count;
}
/// Change the display resolution. Only a native backend that supports mode-setting honours it
/// (on the GOP floor it returns false); on success the display's `info()` reports the new mode.
pub fn setMode(width: u32, height: u32) bool {
var reply: display_protocol.Reply = undefined;
const changed = transact(.{ .operation = @intFromEnum(display_protocol.Operation.set_mode), .width = width, .height = height }, &reply);
const changed = transact(.set_mode, 0, .{ .width = width, .height = height }, &.{}) != null;
if (changed) mode = null; // the cached mode is stale now
return changed;
}
@@ -107,93 +120,46 @@ pub fn color(r: u8, g: u8, b: u8) u32 {
/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
///
/// The id is the packet header's `target` on every call below, so it is named once
/// per request rather than repeated inside one.
pub const Layer = struct {
id: u32,
/// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
var reply: display_protocol.Reply = undefined;
return transact(.{
.operation = @intFromEnum(display_protocol.Operation.fill_rect),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
.colour = colour,
}, &reply);
return transact(.fill_rect, self.id, .{ .x = x, .y = y, .width = w, .height = h, .colour = colour }, &.{}) != null;
}
/// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
/// layer at (`x`, `y`). The tile rides inline in the request, so `w*h*4` must fit
/// `display_protocol.maximum_payload`.
/// layer at (`x`, `y`). The tile rides inline as the request's tail, so `w*h*4` must
/// fit `display_protocol.maximum_payload` — the bound the protocol derives from this
/// verb's own fixed part, so the check here can never drift from what fits.
pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
var request = display_protocol.Request{
.operation = @intFromEnum(display_protocol.Operation.blit_tile),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
};
const header = std.mem.asBytes(&request);
if (header.len + pixels.len > display_protocol.message_maximum) return false;
var buffer: [display_protocol.message_maximum]u8 = undefined;
@memcpy(buffer[0..header.len], header);
@memcpy(buffer[header.len..][0..pixels.len], pixels);
const h_svc = service() orelse return false;
var reply: [display_protocol.reply_size]u8 = undefined;
const len = ipc.call(h_svc, buffer[0 .. header.len + pixels.len], &reply) catch return false;
if (len < display_protocol.reply_size) return false;
return std.mem.bytesToValue(display_protocol.Reply, reply[0..display_protocol.reply_size]).status == 0;
if (pixels.len > display_protocol.maximum_payload) return false;
return transact(.blit_tile, self.id, .{ .x = x, .y = y, .width = w, .height = h }, pixels) != null;
}
/// Move / restack / show or hide the layer.
pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
var reply: display_protocol.Reply = undefined;
return transact(.{
.operation = @intFromEnum(display_protocol.Operation.configure_layer),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.z = z,
.visible = if (visible) 1 else 0,
}, &reply);
return transact(.configure_layer, self.id, .{ .x = x, .y = y, .z = z, .visible = if (visible) 1 else 0 }, &.{}) != null;
}
/// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
/// the layer's pixels changed without a drawing call the compositor already tracked.
pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
var reply: display_protocol.Reply = undefined;
return transact(.{
.operation = @intFromEnum(display_protocol.Operation.damage),
.layer = self.id,
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
}, &reply);
return transact(.damage, self.id, .{ .x = x, .y = y, .width = w, .height = h }, &.{}) != null;
}
/// Release the layer and its surface.
pub fn destroy(self: Layer) bool {
var reply: display_protocol.Reply = undefined;
return transact(.{ .operation = @intFromEnum(display_protocol.Operation.destroy_layer), .layer = self.id }, &reply);
return transact(.destroy_layer, self.id, {}, &.{}) != null;
}
};
/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
var reply: display_protocol.Reply = undefined;
if (!transact(.{
.operation = @intFromEnum(display_protocol.Operation.create_layer),
.x = @bitCast(x),
.y = @bitCast(y),
.width = w,
.height = h,
.z = z,
.visible = 1,
}, &reply)) return null;
return .{ .id = reply.layer };
const answered = transact(.create_layer, 0, .{ .x = x, .y = y, .width = w, .height = h, .z = z, .visible = 1 }, &.{}) orelse return null;
return .{ .id = (Protocol.decodeReply(.create_layer, answered.bytes()) orelse return null).layer };
}
+31 -15
View File
@@ -21,12 +21,14 @@
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
//! }
const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const input_protocol = @import("input-protocol");
const Protocol = input_protocol.Protocol;
pub const DeviceKind = input_protocol.DeviceKind;
pub const InputEvent = input_protocol.InputEvent;
pub const KeyEvent = input_protocol.KeyEvent;
@@ -66,31 +68,44 @@ pub const Subscriber = struct {
/// The endpoint the service delivers events to (created and owned by us; its handle
/// was handed to the service as a capability at subscribe time).
endpoint: ipc.Handle,
receive: [input_protocol.event_size]u8 = undefined,
/// A pushed packet is the folded header plus one typed event, so the buffer is
/// the push floor rather than any one event's size.
receive: [envelope.post_maximum]u8 = undefined,
/// Block until the next event is pushed, and return it. Events arrive as asynchronous
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply — the
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
/// (there should be none), so callers can loop.
///
/// The device class is the packet's operation, so it is read from the header and
/// re-tagged into an `InputEvent` here — one decoded type for a caller that took
/// several classes on one stream.
pub fn next(self: *Subscriber) ?InputEvent {
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
if (!got.isMessage() or got.len < input_protocol.event_size) return null;
return std.mem.bytesToValue(InputEvent, self.receive[0..input_protocol.event_size]);
if (!got.isMessage()) return null;
const packet = self.receive[0..@min(got.len, self.receive.len)];
return switch (Protocol.eventOf(packet) orelse return null) {
.keyboard => InputEvent.fromKeyboard(Protocol.decodeEvent(.keyboard, packet) orelse return null),
.mouse => InputEvent.fromMouse(Protocol.decodeEvent(.mouse, packet) orelse return null),
.joystick => InputEvent.fromJoystick(Protocol.decodeEvent(.joystick, packet) orelse return null),
};
}
};
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
/// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
/// capability — the envelope's reserved `subscribe`, whose shape this is exactly. Returns
/// a `Subscriber` to loop `next` on, or null on failure.
pub fn subscribe(device_mask: u32) ?Subscriber {
const service = lookupService() orelse return null;
const endpoint = ipc.createIpcEndpoint() orelse return null;
var request = input_protocol.Request{ .operation = @intFromEnum(input_protocol.Operation.subscribe), .device_mask = device_mask };
var reply: [input_protocol.reply_size]u8 = undefined;
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
if (result.len < input_protocol.reply_size) return null;
if (std.mem.bytesToValue(input_protocol.Reply, reply[0..input_protocol.reply_size]).status != 0) return null;
var packet: [input_protocol.message_maximum]u8 = undefined;
const framed = input_protocol.encodeSubscribe(device_mask, &packet) orelse return null;
var reply: [input_protocol.message_maximum]u8 = undefined;
const result = ipc.callCap(service, framed, &reply, endpoint) catch return null;
const status = envelope.statusOf(reply[0..result.len]) orelse return null;
if (status.status != 0) return null;
return .{ .endpoint = endpoint };
}
@@ -149,11 +164,12 @@ pub const Publisher = struct {
service: ipc.Handle,
fn publish(self: Publisher, event: InputEvent) bool {
var request = input_protocol.Request{ .operation = @intFromEnum(input_protocol.Operation.publish), .event = event };
var reply: [input_protocol.reply_size]u8 = undefined;
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
if (len < input_protocol.reply_size) return false;
return std.mem.bytesToValue(input_protocol.Reply, reply[0..input_protocol.reply_size]).status == 0;
var packet: [input_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(.publish, 0, event, &.{}, &packet) orelse return false;
var reply: [input_protocol.message_maximum]u8 = undefined;
const len = ipc.call(self.service, framed, &reply) catch return false;
const status = envelope.statusOf(reply[0..len]) orelse return false;
return status.status == 0;
}
/// Broadcast a keyboard event to every subscriber that took keyboard events.
+29 -21
View File
@@ -7,12 +7,14 @@
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
//! limit — the same handoff usb-storage uses toward the controller.
const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const block_protocol = @import("block-protocol");
const Protocol = block_protocol.Protocol;
pub const Geometry = struct { block_size: u32, block_count: u64 };
pub const Device = struct {
@@ -20,12 +22,9 @@ pub const Device = struct {
/// The device's block size and total block count.
pub fn geometry(self: Device) ?Geometry {
var request = block_protocol.Request{ .operation = @intFromEnum(block_protocol.Operation.geometry), .lba = 0, .count = 0, .physical = 0 };
var reply: [block_protocol.reply_size]u8 = undefined;
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return null;
if (n < block_protocol.reply_size) return null;
const result = std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]);
if (result.status != 0) return null;
var reply: [block_protocol.message_maximum]u8 = undefined;
const answered = self.call(.geometry, {}, null, &reply) orelse return null;
const result = Protocol.decodeReply(.geometry, answered) orelse return null;
return .{ .block_size = result.block_size, .block_count = result.block_count };
}
@@ -34,36 +33,45 @@ pub const Device = struct {
/// addresses become reachable by the device. Call once per buffer before naming it
/// in `read`/`write`. Harmless success when no IOMMU is enforcing.
pub fn attach(self: Device, handle: ipc.Handle) bool {
var request = block_protocol.Request{ .operation = @intFromEnum(block_protocol.Operation.attach), .lba = 0, .count = 0, .physical = 0 };
var reply: [block_protocol.reply_size]u8 = undefined;
const result = ipc.callCap(self.endpoint, std.mem.asBytes(&request), &reply, handle) catch return false;
if (result.len < block_protocol.reply_size) return false;
return std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]).status == 0;
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.attach, {}, handle, &reply) != null;
}
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
return self.transfer(.read, lba, count, physical);
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.read, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
}
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
return self.transfer(.write, lba, count, physical);
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.write, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
}
/// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so
/// prior writes survive a power-off. A filesystem calls this before the machine
/// goes down; no data transfer, so the buffer arguments are unused.
pub fn flush(self: Device) bool {
return self.transfer(.flush, 0, 0, 0);
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.flush, {}, null, &reply) != null;
}
fn transfer(self: Device, operation: block_protocol.Operation, lba: u64, count: u32, physical: u64) bool {
var request = block_protocol.Request{ .operation = @intFromEnum(operation), .lba = lba, .count = count, .physical = physical };
var reply: [block_protocol.reply_size]u8 = undefined;
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return false;
if (n < block_protocol.reply_size) return false;
return std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]).status == 0;
/// One request at the driver. `target` is always 0: one endpoint per device, so
/// there is no object within the peer to address.
fn call(
self: Device,
comptime operation: Protocol.Operation,
request: Protocol.RequestOf(operation),
capability: ?ipc.Handle,
reply: []u8,
) ?[]u8 {
var packet: [block_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
const answer = ipc.callCap(self.endpoint, framed, reply, capability) catch return null;
const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
if (status.status != 0) return null;
return reply[0..answer.len];
}
};
+1
View File
@@ -99,6 +99,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("block/block.zig"),
.imports = &.{
.{ .name = "channel", .module = channel },
.{ .name = "envelope", .module = protocol.module("envelope") },
.{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time },
.{ .name = "block-protocol", .module = protocol.module("block-protocol") },
+1
View File
@@ -59,6 +59,7 @@ pub fn build(b: *std.Build) void {
.{ .name = "system-call", .module = system_call },
.{ .name = "ipc", .module = ipc },
.{ .name = "vfs-protocol", .module = protocol.module("vfs-protocol") },
.{ .name = "envelope", .module = protocol.module("envelope") },
},
});
// The channel is the L1 concept made concrete (docs/os-development/communication.md):
+22 -26
View File
@@ -192,41 +192,37 @@ fn reach(path: []const u8) ?Registry {
}
}
/// One vfs-protocol round trip at a backend: fixed header, inline payload, and
/// an optional capability in each direction.
/// 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,
operation: vfs_protocol.Operation,
payload: []const u8,
request: vfs_protocol.Protocol.RequestOf(operation),
name: []const u8,
send_capability: ?ipc.Handle,
) ?struct { reply: vfs_protocol.Reply, capability: ?ipc.Handle } {
var request: [vfs_protocol.message_maximum]u8 = undefined;
if (vfs_protocol.request_size + payload.len > request.len) return null;
const header = vfs_protocol.Request{
.operation = operation,
.node = 0,
.offset = 0,
.len = @intCast(payload.len),
.flags = 0,
};
@memcpy(request[0..vfs_protocol.request_size], std.mem.asBytes(&header));
@memcpy(request[vfs_protocol.request_size..][0..payload.len], payload);
) ?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, request[0 .. vfs_protocol.request_size + payload.len], &reply, send_capability) catch return null;
if (answer.len < vfs_protocol.reply_size) return null;
return .{
.reply = std.mem.bytesToValue(vfs_protocol.Reply, reply[0..vfs_protocol.reply_size]),
.capability = answer.cap,
};
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(registry.handle, .open, registry.path(), null) orelse return null;
if (answered.reply.status != 0) 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;
@@ -257,8 +253,8 @@ pub fn openEndpoint(name: []const u8) ?ipc.Handle {
/// `-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(registry.handle, .bind, name, endpoint) orelse return null;
return answered.reply.status;
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
+54 -53
View File
@@ -14,8 +14,13 @@ const std = @import("std");
const abi = @import("abi");
const sc = @import("system-call");
const ipc = @import("ipc");
const envelope = @import("envelope");
const vfs_protocol = @import("vfs-protocol");
/// The generated vfs contract: encode/decode for every verb, with the node id
/// carried in the packet header's `target`.
const Protocol = vfs_protocol.Protocol;
/// The kind of a filesystem node — re-exported so a caller need not import the
/// wire protocol.
pub const Kind = vfs_protocol.NodeKind;
@@ -89,23 +94,24 @@ fn resolve(path: []const u8, flags: usize) ?Route {
}
}
const Result = struct { reply: vfs_protocol.Reply, payload: []u8 };
// One request/reply round trip: [Request header][send payload] -> backend ->
// [Reply header][receive payload]. The receive payload lands in `out`.
fn transact(h: ipc.Handle, request: vfs_protocol.Request, send: []const u8, out: []u8) ?Result {
var message: [vfs_protocol.message_maximum]u8 = undefined;
@memcpy(message[0..vfs_protocol.request_size], std.mem.asBytes(&request));
const slen = @min(send.len, vfs_protocol.maximum_payload);
@memcpy(message[vfs_protocol.request_size..][0..slen], send[0..slen]);
var rbuf: [vfs_protocol.message_maximum]u8 = undefined;
const n = ipc.call(h, message[0 .. vfs_protocol.request_size + slen], &rbuf) catch return null;
if (n < vfs_protocol.reply_size) return null;
const reply = std.mem.bytesToValue(vfs_protocol.Reply, rbuf[0..vfs_protocol.reply_size]);
const rpl = @min(n - vfs_protocol.reply_size, out.len);
@memcpy(out[0..rpl], rbuf[vfs_protocol.reply_size..][0..rpl]);
return .{ .reply = reply, .payload = out[0..rpl] };
// One request/reply round trip: frame `[Header][request][tail]`, send it, and
// hand back the whole reply packet for the caller to decode with the generated
// helpers. A backend that refused (a negative status) reads as null, which is
// what every caller here did with it anyway.
fn transact(
comptime operation: Protocol.Operation,
handle: ipc.Handle,
target: u64,
request: Protocol.RequestOf(operation),
tail: []const u8,
reply: []u8,
) ?[]u8 {
var packet: [vfs_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
const n = ipc.call(handle, framed, reply) catch return null;
const status = envelope.statusOf(reply[0..n]) orelse return null;
if (status.status != 0) return null;
return reply[0..n];
}
/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
@@ -125,11 +131,13 @@ pub const File = struct {
return n;
};
const want: u32 = @intCast(@min(buffer.len, vfs_protocol.maximum_payload));
const request = vfs_protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
const r = transact(h, request, &.{}, buffer) orelse return null;
if (r.reply.status != 0) return null;
self.offset += r.reply.len;
return r.reply.len;
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.read, h, self.node, .{ .offset = self.offset, .len = want }, &.{}, &reply) orelse return null;
const bytes = Protocol.replyTail(.read, answered);
const n = @min(bytes.len, buffer.len);
@memcpy(buffer[0..n], bytes[0..n]);
self.offset += n;
return n;
}
/// Write `data` at the current offset; returns the count written. A single
@@ -139,11 +147,11 @@ pub const File = struct {
pub fn write(self: *File, data: []const u8) ?usize {
const h = self.backend orelse return null;
const want: u32 = @intCast(@min(data.len, vfs_protocol.maximum_payload));
const request = vfs_protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
const r = transact(h, request, data[0..want], &.{}) orelse return null;
if (r.reply.status != 0) return null;
self.offset += r.reply.len;
return r.reply.len;
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.write, h, self.node, .{ .offset = self.offset, .len = want }, data[0..want], &reply) orelse return null;
const written = Protocol.decodeReply(.write, answered) orelse return null;
self.offset += written.count;
return written.count;
}
/// Write all of `data`, looping past the per-call payload cap. Returns the
@@ -169,11 +177,9 @@ pub const File = struct {
const a = fsNodeStatus(self.node) orelse return null;
return .{ .size = a.size, .kind = if (a.kind == file_kind_directory) .directory else .regular, .mtime = a.mtime };
};
const request = vfs_protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
var buffer: [@sizeOf(vfs_protocol.FileStatus)]u8 = undefined;
const r = transact(h, request, &.{}, &buffer) orelse return null;
if (r.reply.status != 0 or r.payload.len < @sizeOf(vfs_protocol.FileStatus)) return null;
const status = std.mem.bytesToValue(vfs_protocol.FileStatus, buffer[0..@sizeOf(vfs_protocol.FileStatus)]);
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.status, h, self.node, {}, &.{}, &reply) orelse return null;
const status = Protocol.decodeReply(.status, answered) orelse return null;
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
}
@@ -181,8 +187,8 @@ pub const File = struct {
/// tokens are permanent — nothing to release.
pub fn close(self: *File) void {
const h = self.backend orelse return;
const request = vfs_protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
_ = transact(h, request, &.{}, &.{});
var reply: [vfs_protocol.message_maximum]u8 = undefined;
_ = transact(.close, h, self.node, {}, &.{}, &reply);
}
};
@@ -193,10 +199,10 @@ pub fn open(path: []const u8, options: OpenOptions) ?File {
.kernel => |token| return .{ .node = token, .backend = null },
.backend => |b| {
const relative = route.backendPath();
const request = vfs_protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = options.wireFlags() };
const r = transact(b.handle, request, relative, &.{}) orelse return null;
if (r.reply.status != 0) return null;
return .{ .node = r.reply.node, .backend = b.handle };
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.open, b.handle, 0, .{ .flags = options.wireFlags() }, relative, &reply) orelse return null;
const opened = Protocol.decodeReply(.open, answered) orelse return null;
return .{ .node = opened.node, .backend = b.handle };
},
}
}
@@ -248,15 +254,13 @@ pub const Directory = struct {
self.cursor += 1;
return true;
};
const request = vfs_protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
var buffer: [vfs_protocol.message_maximum]u8 = undefined;
const r = transact(h, request, &.{}, &buffer) orelse return false;
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
if (r.payload.len < vfs_protocol.directory_entry_size) return false;
const header = std.mem.bytesToValue(vfs_protocol.DirectoryEntry, r.payload[0..vfs_protocol.directory_entry_size]);
var reply: [vfs_protocol.message_maximum]u8 = undefined;
const answered = transact(.readdir, h, self.node, .{ .cursor = self.cursor }, &.{}, &reply) orelse return false;
const header = Protocol.decodeReply(.readdir, answered) orelse return false;
if (header.name_len == 0) return false; // end of directory
entry.kind = kindFromWire(header.kind);
entry.size = header.size;
const source = r.payload[vfs_protocol.directory_entry_size..];
const source = Protocol.replyTail(.readdir, answered);
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
entry.name_len = nlen;
@@ -280,13 +284,11 @@ pub fn openDirectory(path: []const u8) ?Directory {
// A path-based request that returns only a status (mkdir, unlink). Kernel-served
// paths (the read-only /system) refuse mutation by construction: the resolve
// must land on a backend.
fn pathOperation(operation: vfs_protocol.Operation, path: []const u8) bool {
fn pathOperation(comptime operation: Protocol.Operation, path: []const u8) bool {
const route = resolve(path, 0) orelse return false;
if (route != .backend) return false;
const relative = route.backendPath();
const request = vfs_protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
const r = transact(route.backend.handle, request, relative, &.{}) orelse return false;
return r.reply.status == 0;
var reply: [vfs_protocol.message_maximum]u8 = undefined;
return transact(operation, route.backend.handle, 0, {}, route.backendPath(), &reply) != null;
}
/// Create a directory at `path` (its parent must already exist). Returns true on
@@ -336,9 +338,8 @@ pub fn rename(old_path: []const u8, new_path: []const u8) bool {
@memcpy(payload[0..old_relative.len], old_relative);
payload[old_relative.len] = 0;
@memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative);
const request = vfs_protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
const r = transact(old_route.backend.handle, request, payload[0..total], &.{}) orelse return false;
return r.reply.status == 0;
var reply: [vfs_protocol.message_maximum]u8 = undefined;
return transact(.rename, old_route.backend.handle, 0, {}, payload[0..total], &reply) != null;
}
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
+49 -39
View File
@@ -1,51 +1,61 @@
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
//! block driver (usb-storage) over its well-known `.block` endpoint. A protocol
//! module like vfs-protocol / usb-transfer-protocol: extern-struct messages, an
//! `Operation` tag, everything in one IPC message.
//! block driver (usb-storage) over `/protocol/block`. Defined through the
//! envelope, so every packet begins with the folded `Header`.
//!
//! **`Header.target` is always 0 here**: a block driver instance serves exactly
//! one device over its own endpoint, so there is no object within the peer to
//! address. A driver that later fronts several volumes gives them target ids and
//! `enumerate` lists them; nothing else about the protocol changes.
//!
//! Data path: read and write move whole blocks to or from a **caller-owned DMA
//! buffer**, named by its physical address — the same physical-address handoff
//! usb-storage already uses toward the controller, one layer up. So a 512-byte
//! sector never has to cross the 256-byte IPC boundary; only the small request /
//! reply headers do. Under an enforcing IOMMU the buffer's physical addresses are
//! only reachable by the device once the filesystem has `attach`ed the buffer's
//! capability (the block server forwards it to the controller); see docs/driver-model.md.
//! sector never has to cross the packet floor; only the small request / reply
//! parts do. Under an enforcing IOMMU the buffer's physical addresses are only
//! reachable by the device once the filesystem has `attach`ed the buffer's
//! capability (the block server forwards it to the controller); see
//! docs/driver-model.md.
pub const Operation = enum(u32) {
/// geometry() -> { block_size, block_count }
geometry = 0,
/// read(lba, count, physical): read `count` blocks from `lba` into the buffer
read = 1,
/// write(lba, count, physical): write `count` blocks at `lba` from the buffer
write = 2,
/// flush(): commit any device write cache to stable media (no data transfer).
/// A filesystem calls this to make prior writes durable — e.g. before power-off,
/// so a shutdown-time write isn't lost in the USB flash controller's cache.
flush = 3,
/// attach(): the caller's DMA-region capability rides the call's cap slot; the
/// block server forwards it to the controller so the buffer's physical addresses
/// (named in later read/write) are reachable by the device under an enforcing
/// IOMMU. Call once per buffer before using it in a transfer.
attach = 4,
const envelope = @import("envelope");
/// The answer to `geometry()`.
pub const Geometry = extern struct {
block_size: u32, // bytes per block (512)
_padding: u32 = 0,
block_count: u64, // total blocks
};
pub const Request = extern struct {
operation: u32,
reserved: u32 = 0,
/// `read(lba, count, physical)` / `write(...)`: move `count` blocks between the
/// device and the caller's DMA buffer at `physical`.
pub const Transfer = extern struct {
lba: u64,
count: u32, // number of blocks (read/write)
reserved2: u32 = 0,
physical: u64, // caller's DMA buffer physical address (read/write)
count: u32,
_padding: u32 = 0,
physical: u64, // caller's DMA buffer physical address
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
block_size: u32, // geometry: bytes per block (512)
reserved2: u32 = 0,
block_count: u64, // geometry: total blocks; read/write: blocks moved
};
/// How many blocks a transfer actually moved.
pub const Transferred = extern struct { count: u32 };
pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const Protocol = envelope.Define(.{
.name = "block",
.version = 1,
.operations = &.{
.{ .name = "geometry", .reply = Geometry },
.{ .name = "read", .request = Transfer, .reply = Transferred },
.{ .name = "write", .request = Transfer, .reply = Transferred },
// flush(): commit any device write cache to stable media (no data
// transfer). A filesystem calls this to make prior writes durable —
// before power-off, so a shutdown-time write isn't lost in the USB flash
// controller's cache.
.{ .name = "flush" },
// attach(): the caller's DMA-region capability rides the call's cap
// slot; the block server forwards it to the controller so the buffer's
// physical addresses (named in later read/write) are reachable by the
// device under an enforcing IOMMU. Call once per buffer before using it.
.{ .name = "attach" },
},
});
pub const Operation = Protocol.Operation;
pub const message_maximum: usize = Protocol.message_maximum;
+23 -6
View File
@@ -3,7 +3,8 @@
//! every conversation depend on the contract by name; neither reaches into the
//! other's files. Pure flat wire types: no protocol module imports anything.
//!
//! One module here is not a protocol but the shape the others are written in:
//! One module here is not a protocol but the shape the others are written in,
//! and therefore the one module every other one imports:
//!
//! envelope : the packet prefix + comptime Define (docs/os-development/protocol-namespace.md)
//!
@@ -19,10 +20,12 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
// Not a protocol, hence not `-protocol`: the envelope is what a protocol is
// defined *through*, so it is built first and handed to every protocol
// below as their one import.
const envelope = b.addModule("envelope", .{ .root_source_file = b.path("envelope/envelope.zig") });
for ([_]struct { name: []const u8, root: []const u8 }{
// Not a protocol, hence not `-protocol`: the envelope is what a
// protocol is defined *through*.
.{ .name = "envelope", .root = "envelope/envelope.zig" },
.{ .name = "vfs-protocol", .root = "vfs/vfs-protocol.zig" },
.{ .name = "input-protocol", .root = "input/input-protocol.zig" },
.{ .name = "block-protocol", .root = "block/block-protocol.zig" },
@@ -32,21 +35,35 @@ pub fn build(b: *std.Build) void {
.{ .name = "scanout-protocol", .root = "scanout/scanout-protocol.zig" },
.{ .name = "power-protocol", .root = "power/power-protocol.zig" },
}) |protocol| {
_ = b.addModule(protocol.name, .{ .root_source_file = b.path(protocol.root) });
_ = b.addModule(protocol.name, .{
.root_source_file = b.path(protocol.root),
.imports = &.{.{ .name = "envelope", .module = envelope }},
});
}
// Standalone `zig build test` for this domain alone; the root build keeps
// its aggregate test step.
const test_step = b.step("test", "Run the protocol unit tests");
// The envelope tests itself with no import of its own — everything else
// imports it, so it is built separately rather than importing itself.
const envelope_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("envelope/envelope.zig"), // framing round trips, verb numbering, dispatch, the floors
.target = b.resolveTargetQuery(.{}),
}),
});
test_step.dependOn(&b.addRunArtifact(envelope_tests).step);
for ([_][]const u8{
"envelope/envelope.zig", // framing round trips, verb numbering, dispatch, the floors
"vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"input/input-protocol.zig", // event numbering + the push-floor budget
"display/display-protocol.zig", // pack(): native pixel encoding per format
}) |root| {
const protocol_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(root),
.target = b.resolveTargetQuery(.{}),
.imports = &.{.{ .name = "envelope", .module = envelope }},
}),
});
test_step.dependOn(&b.addRunArtifact(protocol_tests).step);
+132 -76
View File
@@ -1,93 +1,137 @@
//! The display wire protocol — what a client says to the display service over its
//! well-known `.display` endpoint. extern-struct messages with an `Operation` tag, the
//! same shape as block/vfs/input protocols. The compositor owns the framebuffer and an
//! ordered stack of **layers**; a client creates layers, draws into them with these
//! operations, marks damage, and asks for a `present`. v1 surfaces are server-owned (a
//! client draws by command); shared-memory surfaces are a later milestone (docs/display.md).
//! The display wire protocol — what a client says to the display service over
//! `/protocol/display`. The compositor owns the framebuffer and an ordered stack of
//! **layers**; a client creates layers, draws into them with these operations, marks damage,
//! and asks for a `present`. v1 surfaces are server-owned (a client draws by command);
//! shared-memory surfaces are a later milestone (docs/display.md).
//!
//! **`Header.target` is the layer** on every verb that names one — the field that used to be
//! `Request.layer`. `info`, `present`, `set_mode`, `get_modes` and `attach_scanout` address
//! the compositor itself, so they leave it 0.
//!
//! Every verb carries its own request type. The single overloaded 40-byte request this
//! protocol used to have is gone, and with it the field abuse it invited: `attach_scanout`
//! spent `x` on a stride, `y` on a refresh rate and `colour` on a pixel format, which no
//! reader could have guessed and no compiler could have caught.
const envelope = @import("envelope");
const std = @import("std");
pub const Operation = enum(u32) {
/// info() -> { width, height, pitch, format }: the display's current mode.
info = 0,
/// create_layer(x, y, width, height, z) -> { layer }: a new server-owned surface.
create_layer = 1,
/// configure_layer(layer, x, y, z, visible): move, restack, show, or hide a layer.
configure_layer = 2,
/// destroy_layer(layer): release a layer.
destroy_layer = 3,
/// fill_rect(layer, x, y, width, height, colour): fill a rectangle of a layer.
fill_rect = 4,
/// blit_tile(layer, x, y, width, height, <inline pixels>): copy a small pixel tile in.
blit_tile = 5,
/// damage(layer, x, y, width, height): mark a region dirty for the next present.
damage = 6,
/// present(): composite the dirty layers and flush to the screen.
present = 7,
/// attach_scanout(x=stride, y=refresh_hz, width, height, colour=format) + <surface
/// capability>: a native scanout driver announces itself, handing over the shared scanout
/// surface as an `ipc_call` send_cap. The compositor maps it, looks up the driver's
/// `.scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md V4).
/// `x` is the surface's row stride in pixels, `y` the panel refresh rate from the
/// driver's EDID read (0 = unknown; paces the compositor's frame clock), `colour` the
/// DisplayFormat.
attach_scanout = 8,
/// set_mode(width, height): change the display resolution — only a native backend that
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
set_mode = 9,
/// get_modes() -> ModesReply: the resolutions the display can switch to (empty on GOP).
get_modes = 10,
};
/// The fixed request header. A `blit_tile`'s pixel payload (width*height 32-bit pixels)
/// follows this header inline in the same message, up to `maximum_payload`.
pub const Request = extern struct {
operation: u32,
layer: u32 = 0, // create/configure/destroy/fill/blit/damage: the target layer
x: u32 = 0,
y: u32 = 0,
/// The answer to `info()`: the display's current mode.
pub const Info = extern struct {
width: u32 = 0,
height: u32 = 0,
z: u32 = 0, // create_layer / configure_layer: stacking order (higher = in front)
colour: u32 = 0, // fill_rect: the fill colour (native pixel value)
visible: u32 = 1, // configure_layer: 0 hides the layer
reserved: u32 = 0,
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
// info():
width: u32 = 0,
height: u32 = 0,
pitch: u32 = 0,
pitch: u32 = 0, // bytes per row (may exceed width*4)
format: u32 = 0, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
// create_layer():
layer: u32 = 0,
reserved2: u32 = 0,
};
/// `create_layer(...)`: a new server-owned surface. Coordinates are signed — a layer may sit
/// partly off-screen.
pub const CreateLayer = extern struct {
x: i32,
y: i32,
width: u32,
height: u32,
z: u32 = 0, // stacking order (higher = nearer the front)
visible: u32 = 1,
};
/// The layer a `create_layer` established — the integer later packets put in `Header.target`.
pub const Created = extern struct { layer: u32 };
/// `configure_layer(...)` on `Header.target`: move, restack, show, or hide it.
pub const ConfigureLayer = extern struct {
x: i32,
y: i32,
z: u32 = 0,
visible: u32 = 1, // 0 hides the layer
};
/// `fill_rect(...)` on `Header.target`: fill a layer-local rectangle with a native pixel value.
pub const FillRect = extern struct {
x: i32,
y: i32,
width: u32,
height: u32,
colour: u32,
};
/// `blit_tile(...)` on `Header.target`: copy a `width`×`height` tile of native pixels
/// (row-major, little-endian) into the layer. The pixels ride inline as the packet's tail,
/// up to `maximum_payload`.
pub const BlitTile = extern struct {
x: i32,
y: i32,
width: u32,
height: u32,
};
/// `damage(...)` on `Header.target`: mark a layer-local region dirty for the next present.
pub const Damage = extern struct {
x: i32,
y: i32,
width: u32,
height: u32,
};
/// `attach_scanout(...)` + the shared surface as the call's capability: a native scanout
/// driver announces itself. The compositor maps the surface, opens the driver's
/// `/protocol/scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md
/// V4). Each field says what it is, which the old shared request could not.
pub const AttachScanout = extern struct {
/// The surface's row stride in pixels (it is sized to the driver's largest mode).
stride: u32,
/// The active mode within that surface.
width: u32,
height: u32,
/// A device-abi DisplayFormat value.
format: u32,
/// The panel refresh rate from the driver's EDID read (0 = unknown); it paces the
/// compositor's frame clock.
refresh_hz: u32 = 0,
};
/// `set_mode(width, height)`: change the display resolution — only a native backend that
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
pub const SetMode = extern struct { width: u32, height: u32 };
/// One selectable display mode.
pub const Mode = extern struct { width: u32, height: u32 };
pub const max_modes = 4;
/// The reply to `get_modes`: a small fixed list of resolutions the display can switch to.
pub const ModesReply = extern struct {
status: i32,
count: u32,
modes: [max_modes]Mode,
/// The answer to `get_modes`: the resolutions the display can switch to (empty on GOP).
pub const Modes = extern struct {
count: u32 = 0,
_padding: u32 = 0,
modes: [max_modes]Mode = @splat(.{ .width = 0, .height = 0 }),
};
pub const modes_reply_size: usize = @sizeOf(ModesReply);
/// The IPC message size — the kernel caps every message at `MESSAGE_MAXIMUM` (256 bytes,
/// system/kernel/ipc-synchronous.zig), so this matches it (a larger receive/reply buffer
/// is rejected with -E2BIG). A `blit_tile` therefore carries only a *small* tile inline —
/// `maximum_payload` bytes = up to 54 pixels, enough for a cursor or small sprite; larger
/// bitmaps are the deferred shared-memory surface path (docs/display.md).
pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const maximum_payload: usize = message_maximum - request_size;
pub const Protocol = envelope.Define(.{
.name = "display",
.version = 1,
.operations = &.{
.{ .name = "info", .reply = Info },
.{ .name = "create_layer", .request = CreateLayer, .reply = Created },
.{ .name = "configure_layer", .request = ConfigureLayer },
.{ .name = "destroy_layer" },
.{ .name = "fill_rect", .request = FillRect },
.{ .name = "blit_tile", .request = BlitTile },
.{ .name = "damage", .request = Damage },
.{ .name = "present" },
.{ .name = "attach_scanout", .request = AttachScanout },
.{ .name = "set_mode", .request = SetMode },
.{ .name = "get_modes", .reply = Modes },
},
});
pub const Operation = Protocol.Operation;
pub const message_maximum: usize = Protocol.message_maximum;
/// The largest inline pixel tile a `blit_tile` may carry: the call floor less the header and
/// this verb's own fixed part — 224 bytes, up to 56 pixels, enough for a cursor or a small
/// sprite. Larger bitmaps are the deferred shared-memory surface path (docs/display.md).
/// Per-verb rather than protocol-wide, because with per-operation requests there is no
/// single "request size" to subtract any more.
pub const maximum_payload: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(BlitTile);
/// Pack an 8-bit-per-channel colour into the display's native 32-bit pixel for `format`
/// (a device-abi `DisplayFormat`: 0 = rgbx, 1 = bgrx). Shared so a `colour` in a
@@ -113,3 +157,15 @@ test "pack encodes native byte order for rgbx and bgrx" {
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(1, 0xAA, 0, 0));
try std.testing.expectEqual(@as(u32, 0x0000_3020), pack(0, 0x20, 0x30, 0)); // green in byte 1
}
test "the layer rides the header, and the blit tile grew with the split" {
var buffer: [message_maximum]u8 = undefined;
const pixels = [_]u8{0xFF} ** 16;
const packet = Protocol.encodeRequest(.blit_tile, 3, .{ .x = 1, .y = 2, .width = 2, .height = 2 }, &pixels, &buffer).?;
try std.testing.expectEqual(@as(u64, 3), envelope.headerOf(packet).?.target);
try std.testing.expectEqual(@as(i32, 1), Protocol.decodeRequest(.blit_tile, packet).?.x);
try std.testing.expectEqual(@as(usize, 16), Protocol.requestTail(.blit_tile, packet).len);
// 216 bytes under the old 40-byte shared request; the header plus this
// verb's own four fields is 32.
try std.testing.expectEqual(@as(usize, 224), maximum_payload);
}
+119 -44
View File
@@ -4,25 +4,29 @@
//! **subscriber** (any program) that subscribes and is then pushed each event.
//!
//! The service handles several device classes over one endpoint. Each class has its own
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); they all travel in a common
//! `InputEvent` envelope tagged with a `DeviceKind`, so the fan-out path is one code path
//! and a subscriber can take a mix of devices on a single stream. A subscriber declares
//! which classes it wants with a `device_mask`, and the service routes accordingly.
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); a subscriber declares which
//! classes it wants with a `device_mask`, and the service routes accordingly.
//!
//! Two message shapes ride over the endpoint, tagged by `Operation`, like the
//! [VFS protocol](../vfs/protocol.zig):
//! Three shapes ride over the channel, and the envelope names all three
//! (docs/os-development/protocol-namespace.md):
//!
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
//! hands the service the subscriber's own endpoint as a capability (`send_cap`) and a
//! `device_mask`; `publish` carries an `InputEvent`. The reply is a `Reply`.
//! - **delivery**: the service pushes each `InputEvent` to every interested subscriber with
//! the asynchronous `ipc_send` — no reply owed, and a dead subscriber can never stall the
//! broadcast. Received in the subscriber's buffer with `Received.isMessage()` set.
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: its shape — a
//! synchronous call whose attached capability is the subscriber's endpoint — is exactly
//! what `envelope.operation_subscribe` means everywhere. The interest mask travels as the
//! packet's tail (`Subscribe`), because a reserved verb carries no typed request.
//! - **publish** is this protocol's one verb: a source sends one `InputEvent` and the
//! service answers at once, so publishing never blocks on a slow subscriber.
//! - **delivery** is an event push: the service `ipc_send`s each event to every interested
//! subscriber — no reply owed, so a dead subscriber can never stall the broadcast. The
//! packet is the folded header plus the typed event, and **the device class is the
//! header's operation**: one event per class, so a subscriber reads the kind from the
//! packet rather than from a tag inside the payload.
//!
//! This is a danos-native contract, shared by the input service, the `runtime.input`
//! client helpers, and every source/subscriber. Everything fits one IPC message.
//! `Header.target` is unused (0) in both directions: the service is the only object either
//! side addresses.
const std = @import("std");
const envelope = @import("envelope");
/// The classes of input device the service fans out. Each names a typed event and a bit in
/// the subscription mask.
@@ -242,14 +246,17 @@ pub const JoystickEvent = extern struct {
buttons: u32, // current pressed-button bitmask
};
// --- the common envelope ----------------------------------------------------
// --- the tagged union of the three ------------------------------------------
/// The largest per-device event, so `InputEvent` can hold any of them inline.
pub const max_event_size: usize = @max(@sizeOf(KeyEvent), @max(@sizeOf(MouseEvent), @sizeOf(JoystickEvent)));
/// The tagged envelope broadcast to subscribers: a `DeviceKind` plus the raw bytes of the
/// matching per-device event. Decode it with `asKeyboard`/`asMouse`/`asJoystick` (each
/// returns null unless `device` matches), or build one with the `from*` constructors.
/// One event of any class: a `DeviceKind` plus the raw bytes of the matching per-device
/// event. This is what a source `publish`es (one verb for all three classes) and what a
/// subscriber's helper hands back after decoding a delivery — on the *delivery* wire the
/// class is the packet header's operation instead, so this tag never travels there. Decode
/// it with `asKeyboard`/`asMouse`/`asJoystick` (each returns null unless `device` matches),
/// or build one with the `from*` constructors.
pub const InputEvent = extern struct {
device: u32, // a DeviceKind
_padding: u32 = 0,
@@ -285,35 +292,103 @@ pub const InputEvent = extern struct {
}
};
// --- request / reply --------------------------------------------------------
// --- the contract -----------------------------------------------------------
/// Which side of a request this is.
pub const Operation = enum(u32) {
subscribe = 0, // register the caller's endpoint (send_cap) for the classes in device_mask
publish = 1, // a source submits `event` to broadcast to interested subscribers
};
/// The body of a `subscribe` — the envelope's reserved verb 2, whose shape (a call whose
/// capability is the subscriber's own endpoint) this protocol adopts wholesale. A reserved
/// verb has no typed request, so the mask travels as the packet's tail and `encodeSubscribe`
/// is how a client lays it down. Zero means every class.
pub const Subscribe = extern struct { device_mask: u32 = 0 };
/// Request header. For `subscribe`, `device_mask` is the OR of `device_*` bits the caller
/// wants (0 means all) and the caller's receive endpoint travels as the call's capability;
/// `event` is ignored. For `publish`, `event` is the event to broadcast.
pub const Request = extern struct {
operation: u32, // an Operation
device_mask: u32 = 0, // subscribe: interested device classes (0 => all)
event: InputEvent = .{ .device = 0 },
};
pub const Protocol = envelope.Define(.{
.name = "input",
.version = 1,
.operations = &.{
// A source submits one event; the service broadcasts it to whoever wants that class.
.{ .name = "publish", .request = InputEvent },
},
.events = &.{
// One per device class: the class is the packet's operation, the typed event its
// payload. The push floor is 64 bytes and the header spends 16 of them, so the
// widest of these — the 28-byte mouse event — leaves the budget with room to spare.
.{ .name = "keyboard", .payload = KeyEvent },
.{ .name = "mouse", .payload = MouseEvent },
.{ .name = "joystick", .payload = JoystickEvent },
},
});
/// Reply header. `status` is 0 on success or a negative errno.
pub const Reply = extern struct {
status: i32,
_padding: u32 = 0,
};
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const Operation = Protocol.Operation;
pub const Event = Protocol.Event;
pub const message_maximum: usize = Protocol.message_maximum;
pub const event_size: usize = @sizeOf(InputEvent);
comptime {
// The delivery path posts a bare InputEvent through ipc_send, so it must fit an
// endpoint's async payload slot (POST_MAXIMUM is 64).
if (event_size > 64) @compileError("InputEvent must fit the ipc_send payload (POST_MAXIMUM)");
/// The event class a `DeviceKind` value (as it appears in `InputEvent.device`) is delivered
/// as. Null for a value no class claims, which is delivered to nobody.
pub fn eventOfDevice(device: u32) ?Event {
return switch (device) {
@intFromEnum(DeviceKind.keyboard) => .keyboard,
@intFromEnum(DeviceKind.mouse) => .mouse,
@intFromEnum(DeviceKind.joystick) => .joystick,
else => null,
};
}
/// Frame a `subscribe` request: the reserved verb's header, then the interest mask. Null if
/// the buffer is too small. Spelled here rather than at each caller so the one place that
/// knows a reserved verb carries its body in the tail is the protocol module.
pub fn encodeSubscribe(device_mask: u32, buffer: []u8) ?[]u8 {
const total = envelope.prefix_size + @sizeOf(Subscribe);
if (buffer.len < total) return null;
const header = envelope.Header{ .operation = envelope.operation_subscribe };
const body = Subscribe{ .device_mask = device_mask };
@memcpy(buffer[0..envelope.prefix_size], std.mem.asBytes(&header));
@memcpy(buffer[envelope.prefix_size..][0..@sizeOf(Subscribe)], std.mem.asBytes(&body));
return buffer[0..total];
}
/// The interest mask out of a `subscribe` packet's tail, on the provider's side. A caller
/// that sent no mask at all means every class, which is what a zero mask means anyway.
pub fn decodeSubscribe(tail: []const u8) Subscribe {
if (tail.len < @sizeOf(Subscribe)) return .{};
return std.mem.bytesToValue(Subscribe, tail[0..@sizeOf(Subscribe)]);
}
test "an event of every class fits the push floor, header included" {
// What the hand-rolled comptime assert used to say about `InputEvent`, now
// said by `Define` about each typed event — and counting the header, which
// the old check did not.
try std.testing.expectEqual(envelope.prefix_size + @sizeOf(MouseEvent), Protocol.event_maximum);
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
}
test "the verb numbering, and the class an event carries" {
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.publish));
// Events number in their own space, so the three classes start at 16 too.
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.keyboard));
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.mouse));
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Event.joystick));
// subscribe is the RESERVED verb, below the protocol range entirely.
try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);
var buffer: [envelope.post_maximum]u8 = undefined;
const packet = Protocol.encodeEvent(.mouse, 0, .{
.kind = @intFromEnum(MouseEventKind.motion),
.button = 0,
.dx = 3,
.dy = -4,
.scroll_x = 0,
.scroll_y = 0,
.buttons = 0,
}, &buffer).?;
try std.testing.expectEqual(Event.mouse, Protocol.eventOf(packet).?);
try std.testing.expectEqual(@as(i32, -4), Protocol.decodeEvent(.mouse, packet).?.dy);
}
test "a subscribe carries its mask in the tail of the reserved verb" {
var buffer: [envelope.packet_maximum]u8 = undefined;
const packet = encodeSubscribe(device_mouse, &buffer).?;
try std.testing.expectEqual(envelope.operation_subscribe, envelope.headerOf(packet).?.operation);
try std.testing.expectEqual(device_mouse, decodeSubscribe(packet[envelope.prefix_size..]).device_mask);
// A caller that sent nothing at all reads as the every-class mask.
try std.testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).device_mask);
}
+39 -33
View File
@@ -1,49 +1,55 @@
//! The scanout wire protocol — what the compositor says to a native scanout driver (e.g.
//! virtio-gpu) over its well-known `.scanout` endpoint to put a composited frame on screen.
//! The driver owns the panel and the shared scanout surface it handed the compositor (via the
//! display service's `attach_scanout`); the compositor composites into that surface, then asks
//! the driver to present a damaged rectangle. Tiny by design — one present request. Separate
//! from the display protocol because the directions differ: clients call the compositor over
//! `.display`; the compositor calls the driver over `.scanout`. See docs/display-v2.md.
//! virtio-gpu) over `/protocol/scanout` to put a composited frame on screen. The driver owns
//! the panel and the shared scanout surface it handed the compositor (via the display
//! service's `attach_scanout`); the compositor composites into that surface, then asks the
//! driver to present a damaged rectangle. Tiny by design — one present request. Separate from
//! the display protocol because the directions differ: clients call the compositor over
//! `/protocol/display`; the compositor calls the driver over `/protocol/scanout`. See
//! docs/display-v2.md.
//!
//! One scanout per driver instance, so `Header.target` is always 0.
const std = @import("std");
const envelope = @import("envelope");
pub const Operation = enum(u32) {
/// present(x, y, width, height): put the given rectangle of the shared scanout surface on
/// the panel (on virtio-gpu: transfer-to-host of the region, then a fenced resource flush).
present = 0,
/// get_modes() -> ModesReply: the display modes this scanout can switch to (V5).
get_modes = 1,
/// set_mode(width, height): change the scanout resolution — the shared surface is sized to
/// the largest mode, so this just re-points the scanout rectangle; the surface is unchanged.
set_mode = 2,
};
pub const Request = extern struct {
operation: u32,
/// `present(rect)`: put the given rectangle of the shared scanout surface on the panel (on
/// virtio-gpu: transfer-to-host of the region, then a fenced resource flush).
pub const Present = extern struct {
x: u32 = 0,
y: u32 = 0,
width: u32 = 0,
height: u32 = 0,
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
};
/// `set_mode(width, height)`: change the scanout resolution — the shared surface is sized to
/// the largest mode, so this just re-points the scanout rectangle; the surface is unchanged.
pub const SetMode = extern struct { width: u32, height: u32 };
/// One offered display mode.
pub const Mode = extern struct { width: u32, height: u32 };
pub const max_modes = 4;
/// The reply to `get_modes`: a small fixed list of modes.
pub const ModesReply = extern struct {
status: i32,
count: u32,
modes: [max_modes]Mode,
/// The answer to `get_modes`: a small fixed list of modes. The success/failure verdict is
/// the reply's `Status`, so this carries only the modes.
pub const Modes = extern struct {
count: u32 = 0,
_padding: u32 = 0,
modes: [max_modes]Mode = @splat(.{ .width = 0, .height = 0 }),
};
pub const message_maximum: usize = 64;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const modes_reply_size: usize = @sizeOf(ModesReply);
pub const Protocol = envelope.Define(.{
.name = "scanout",
.version = 1,
.operations = &.{
.{ .name = "present", .request = Present },
.{ .name = "get_modes", .reply = Modes },
.{ .name = "set_mode", .request = SetMode },
},
});
pub const Operation = Protocol.Operation;
/// The call floor, like every synchronous protocol. This module used to declare
/// 64 — the *push* floor — which was simply wrong: nothing here is pushed, and a
/// provider sizing its receive buffer to 64 refuses (`-E2BIG`) any caller that
/// sends up to the floor it is entitled to.
pub const message_maximum: usize = Protocol.message_maximum;
+134 -85
View File
@@ -1,52 +1,27 @@
//! The VFS wire protocol — the message format spoken between a client (via the file
//! API) and the user-space VFS server over IPC. A request is a fixed `Request` header
//! followed by an inline payload (a path, or write bytes); a reply is a fixed `Reply`
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
//! The VFS wire protocol — what a client (through the file API,
//! library/kernel/file-system.zig) says to a filesystem backend over IPC. Defined
//! through the envelope (docs/os-development/protocol-namespace.md), so every
//! packet begins with the folded `Header`: the verb in `Header.operation`, and
//! **the open node id in `Header.target`** — the field that used to be
//! `Request.node`. A path appears in the conversation once, at `open`; every
//! packet after it addresses that integer.
//!
//! This is a danos-native contract, so it uses danos names throughout. The client
//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
//! This is a danos-native contract, so it uses danos names throughout. It is
//! user-space only — the kernel knows nothing of files or paths; it only routes
//! (`fs_resolve`) and moves the bytes. The backends that serve it today are the
//! FAT server (system/services/fat/) and the protocol registry inside PID 1
//! (system/services/init/), which is a *synthetic* backend: `/protocol` holds
//! contracts rather than files.
//!
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
//! Shared by library/runtime/fs.zig (the client) and the mount backends that serve it (today
//! the fat server, system/services/fat/). The standalone user-space VFS server it was first
//! written against has retired — path routing moved into the kernel (system/kernel/vfs.zig,
//! fs_resolve) — but the protocol module outlived it.
//! **An `open` reply may carry a capability.** The vfs `open` request rides
//! **An `open` reply may carry a capability.** The `open` request rides
//! `ipc_call`, and the reply direction of a call can hand back an endpoint
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers
//! with a node id and nothing else — but a *synthetic* backend does: opening a
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers with
//! a node id and nothing else — but the registry does: opening a
//! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint,
//! which is the channel (docs/os-development/protocol-namespace.md). The
//! convention is per-backend, not per-operation: a client that did not ask a
//! synthetic backend simply gets no capability back, exactly as today.
//! which is the channel. The convention is per-backend, not per-operation: a
//! client that did not ask a synthetic backend simply gets no capability back.
pub const Operation = enum(u32) {
open, // open(path) -> node id (a synthetic backend may reply with a capability instead)
close, // close(node)
read, // read(node, offset, len) -> bytes
write, // write(node, offset, bytes) -> count
status, // status(node) -> FileStatus
// Appended for the mount router (M5). Values stay stable, so existing clients
// and the flat-ramfs tests are unaffected.
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
mount, // mount(prefix payload, capability = backend endpoint)
unmount, // unmount(prefix payload)
// Appended for filesystem mutation (Phase 2). Path-based (the path is the
// payload); a mounted backend handles them, the flat ramfs refuses them.
mkdir, // mkdir(path payload) -> status
unlink, // unlink(path payload) -> status
// rename: the payload is the old path, a single 0x00 separator, then the new
// path. Same-directory rename only (the router requires both under one mount).
rename, // rename(old\0new payload) -> status
// Appended for the protocol namespace (P2). The registry is a synthetic
// backend mounted at /protocol: `open` establishes a channel and `readdir`
// lists the bound names like any directory, so those two verbs need nothing
// new — but *claiming* a name does. A file backend refuses it, alongside the
// router verbs it does not implement either; only the registry implements it.
bind, // bind(name payload, capability = the provider's endpoint) -> status
};
const envelope = @import("envelope");
/// The type of a filesystem node, aligned to the node-kind table
/// (docs/file-system-development/file-system-hierarchy.md). Fills `FileStatus.kind` and
@@ -67,38 +42,18 @@ pub const NodeKind = enum(u32) {
protocol = 7,
};
/// One directory entry, returned by `readdir`: a fixed header followed inline in
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
/// One directory entry: the fixed part of a `readdir` reply, followed inline by
/// `name_len` bytes of name. **A zero `name_len` is end of directory** — the
/// reply's own length cannot say so any more, because the envelope always sends
/// the fixed part.
pub const DirectoryEntry = extern struct {
kind: u32, // a NodeKind
name_len: u32,
size: u64,
kind: u32 = 0, // a NodeKind
name_len: u32 = 0,
size: u64 = 0,
};
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
/// Request header. `node` is the server-side open-file id (from a prior open);
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
/// carry the read/write position and count.
pub const Request = extern struct {
operation: Operation,
node: u64,
offset: u64,
len: u32,
flags: u32,
};
/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
/// FileStatus size).
pub const Reply = extern struct {
status: i32,
_padding: u32 = 0,
node: u64 = 0,
len: u32 = 0,
_padding2: u32 = 0,
};
/// A file's metadata (the danos-native answer to a `status` request). The POSIX
/// layer maps this onto `struct stat`.
pub const FileStatus = extern struct {
@@ -110,13 +65,84 @@ pub const FileStatus = extern struct {
mtime: u64 = 0,
};
pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
/// Largest inline payload that still fits one IPC message alongside a header.
pub const maximum_payload: usize = message_maximum - request_size;
// --- the per-operation request and reply parts ------------------------------
//
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
// node id: those are the packet header's, folded in once.
/// Open flags (danos-native; `runtime.fs.OpenOptions` maps its booleans onto these).
/// `open(flags)` with the path as the packet's tail. The one verb that spends a
/// path; everything after it addresses the node id this returns.
pub const Open = extern struct { flags: u32 = 0 };
/// The node id an `open` established — the integer every later packet puts in
/// `Header.target`. Meaningful only between this client and this backend.
pub const Opened = extern struct { node: u64 };
/// `read(offset, len)` on `Header.target`; the bytes come back as the reply tail.
pub const Read = extern struct {
offset: u64,
len: u32,
_padding: u32 = 0,
};
/// `write(offset, len)` on `Header.target`, with the data as the packet's tail.
pub const Write = extern struct {
offset: u64,
len: u32,
_padding: u32 = 0,
};
/// How many bytes a `write` actually took — it may be short.
pub const Written = extern struct { count: u32 };
/// `readdir(cursor)` on `Header.target`: one entry per call, cursor-advanced.
pub const Readdir = extern struct { cursor: u64 };
/// The contract, whole. Verbs number from `envelope.first_protocol_operation`
/// (16) in this order; the reserved verbs below it mean what they mean
/// everywhere. `readdir` stays a protocol verb rather than folding into the
/// reserved `enumerate`: it enumerates the children of one *node*, where
/// `enumerate` names a provider's targets.
pub const Protocol = envelope.Define(.{
.name = "vfs",
.version = 1,
.operations = &.{
.{ .name = "open", .request = Open, .reply = Opened },
.{ .name = "close" },
.{ .name = "read", .request = Read },
.{ .name = "write", .request = Write, .reply = Written },
.{ .name = "status", .reply = FileStatus },
.{ .name = "readdir", .request = Readdir, .reply = DirectoryEntry },
// The mount router's two verbs. Path routing lives in the kernel now
// (system/kernel/vfs.zig), so no backend implements either; they keep
// their numbers so the vocabulary stays the one docs/vfs-protocol.md
// describes.
.{ .name = "mount" }, // tail = the prefix, capability = the backend's endpoint
.{ .name = "unmount" }, // tail = the prefix
// Filesystem mutation, path-based: the path is the packet's tail.
.{ .name = "mkdir" },
.{ .name = "unlink" },
// rename: the tail is the old path, a single 0x00 separator, then the
// new path. Same-directory rename only.
.{ .name = "rename" },
// The registry's claim verb (P2): the name is the tail and the
// provider's endpoint rides the call as its capability. A file backend
// refuses it; only init implements it.
.{ .name = "bind" },
},
});
pub const Operation = Protocol.Operation;
/// What a backend sizes its buffers to — the call floor, as every protocol does.
pub const message_maximum: usize = Protocol.message_maximum;
/// The most inline payload any request may carry: the floor less the header and
/// the widest fixed request part, so one bound serves every verb (a path, write
/// data, a read's answer).
pub const maximum_payload: usize = envelope.packet_maximum - Protocol.request_maximum;
/// Open flags (danos-native; `file_system.OpenOptions` maps its booleans onto these).
pub const create: u32 = 1;
/// Open a directory (for readdir) rather than a file. A mounted backend uses
/// this to open a directory node; the flat ramfs ignores it.
@@ -126,7 +152,7 @@ pub const directory: u32 = 2;
/// backend frees the old cluster chain; the flat ramfs ignores it.
pub const truncate: u32 = 4;
test "protocol struct sizes and node kinds" {
test "the stable wire values: node kinds, entry layout, and the verb numbering" {
const std = @import("std");
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
@@ -134,11 +160,34 @@ test "protocol struct sizes and node kinds" {
try std.testing.expectEqual(@as(u32, 6), @intFromEnum(NodeKind.socket));
try std.testing.expectEqual(@as(u32, 7), @intFromEnum(NodeKind.protocol));
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
// The appended operations keep the original values.
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
try std.testing.expectEqual(@as(u32, 10), @intFromEnum(Operation.rename));
// The registry's claim verb, appended last with the protocol namespace.
try std.testing.expectEqual(@as(u32, 11), @intFromEnum(Operation.bind));
// The numbering the envelope gives this protocol. These are NEW values: the
// rebase moved every verb above the reserved range, so the old 0..11 are
// gone and 16..27 are what the wire carries. Pinned because both sides of a
// flag-day have to agree on them, not because they may never change again.
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.close));
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.read));
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.write));
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.status));
try std.testing.expectEqual(@as(u32, 21), @intFromEnum(Operation.readdir));
try std.testing.expectEqual(@as(u32, 26), @intFromEnum(Operation.rename));
try std.testing.expectEqual(@as(u32, 27), @intFromEnum(Operation.bind));
// The payload bound is what it always was, arrived at the other way round:
// the header plus the widest fixed request part is 32 bytes of the floor.
try std.testing.expectEqual(@as(usize, 224), maximum_payload);
}
test "the node id rides the header, and a path rides the tail" {
const std = @import("std");
var buffer: [message_maximum]u8 = undefined;
const opening = Protocol.encodeRequest(.open, 0, .{ .flags = create }, "/a/b", &buffer).?;
try std.testing.expectEqual(@as(u32, create), Protocol.decodeRequest(.open, opening).?.flags);
try std.testing.expectEqualStrings("/a/b", Protocol.requestTail(.open, opening));
try std.testing.expectEqual(@as(u64, 0), envelope.headerOf(opening).?.target);
const reading = Protocol.encodeRequest(.read, 7, .{ .offset = 512, .len = 64 }, &.{}, &buffer).?;
try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(reading).?.target);
try std.testing.expectEqual(@as(u64, 512), Protocol.decodeRequest(.read, reading).?.offset);
}