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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user