//! The envelope — the fixed prefix every danos packet begins with, and the //! comptime `Define` that builds a protocol out of it. Layer L2 of //! [communication.md](../../../docs/os-development/communication.md); the //! authoritative description is //! [protocol-namespace.md](../../../docs/os-development/protocol-namespace.md). //! //! This is the one module in the protocol domain that is not itself a protocol: //! it is the shape every protocol is expressed in. A protocol module hands //! `Define` its verbs and events and gets back numbered operations (never //! colliding with the reserved range), typed encode/decode helpers, a //! provider-side dispatch table that answers `describe` on its own — and, the //! point of the exercise, compile-time proof that none of its packets can //! exceed the transport floor. Errors that used to surface as runtime //! truncation are compile errors, and "packets never fragment" is enforced at //! the source rather than by review. //! //! **The prefix is folded, never stacked.** A `request`, `reply`, or `payload` //! type names the bytes that follow the prefix — never the whole packet. The //! verb and the object being addressed live in the prefix, so a protocol type //! carries neither an `operation` field of its own nor a nested `Header`; //! `rejectStacking` refuses one at compile time, and every size check adds //! `prefix_size` exactly once. const std = @import("std"); // --- the prefix ------------------------------------------------------------- /// Every packet a danos protocol transmits begins with this header — requests /// on the synchronous call path, event packets on the asynchronous push path. /// A reply spends the same 16 bytes on `Status` instead. pub const Header = extern struct { /// The verb. Values below `first_protocol_operation` are the reserved /// universal verbs, which mean the same thing in every protocol. operation: u32, _padding: u32 = 0, /// **Object** addressing within the peer, never party addressing: which of /// the peer's objects this packet operates on — a volume, a layer, a node, /// a device. `0` addresses the provider itself, and a protocol with no /// objects never uses the field. *Which* party is at the other end was /// decided once, when the channel was opened, and *who sent this* is the /// kernel-stamped badge; neither is ever written here, which is what keeps /// the source unforgeable. target: u64 = 0, }; /// Every reply begins with this. `len` counts the bytes that follow: the /// reply's fixed part plus whatever variable tail the operation defines. pub const Status = extern struct { status: i32, // 0, or a negative errno _padding: u32 = 0, len: u32 = 0, _padding2: u32 = 0, }; /// The fixed prefix every packet spends — `Header` on a request or an event, /// `Status` on a reply. One constant, because the two are deliberately the same /// width: the packet budget does not depend on the direction. pub const prefix_size: usize = @sizeOf(Header); comptime { if (@sizeOf(Header) != 16 or @sizeOf(Status) != 16) @compileError("the envelope prefix is 16 bytes in both directions"); } // --- the reserved verbs ----------------------------------------------------- /// Reserved verbs, answered by every provider. `Define` numbers a protocol's /// own verbs from `first_protocol_operation`, so no protocol can reach in here. pub const operation_describe: u32 = 0; // -> protocol name, version, target kinds pub const operation_enumerate: u32 = 1; // -> the current targets, one per reply page pub const operation_subscribe: u32 = 2; // capability = the subscriber's endpoint pub const operation_unsubscribe: u32 = 3; pub const first_protocol_operation: u32 = 16; /// The `describe` reply's fixed part, followed inline by `name_len` bytes of the /// protocol's name. This is the version handshake: the version is asked for /// once, at connect time, rather than re-carried by every packet out of a /// 256-byte budget. pub const Description = extern struct { version: u32, operation_count: u32, event_count: u32, name_len: u32, }; /// Longest protocol name a `describe` reply can carry. pub const name_maximum: usize = packet_maximum - prefix_size - @sizeOf(Description); // --- the transport floor ---------------------------------------------------- /// The packet budget every protocol may assume on *any* transport. These are /// the kernel-ipc transport's limits — `MESSAGE_MAXIMUM` and `POST_MAXIMUM` in /// system/kernel/ipc-synchronous.zig — restated here because the kernel keeps /// them private and a protocol has to compile against something. A fatter /// transport raises its own ceiling; the floor does not move, so a protocol /// that fits here fits everywhere (communication.md: ceilings are transport /// properties, the floor is the protocol's contract). pub const packet_maximum: usize = 256; // one request or one reply (ipc_call) pub const post_maximum: usize = 64; // one event packet (ipc_send) /// Whether a request or reply whose fixed part is `T` fits the call floor once /// the prefix is counted. The folded rule in one line: `prefix_size` is added /// exactly once, because `T` describes only what follows it. Exported so the /// rule itself is testable — `Define` enforces it as a compile error. pub fn fitsPacket(comptime T: type) bool { return prefix_size + @sizeOf(T) <= packet_maximum; } /// The same, against the much smaller push floor an event packet lives within. pub fn fitsPost(comptime T: type) bool { return prefix_size + @sizeOf(T) <= post_maximum; } // --- reply statuses the envelope itself produces ----------------------------- /// Continued from the kernel's danos-native errno numbering /// (system/kernel/ipc-synchronous.zig, which ends at `EPERM` = 9), so a client /// reads one vocabulary whether the number came from the kernel or a provider. /// Positive here, sent negated in `Status.status`, as the kernel spells it. pub const ENOSYS: i32 = 10; // this protocol has no such operation pub const EPROTO: i32 = 11; // malformed packet: shorter than the verb it names pub const EBUSY: i32 = 12; // the thing asked for is held by someone still alive /// Restated from the kernel's half of the numbering, because a provider refuses /// too and userspace has no other place to read these from: `ENOENT` is "no such /// name", `EPERM` "not permitted". The protocol registry answers an ungranted /// bind with the second and a name a live provider already holds with `EBUSY`. pub const ENOENT: i32 = 4; pub const ENOSPC: i32 = 5; pub const EPERM: i32 = 9; // --- framing ---------------------------------------------------------------- /// The header of a received packet, or null when it is too short to have one. pub fn headerOf(packet: []const u8) ?Header { if (packet.len < prefix_size) return null; return std.mem.bytesToValue(Header, packet[0..prefix_size]); } /// The status of a received reply, or null when it is too short to have one. pub fn statusOf(packet: []const u8) ?Status { if (packet.len < prefix_size) return null; return std.mem.bytesToValue(Status, packet[0..prefix_size]); } /// Frame a bare `describe` request. Protocol-independent: the reserved verbs /// are asked the same way of every provider. pub fn encodeDescribe(buffer: []u8) ?[]u8 { const header = Header{ .operation = operation_describe }; return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer); } /// A decoded `describe` reply: the fixed part, plus the name that follows it. pub const Described = struct { description: Description, name: []const u8, }; /// Decode a `describe` reply packet. Null if it failed, was truncated, or is /// not a description at all. pub fn decodeDescribe(packet: []const u8) ?Described { const status = statusOf(packet) orelse return null; if (status.status != 0) return null; const body = packet[prefix_size..]; if (body.len < @sizeOf(Description)) return null; const description = std.mem.bytesToValue(Description, body[0..@sizeOf(Description)]); const name = body[@sizeOf(Description)..]; if (name.len < description.name_len) return null; return .{ .description = description, .name = name[0..description.name_len] }; } /// The single framing point: prefix, then the fixed part, then the variable /// tail, contiguous in one buffer. Null when the packet would not fit — a /// packet is never split, so not fitting is a failure, not a continuation. fn frame(prefix: []const u8, fixed: []const u8, tail: []const u8, buffer: []u8) ?[]u8 { const total = prefix.len + fixed.len + tail.len; if (total > buffer.len) return null; @memcpy(buffer[0..prefix.len], prefix); @memcpy(buffer[prefix.len..][0..fixed.len], fixed); @memcpy(buffer[prefix.len + fixed.len ..][0..tail.len], tail); return buffer[0..total]; } /// The bytes of a fixed part — empty for `void`, which is how an operation says /// "nothing but the verb". fn bytesOf(comptime T: type, value: *const T) []const u8 { if (@sizeOf(T) == 0) return &.{}; return @as([*]const u8, @ptrCast(value))[0..@sizeOf(T)]; } /// Read a fixed part out of a packet body. A zero-sized part always succeeds /// (there is nothing to be short of); anything else needs its full width. fn valueOf(comptime T: type, body: []const u8) ?T { if (@sizeOf(T) == 0) return @as(T, undefined); if (body.len < @sizeOf(T)) return null; return std.mem.bytesToValue(T, body[0..@sizeOf(T)]); } // --- the specification ------------------------------------------------------ /// One verb of a protocol. `request` and `reply` describe the bytes *after* the /// prefix; either may be `void`, meaning the verb (and its target) says it all. pub const OperationSpecification = struct { name: []const u8, request: type = void, reply: type = void, }; /// One event a provider pushes to its subscribers. `payload` is the bytes after /// the `Header`, and the whole packet must fit the push floor. pub const EventSpecification = struct { name: []const u8, payload: type = void, }; /// What `Define` is given: the contract, whole. pub const Specification = struct { /// The contract's name — the same word as its `/protocol/` leaf and /// its `library/protocol/` module. name: []const u8, version: u32, operations: []const OperationSpecification = &.{}, events: []const EventSpecification = &.{}, }; const reserved_names = [_][]const u8{ "describe", "enumerate", "subscribe", "unsubscribe" }; fn isReservedName(comptime name: []const u8) bool { for (reserved_names) |reserved| { if (std.mem.eql(u8, reserved, name)) return true; } return false; } /// Refuse a protocol type that carries the prefix inside itself. The header is /// folded into every packet, so a type that also holds one would send it twice /// and re-invent per-protocol addressing — the mistake the envelope exists to /// prevent. fn rejectStacking(comptime protocol: []const u8, comptime verb: []const u8, comptime T: type) void { switch (@typeInfo(T)) { .@"struct" => |info| for (info.fields) |field| { if (field.type == Header or field.type == Status) @compileError(std.fmt.comptimePrint( "protocol '{s}', verb '{s}': the envelope prefix is folded, not stacked — " ++ "drop the {s} field '{s}' and use the packet's own Header.operation / Header.target", .{ protocol, verb, @typeName(field.type), field.name }, )); }, else => {}, } } fn nullDefault(comptime T: type) *const anyopaque { const empty: ?T = null; return @ptrCast(&empty); } // --- Define ----------------------------------------------------------------- /// Build a protocol from its specification. Everything below happens at compile /// time; the generated type is what both sides of the conversation import. /// /// ```zig /// pub const Protocol = envelope.Define(.{ /// .name = "display", /// .version = 1, /// .operations = &.{ /// .{ .name = "configure_layer", .request = ConfigureLayer, .reply = void }, /// .{ .name = "blit", .request = Blit, .reply = void }, /// }, /// .events = &.{ /// .{ .name = "layer_lost", .payload = LayerLost }, /// }, /// }); /// ``` /// /// Refused at compile time, each with the protocol, the verb, and the numbers /// named in the message: /// /// - a request or reply that does not fit `packet_maximum` once `prefix_size` /// is added (`.request = extern struct { bytes: [241]u8 }` — 241 + 16 = 257); /// - an event payload that does not fit `post_maximum` the same way /// (`.payload = extern struct { bytes: [49]u8 }` — 49 + 16 = 65); /// - a type that stacks the prefix instead of folding it (a `Header` field); /// - a verb named after a reserved one, or named twice. /// /// A variable tail is bounded at *run* time instead, by `encodeRequest` and its /// siblings, because only the caller knows how long it is. pub fn Define(comptime specification: Specification) type { comptime { if (specification.name.len == 0) @compileError("a protocol needs a name"); if (specification.name.len > name_maximum) @compileError(std.fmt.comptimePrint( "protocol '{s}': the name is {d} bytes, and a describe reply carries at most {d}", .{ specification.name, specification.name.len, name_maximum }, )); for (specification.operations, 0..) |operation, index| { if (isReservedName(operation.name)) @compileError(std.fmt.comptimePrint( "protocol '{s}': '{s}' is a reserved universal verb — the envelope already answers it", .{ specification.name, operation.name }, )); for (specification.operations[0..index]) |earlier| { if (std.mem.eql(u8, earlier.name, operation.name)) @compileError(std.fmt.comptimePrint( "protocol '{s}': operation '{s}' is declared twice", .{ specification.name, operation.name }, )); } rejectStacking(specification.name, operation.name, operation.request); rejectStacking(specification.name, operation.name, operation.reply); if (!fitsPacket(operation.request)) @compileError(std.fmt.comptimePrint( "protocol '{s}', operation '{s}': the request is {d} bytes and the header {d}, " ++ "over the {d}-byte call floor — packets never fragment, so this has to shrink " ++ "or move its bulk to shared memory", .{ specification.name, operation.name, @sizeOf(operation.request), prefix_size, packet_maximum }, )); if (!fitsPacket(operation.reply)) @compileError(std.fmt.comptimePrint( "protocol '{s}', operation '{s}': the reply is {d} bytes and the status header {d}, " ++ "over the {d}-byte call floor", .{ specification.name, operation.name, @sizeOf(operation.reply), prefix_size, packet_maximum }, )); } for (specification.events, 0..) |event, index| { for (specification.events[0..index]) |earlier| { if (std.mem.eql(u8, earlier.name, event.name)) @compileError(std.fmt.comptimePrint( "protocol '{s}': event '{s}' is declared twice", .{ specification.name, event.name }, )); } rejectStacking(specification.name, event.name, event.payload); if (!fitsPost(event.payload)) @compileError(std.fmt.comptimePrint( "protocol '{s}', event '{s}': the payload is {d} bytes and the header {d}, " ++ "over the {d}-byte push floor — an event carries the header too, so it is the " ++ "payload that has to give", .{ specification.name, event.name, @sizeOf(event.payload), prefix_size, post_maximum }, )); } } return struct { pub const protocol_name: []const u8 = specification.name; pub const version: u32 = specification.version; /// What a provider sizes its receive and reply buffers to. A packet's /// fixed part may be far smaller, but any caller may send up to the /// floor and a short buffer truncates rather than refuses. pub const message_maximum: usize = packet_maximum; /// The widest packet this protocol's fixed parts can actually produce, /// prefix included — a diagnostic, and what a test pins. pub const request_maximum: usize = widest(specification.operations, .request); pub const reply_maximum: usize = widest(specification.operations, .reply); pub const event_maximum: usize = blk: { var widest_event: usize = prefix_size; for (specification.events) |event| widest_event = @max(widest_event, prefix_size + @sizeOf(event.payload)); break :blk widest_event; }; /// This protocol's verbs, numbered from `first_protocol_operation` in /// declaration order. pub const Operation = numbered(specification.operations, "name"); /// This protocol's events, numbered from `first_protocol_operation` in /// their **own** space. Events travel only provider → subscriber over /// `ipc_send` and operations only client → provider over `ipc_call`, so /// the direction already tells the two apart; separate spaces mean /// appending an operation can never renumber a shipped event. pub const Event = numbered(specification.events, "name"); /// The bytes after the `Header` on a request for `operation`. pub fn RequestOf(comptime operation: Operation) type { return specification.operations[indexOf(@intFromEnum(operation))].request; } /// The bytes after the `Status` on the reply to `operation`. pub fn ReplyOf(comptime operation: Operation) type { return specification.operations[indexOf(@intFromEnum(operation))].reply; } /// The bytes after the `Header` on an `event` packet. pub fn PayloadOf(comptime event: Event) type { return specification.events[indexOf(@intFromEnum(event))].payload; } // --- client side ---------------------------------------------------- /// Frame `[Header][request][tail]`. `tail` is the variable part (a path, /// write bytes); pass `&.{}` when the verb has none. Null if the packet /// would exceed the buffer or the call floor. pub fn encodeRequest( comptime operation: Operation, target: u64, request: RequestOf(operation), tail: []const u8, buffer: []u8, ) ?[]u8 { const header = Header{ .operation = @intFromEnum(operation), .target = target }; const packet = frame(std.mem.asBytes(&header), bytesOf(RequestOf(operation), &request), tail, buffer) orelse return null; return if (packet.len > packet_maximum) null else packet; } /// Frame `[Status][reply][tail]` — the provider's answer, for a provider /// that composes its own reply rather than using `Provider.dispatch`. pub fn encodeReply( comptime operation: Operation, status: i32, reply: ReplyOf(operation), tail: []const u8, buffer: []u8, ) ?[]u8 { const fixed = bytesOf(ReplyOf(operation), &reply); const head = Status{ .status = status, .len = @intCast(fixed.len + tail.len) }; const packet = frame(std.mem.asBytes(&head), fixed, tail, buffer) orelse return null; return if (packet.len > packet_maximum) null else packet; } /// Frame `[Header][payload]` for an asynchronous push. Null if it would /// exceed the buffer or the push floor — an event that does not fit is /// dropped at the source, never split. pub fn encodeEvent( comptime event: Event, target: u64, payload: PayloadOf(event), buffer: []u8, ) ?[]u8 { const header = Header{ .operation = @intFromEnum(event), .target = target }; const packet = frame(std.mem.asBytes(&header), bytesOf(PayloadOf(event), &payload), &.{}, buffer) orelse return null; return if (packet.len > post_maximum) null else packet; } /// Which of this protocol's verbs a packet names — null for a reserved /// verb, or for a number this protocol does not define. pub fn operationOf(packet: []const u8) ?Operation { const header = headerOf(packet) orelse return null; const index = header.operation -% first_protocol_operation; if (header.operation < first_protocol_operation or index >= specification.operations.len) return null; return @enumFromInt(header.operation); } /// Which of this protocol's events a pushed packet carries. pub fn eventOf(packet: []const u8) ?Event { const header = headerOf(packet) orelse return null; const index = header.operation -% first_protocol_operation; if (header.operation < first_protocol_operation or index >= specification.events.len) return null; return @enumFromInt(header.operation); } /// The fixed request part of a packet already known to name `operation`. pub fn decodeRequest(comptime operation: Operation, packet: []const u8) ?RequestOf(operation) { if (packet.len < prefix_size) return null; return valueOf(RequestOf(operation), packet[prefix_size..]); } /// The bytes after the fixed request part — empty when there are none. pub fn requestTail(comptime operation: Operation, packet: []const u8) []const u8 { const start = prefix_size + @sizeOf(RequestOf(operation)); return if (packet.len <= start) &.{} else packet[start..]; } /// The fixed reply part of a reply packet. Null on a short packet; the /// caller checks `statusOf(packet).status` for the provider's verdict. pub fn decodeReply(comptime operation: Operation, packet: []const u8) ?ReplyOf(operation) { if (packet.len < prefix_size) return null; return valueOf(ReplyOf(operation), packet[prefix_size..]); } /// The bytes after the fixed reply part, clipped to what `Status.len` /// says actually arrived. pub fn replyTail(comptime operation: Operation, packet: []const u8) []const u8 { const status = statusOf(packet) orelse return &.{}; const start = prefix_size + @sizeOf(ReplyOf(operation)); const end = @min(packet.len, prefix_size + @as(usize, status.len)); return if (end <= start) &.{} else packet[start..end]; } /// The payload of a pushed packet already known to carry `event`. pub fn decodeEvent(comptime event: Event, packet: []const u8) ?PayloadOf(event) { if (packet.len < prefix_size) return null; return valueOf(PayloadOf(event), packet[prefix_size..]); } // --- provider side -------------------------------------------------- /// This protocol's dispatch table, bound to the provider's own state /// type. `describe` is answered here, from the specification; every verb /// this provider left null answers `-ENOSYS`, which is what makes the /// reserved verbs mean the same thing at every provider in the system. /// /// ```zig /// const Serve = Protocol.Provider(*Server); /// const handlers = Serve.Handlers{ .blit = onBlit, .configure_layer = onConfigureLayer }; /// const reply_len = Serve.dispatch(server, handlers, message, sender, capability, reply); /// ``` /// /// A handler returns the number of `answer.tail()` bytes it wrote, or a /// negative errno. pub fn Provider(comptime Context: type) type { return struct { /// A reserved verb a provider chooses to implement itself. /// `enumerate` writes its targets into the tail; `subscribe` /// takes the subscriber's endpoint from `invocation.capability`. pub const ReservedHandler = *const fn (Context, Invocation(void), Answer(void)) isize; /// One optional handler per verb, named exactly as the verb, /// plus the reserved verbs the envelope cannot answer alone. pub const Handlers = handlerTable(Context); /// Answer one received packet: writes `[Status][reply][tail]` /// into `reply` and returns its length. Zero means the reply /// buffer could not even hold a status, so nothing was written. pub fn dispatch( context: Context, handlers: Handlers, packet: []const u8, sender: u32, capability: ?usize, reply: []u8, ) usize { if (reply.len < prefix_size) return 0; const header = headerOf(packet) orelse return refuse(reply, -EPROTO); const body = packet[prefix_size..]; if (header.operation == operation_describe) return describeInto(reply); inline for (specification.operations, 0..) |operation, index| { if (header.operation == first_protocol_operation + index) { return invoke( Context, operation.request, operation.reply, @field(handlers, operation.name), context, header.target, body, sender, capability, reply, ); } } const reserved: ?ReservedHandler = switch (header.operation) { operation_enumerate => handlers.enumerate, operation_subscribe => handlers.subscribe, operation_unsubscribe => handlers.unsubscribe, else => null, }; return invoke(Context, void, void, reserved, context, header.target, body, sender, capability, reply); } }; } // Shared by the protocol verbs and the reserved ones: decode, hand the // handler a typed invocation, stamp the status. One place, so a reserved // verb and a protocol verb behave identically. fn invoke( comptime Context: type, comptime RequestType: type, comptime ReplyType: type, handler: ?*const fn (Context, Invocation(RequestType), Answer(ReplyType)) isize, context: Context, target: u64, body: []const u8, sender: u32, capability: ?usize, reply: []u8, ) usize { const call = handler orelse return refuse(reply, -ENOSYS); const request = valueOf(RequestType, body) orelse return refuse(reply, -EPROTO); if (reply.len < prefix_size + @sizeOf(ReplyType)) return refuse(reply, -EPROTO); const produced = call(context, .{ .target = target, .request = request, .tail = body[@min(@sizeOf(RequestType), body.len)..], .sender = sender, .capability = capability, }, .{ .buffer = reply[prefix_size..] }); if (produced < 0) return refuse(reply, @intCast(produced)); return succeed(reply, @sizeOf(ReplyType) + @as(usize, @intCast(produced))); } fn describeInto(reply: []u8) usize { const description = Description{ .version = specification.version, .operation_count = specification.operations.len, .event_count = specification.events.len, .name_len = specification.name.len, }; const total = @sizeOf(Description) + specification.name.len; if (reply.len < prefix_size + total) return refuse(reply, -EPROTO); @memcpy(reply[prefix_size..][0..@sizeOf(Description)], std.mem.asBytes(&description)); @memcpy(reply[prefix_size + @sizeOf(Description) ..][0..specification.name.len], specification.name); return succeed(reply, total); } // "describe" is answered by the envelope, so it is the one reserved verb // with no slot in the table. const implementable_reserved = [_][]const u8{ "enumerate", "subscribe", "unsubscribe" }; fn handlerTable(comptime Context: type) type { const count = specification.operations.len + implementable_reserved.len; var names: [count][]const u8 = undefined; var types: [count]type = undefined; var attributes: [count]std.builtin.Type.StructField.Attributes = undefined; for (specification.operations, 0..) |operation, index| { const Handler = *const fn (Context, Invocation(operation.request), Answer(operation.reply)) isize; names[index] = operation.name; types[index] = ?Handler; attributes[index] = .{ .default_value_ptr = nullDefault(Handler) }; } const Reserved = *const fn (Context, Invocation(void), Answer(void)) isize; for (implementable_reserved, 0..) |name, offset| { const slot = specification.operations.len + offset; names[slot] = name; types[slot] = ?Reserved; attributes[slot] = .{ .default_value_ptr = nullDefault(Reserved) }; } const frozen_names = names; const frozen_types = types; const frozen_attributes = attributes; return @Struct(.auto, null, &frozen_names, &frozen_types, &frozen_attributes); } }; } // --- the provider's view of one packet -------------------------------------- /// What a provider's handler is given. pub fn Invocation(comptime RequestType: type) type { return struct { /// Object addressing within this provider — the packet's `Header.target`. target: u64, /// The fixed request part, already decoded. request: RequestType, /// The bytes after it: a path, write data, a name. tail: []const u8, /// The kernel-stamped badge of the caller. The only source identity /// there is — no protocol defines a sender field — so per-client state /// is keyed on this. sender: u32, /// A capability the call carried: a subscriber's endpoint, a DMA /// region. Only the synchronous call path can move one. capability: ?usize, }; } /// Where a provider's handler writes its answer. The `Status` in front of it is /// the dispatcher's to stamp — a handler never writes its own. pub fn Answer(comptime ReplyType: type) type { return struct { buffer: []u8, const fixed_size = @sizeOf(ReplyType); /// Write the fixed reply part. A `void` reply writes nothing. pub fn set(self: @This(), reply: ReplyType) void { if (fixed_size == 0) return; @memcpy(self.buffer[0..fixed_size], bytesOf(ReplyType, &reply)); } /// Room for the variable tail; the handler returns how much of it it used. pub fn tail(self: @This()) []u8 { return self.buffer[fixed_size..]; } }; } fn refuse(reply: []u8, status: i32) usize { const head = Status{ .status = status, .len = 0 }; @memcpy(reply[0..prefix_size], std.mem.asBytes(&head)); return prefix_size; } fn succeed(reply: []u8, len: usize) usize { const head = Status{ .status = 0, .len = @intCast(len) }; @memcpy(reply[0..prefix_size], std.mem.asBytes(&head)); return prefix_size + len; } /// The widest `[prefix][fixed]` over a set of operations, on one side. fn widest(comptime operations: []const OperationSpecification, comptime side: enum { request, reply }) usize { var found: usize = prefix_size; for (operations) |operation| { const size = switch (side) { .request => @sizeOf(operation.request), .reply => @sizeOf(operation.reply), }; found = @max(found, prefix_size + size); } return found; } /// An enum over `specifications`, tagged by their `name` field, numbered from /// `first_protocol_operation` in declaration order. fn numbered(comptime specifications: anytype, comptime field: []const u8) type { var names: [specifications.len][]const u8 = undefined; var values: [specifications.len]u32 = undefined; for (specifications, 0..) |specification, index| { names[index] = @field(specification, field); values[index] = first_protocol_operation + index; } const frozen_names = names; const frozen_values = values; return @Enum(u32, .exhaustive, &frozen_names, &frozen_values); } /// A verb's position in its declaration list, from its wire number. fn indexOf(comptime operation: u32) usize { return operation - first_protocol_operation; } // --- tests ------------------------------------------------------------------ const testing = std.testing; const Produce = extern struct { count: u32, flags: u32 = 0 }; const Produced = extern struct { total: u64 }; const Changed = extern struct { kind: u32, value: u32 }; const Sample = Define(.{ .name = "sample", .version = 3, .operations = &.{ .{ .name = "produce", .request = Produce, .reply = Produced }, .{ .name = "reset" }, }, .events = &.{ .{ .name = "changed", .payload = Changed }, }, }); test "the prefix is 16 bytes in both directions" { try testing.expectEqual(@as(usize, 16), @sizeOf(Header)); try testing.expectEqual(@as(usize, 16), @sizeOf(Status)); try testing.expectEqual(@as(usize, 16), prefix_size); try testing.expectEqual(@as(usize, 256), packet_maximum); try testing.expectEqual(@as(usize, 64), post_maximum); } test "verb numbering skips the reserved range" { try testing.expectEqual(@as(u32, 16), first_protocol_operation); try testing.expectEqual(@as(u32, 16), @intFromEnum(Sample.Operation.produce)); try testing.expectEqual(@as(u32, 17), @intFromEnum(Sample.Operation.reset)); // Events are numbered in their own space, so appending an operation can // never renumber a shipped event. try testing.expectEqual(@as(u32, 16), @intFromEnum(Sample.Event.changed)); for ([_]u32{ operation_describe, operation_enumerate, operation_subscribe, operation_unsubscribe }) |reserved| { try testing.expect(reserved < first_protocol_operation); } } test "request round trip, header folded and tail carried" { var buffer: [packet_maximum]u8 = undefined; const packet = Sample.encodeRequest(.produce, 42, .{ .count = 7 }, "tail bytes", &buffer).?; try testing.expectEqual(prefix_size + @sizeOf(Produce) + "tail bytes".len, packet.len); const header = headerOf(packet).?; try testing.expectEqual(@as(u32, 16), header.operation); try testing.expectEqual(@as(u64, 42), header.target); try testing.expectEqual(Sample.Operation.produce, Sample.operationOf(packet).?); const request = Sample.decodeRequest(.produce, packet).?; try testing.expectEqual(@as(u32, 7), request.count); try testing.expectEqualStrings("tail bytes", Sample.requestTail(.produce, packet)); } test "reply round trip" { var buffer: [packet_maximum]u8 = undefined; const packet = Sample.encodeReply(.produce, 0, .{ .total = 99 }, "more", &buffer).?; const status = statusOf(packet).?; try testing.expectEqual(@as(i32, 0), status.status); try testing.expectEqual(@as(u32, @sizeOf(Produced) + "more".len), status.len); try testing.expectEqual(@as(u64, 99), Sample.decodeReply(.produce, packet).?.total); try testing.expectEqualStrings("more", Sample.replyTail(.produce, packet)); } test "a void request and reply carry nothing but the verb" { var buffer: [packet_maximum]u8 = undefined; const packet = Sample.encodeRequest(.reset, 0, {}, &.{}, &buffer).?; try testing.expectEqual(prefix_size, packet.len); try testing.expectEqual(Sample.Operation.reset, Sample.operationOf(packet).?); try testing.expectEqual(@as(usize, 0), Sample.requestTail(.reset, packet).len); } test "event round trip within the push floor" { var buffer: [post_maximum]u8 = undefined; const packet = Sample.encodeEvent(.changed, 0, .{ .kind = 1, .value = 2 }, &buffer).?; try testing.expectEqual(prefix_size + @sizeOf(Changed), packet.len); try testing.expect(packet.len <= post_maximum); try testing.expectEqual(Sample.Event.changed, Sample.eventOf(packet).?); try testing.expectEqual(@as(u32, 2), Sample.decodeEvent(.changed, packet).?.value); } // A provider over a trivial context, to drive the generated dispatch table. const Counter = struct { total: u64 = 0, fn onProduce(self: *Counter, invocation: Invocation(Produce), answer: Answer(Produced)) isize { self.total += invocation.request.count; answer.set(.{ .total = self.total }); const note = "counted"; @memcpy(answer.tail()[0..note.len], note); return note.len; } }; const CounterProvider = Sample.Provider(*Counter); test "dispatch reaches a handler and stamps the status" { var counter = Counter{}; const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce }; var request: [packet_maximum]u8 = undefined; const packet = Sample.encodeRequest(.produce, 0, .{ .count = 5 }, &.{}, &request).?; var reply: [packet_maximum]u8 = undefined; const len = CounterProvider.dispatch(&counter, handlers, packet, 3, null, &reply); const answered = reply[0..len]; try testing.expectEqual(@as(i32, 0), statusOf(answered).?.status); try testing.expectEqual(@as(u64, 5), Sample.decodeReply(.produce, answered).?.total); try testing.expectEqualStrings("counted", Sample.replyTail(.produce, answered)); } test "describe is answered by the envelope, not the provider" { var counter = Counter{}; const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce }; var request: [packet_maximum]u8 = undefined; const packet = encodeDescribe(&request).?; var reply: [packet_maximum]u8 = undefined; const len = CounterProvider.dispatch(&counter, handlers, packet, 3, null, &reply); const described = decodeDescribe(reply[0..len]).?; try testing.expectEqualStrings("sample", described.name); try testing.expectEqual(@as(u32, 3), described.description.version); try testing.expectEqual(@as(u32, 2), described.description.operation_count); try testing.expectEqual(@as(u32, 1), described.description.event_count); } test "an unimplemented or unknown verb answers -ENOSYS" { var counter = Counter{}; const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce }; var reply: [packet_maximum]u8 = undefined; // A verb this protocol declares but this provider left null. var request: [packet_maximum]u8 = undefined; const declared = Sample.encodeRequest(.reset, 0, {}, &.{}, &request).?; var len = CounterProvider.dispatch(&counter, handlers, declared, 3, null, &reply); try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status); // A number no verb of this protocol wears. const stranger = Header{ .operation = first_protocol_operation + 900 }; len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&stranger), 3, null, &reply); try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status); // A reserved verb the provider does not implement answers the same way. const enumerate = Header{ .operation = operation_enumerate }; len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&enumerate), 3, null, &reply); try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status); } test "a truncated packet answers -EPROTO" { var counter = Counter{}; const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce }; var reply: [packet_maximum]u8 = undefined; // Names `produce`, but stops before the request it promises. const header = Header{ .operation = @intFromEnum(Sample.Operation.produce) }; const len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&header), 3, null, &reply); try testing.expectEqual(@as(i32, -EPROTO), statusOf(reply[0..len]).?.status); } // The size rule, exercised directly. `Define` turns exactly these predicates // into compile errors, which a test cannot catch — so the predicate is what the // test pins, and the boundary protocol below proves the compile-time half from // the other side. The negative example, spelled out: giving `Define` an // operation with `.request = extern struct { bytes: [241]u8 }`, or an event with // `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the // protocol, the verb, and the two numbers named in the message. test "the floor counts the header once, and the boundary is exact" { try testing.expect(fitsPacket(extern struct { bytes: [240]u8 })); try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 })); try testing.expect(fitsPost(extern struct { bytes: [48]u8 })); try testing.expect(!fitsPost(extern struct { bytes: [49]u8 })); try testing.expect(fitsPacket(void)); try testing.expect(fitsPost(void)); } const WidestRequest = extern struct { bytes: [packet_maximum - prefix_size]u8 }; const WidestEvent = extern struct { bytes: [post_maximum - prefix_size]u8 }; // A protocol sitting exactly on both floors. That this compiles at all is the // positive half of the compile-time check. const Boundary = Define(.{ .name = "boundary", .version = 1, .operations = &.{.{ .name = "fill", .request = WidestRequest, .reply = WidestRequest }}, .events = &.{.{ .name = "filled", .payload = WidestEvent }}, }); test "a protocol may sit exactly on the floor" { try testing.expectEqual(packet_maximum, Boundary.request_maximum); try testing.expectEqual(packet_maximum, Boundary.reply_maximum); try testing.expectEqual(post_maximum, Boundary.event_maximum); var buffer: [packet_maximum]u8 = undefined; const packet = Boundary.encodeRequest(.fill, 0, .{ .bytes = @splat(0xAB) }, &.{}, &buffer).?; try testing.expectEqual(packet_maximum, packet.len); try testing.expectEqual(@as(u8, 0xAB), Boundary.decodeRequest(.fill, packet).?.bytes[239]); // One byte of tail past the floor is refused at run time, not truncated. try testing.expect(Boundary.encodeRequest(.fill, 0, .{ .bytes = @splat(0) }, "x", &buffer) == null); var post: [post_maximum]u8 = undefined; const event = Boundary.encodeEvent(.filled, 0, .{ .bytes = @splat(1) }, &post).?; try testing.expectEqual(post_maximum, event.len); } test "a protocol's own sizes are reported prefix-included" { try testing.expectEqual(prefix_size + @sizeOf(Produce), Sample.request_maximum); try testing.expectEqual(prefix_size + @sizeOf(Produced), Sample.reply_maximum); try testing.expectEqual(prefix_size + @sizeOf(Changed), Sample.event_maximum); try testing.expectEqual(packet_maximum, Sample.message_maximum); try testing.expectEqualStrings("sample", Sample.protocol_name); }