975 lines
46 KiB
Zig
975 lines
46 KiB
Zig
//! 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 optional body of a reserved `subscribe`: **which** of a provider's events
|
|
/// the subscriber wants, as a bit mask whose meaning the protocol defines (the
|
|
/// input service's device classes are the model). A reserved verb carries no
|
|
/// typed request, so this rides the packet's tail — and zero, which is also what
|
|
/// a subscribe that sent no body at all reads as, means *every* event.
|
|
///
|
|
/// The mask lives here rather than in each protocol because the subscriber
|
|
/// machinery is the service harness's (library/kernel/service.zig): the harness
|
|
/// records the number, the protocol decides what its bits mean, and neither has
|
|
/// to know the other.
|
|
pub const Subscription = extern struct { interest: u32 = 0 };
|
|
|
|
/// Frame a `subscribe` request. The subscriber's own endpoint travels as the
|
|
/// call's *capability*, never in the packet — that is what makes the reverse
|
|
/// path unforgeable.
|
|
pub fn encodeSubscribe(interest: u32, buffer: []u8) ?[]u8 {
|
|
const header = Header{ .operation = operation_subscribe };
|
|
const body = Subscription{ .interest = interest };
|
|
return frame(std.mem.asBytes(&header), std.mem.asBytes(&body), &.{}, buffer);
|
|
}
|
|
|
|
/// Frame a bare `unsubscribe`: it names no event and no endpoint, because it
|
|
/// means "every subscription this task holds here" (one task, one voice).
|
|
pub fn encodeUnsubscribe(buffer: []u8) ?[]u8 {
|
|
const header = Header{ .operation = operation_unsubscribe };
|
|
return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer);
|
|
}
|
|
|
|
/// The interest mask out of a `subscribe` packet's tail, on the provider's side.
|
|
/// A caller that sent no mask reads as the every-event mask.
|
|
pub fn decodeSubscribe(tail: []const u8) Subscription {
|
|
if (tail.len < @sizeOf(Subscription)) return .{};
|
|
return std.mem.bytesToValue(Subscription, tail[0..@sizeOf(Subscription)]);
|
|
}
|
|
|
|
/// 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/<name>` 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 "a subscribe carries its interest mask in the reserved verb's tail" {
|
|
var buffer: [packet_maximum]u8 = undefined;
|
|
const packet = encodeSubscribe(0b101, &buffer).?;
|
|
try testing.expectEqual(operation_subscribe, headerOf(packet).?.operation);
|
|
try testing.expectEqual(@as(u32, 0b101), decodeSubscribe(packet[prefix_size..]).interest);
|
|
// No body at all — and a body too short to be one — read as "every event",
|
|
// which is what a subscriber that named nothing wants.
|
|
try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).interest);
|
|
try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{ 1, 2 }).interest);
|
|
|
|
const bare = encodeUnsubscribe(&buffer).?;
|
|
try testing.expectEqual(operation_unsubscribe, headerOf(bare).?.operation);
|
|
try testing.expectEqual(prefix_size, bare.len);
|
|
}
|
|
|
|
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);
|
|
}
|