Skip to documentation
SLOP

tiny.game.transport

Reference tiny.game transport

Defined in tiny.game.

Two servers carry lines holding one command each from an outside tool to a host, over standard input or a Unix socket, and carry each reply back.

API (18)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

No direct callersNo direct callstiny.gametransport
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callstransport.CommandWorkspaceapplytransportserveStdiotransport.CommandScratchapply
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsNo direct callstransport.CommandWorkspacedeinittransportserveStdiotransport.CommandScratchdeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstransportserveStdiotransport.CommandScratchinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstransportserveSockettransport.CommandScratchapplytransport.CommandWorkspaceapply
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallstransportserveSockettransport.CommandScratchdeinittransport.CommandWorkspacedeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstransportserveSockettransport.CommandWorkspaceinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstransportserveSockettransport.CommandWorkspacewritten
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstransportserveSockettest; no linkfun.game.src.transporttest: line buffer frames chunked inpu...transport.LineBufferconsume
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstransportserveSockettest; no linkfun.game.src.transporttest: line buffer frames chunked inpu...transport.LineBufferdeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstransportserveSockettest; no linkfun.game.src.transporttest: line buffer frames chunked inpu...transport.LineBufferfeed
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstransportserveSockettest; no linkfun.game.src.transporttest: line buffer frames chunked inpu...transport.LineBufferinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstransportserveSockettest; no linkfun.game.src.transporttest: line buffer frames chunked inpu...transport.LineBuffernext
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callerstransport.CommandWorkspaceapplytransport.CommandWorkspacedeinittransport.CommandWorkspaceinittransport.CommandWorkspacewrittentransport.LineBufferconsume+5 moretransportserveSocket
Static calls · unresolved targets: 2 · external targets: 6.
Called byCallsNo direct callerstransport.CommandScratchapplytransport.CommandScratchdeinittransport.CommandScratchinittransportserveStdio
Static calls · unresolved targets: 0 · external targets: 4.

Source: fun/game/src/root.zig:121

zig
pub const transport = @import("transport.zig");

Source: fun/game/src/transport.zig

zig
//! Two servers carry lines holding one command each from an outside tool to a host, over standard//! input or a Unix socket, and carry each reply back. A tool or test drives a headless game by//! writing one command per line and reading the reply to each. A tool either starts the host and//! talks to it over the host's standard input, or connects to a host that is already running.//!//! A socket hands over bytes in pieces of any size, so a line can arrive split across reads, and//! one read can carry more than one line. A host answers many commands in one session, and it wants//! to reuse its memory from one command to the next.//!//! The caller supplies the object that answers each line (a *control object*): its `applyLine`//! takes an arena, the line and a writer, writes the reply to the writer, and returns false to stop//! serving. Both servers trim spaces, tabs and carriage returns from each line, skip a line left//! empty, and stop when the control object returns false. The arena is reset before each line and//! keeps its memory, so memory from one command serves the next.//!//! `serveStdio` reads lines from standard input and hands the control object a buffered writer to//! standard output. `serveStdio` never flushes that writer, so the control object flushes each//! reply itself. `serveSocket` listens on a Unix socket at a path the caller gives, prints a JSON//! `listening` event with the path on standard output, and serves one connection at a time. A line//! holds at most 1 MiB (`line_capacity`). When a line and its newline together pass 1 MiB, or a//! read fails, `serveStdio` ends as if the input had ended. `serveSocket` returns//! `error.LineCapacityExceeded` when the bytes it holds and one read together pass 1 MiB, so a line//! under 1 MiB can end it. That error, `error.OutOfMemory` from growing the held bytes, an error//! from the control object, or a failed accept returns from `serveSocket` and ends service for//! every later client.const std = @import("std");const pretty_json = @import("pretty").json;const sys = @import("sys");/// The most bytes one line may hold, 1 MiB. Both servers bound a line with it: `serveStdio` sizes/// its read buffer to it, and `LineBuffer.feed` refuses bytes past it. On standard input, a line/// and its newline together fit in it.pub const line_capacity = 1024 * 1024;/// An arena that a control object gets for each line, reset before the next line. `serveStdio`/// keeps one for the whole session, and `CommandWorkspace` holds one, so memory from one command/// serves the next. A reset keeps the memory the arena grew to.pub const CommandScratch = struct {    arena: std.heap.ArenaAllocator,    /// Returns a scratch whose arena draws memory from `allocator`. A server builds one before it    /// reads its first line.    pub fn init(allocator: std.mem.Allocator) CommandScratch {        return .{ .arena = .init(allocator) };    }    /// Frees the arena's memory and leaves the scratch undefined. A server calls it when it stops    /// serving.    pub fn deinit(self: *CommandScratch) void {        self.arena.deinit();        self.* = undefined;    }    /// Resets the arena and passes it, `line` and `out` to `control.applyLine`, returning that    /// call's result. A server calls it once per line, and a true result means the server keeps    /// serving. Memory the control object took from the arena for an earlier line is invalid once    /// the call resets it. The call returns any error that `applyLine` returns.    pub fn apply(self: *CommandScratch, control: anytype, line: []const u8, out: *std.Io.Writer) !bool {        _ = self.arena.reset(.retain_capacity);        return control.applyLine(self.arena.allocator(), line, out);    }};/// A scratch arena and a growing reply buffer for one connection, which keep a control object's/// reply in memory until the server sends it. `serveSocket` keeps one per connection and sends the/// reply after each line, and a test compares its reply with a direct call to the control object./// Once a first command has grown the arena and the buffer, repeating that command allocates/// nothing more.pub const CommandWorkspace = struct {    scratch: CommandScratch,    reply: std.Io.Writer.Allocating,    /// Returns a workspace whose arena and reply buffer draw memory from `allocator`. `serveSocket`    /// builds one for each connection it accepts.    pub fn init(allocator: std.mem.Allocator) CommandWorkspace {        return .{            .scratch = .init(allocator),            .reply = .init(allocator),        };    }    /// Frees the reply buffer and the arena and leaves the workspace undefined. `serveSocket` calls    /// it when a connection ends.    pub fn deinit(self: *CommandWorkspace) void {        self.reply.deinit();        self.scratch.deinit();        self.* = undefined;    }    /// Clears the reply buffer, then hands the control object the reset arena, the line and a    /// writer into the buffer. `serveSocket` calls it once per line and then sends `written` to the    /// client. The call returns the control object's result, true to keep serving, and any error    /// the control object returns. A failed allocation for the reply reaches the caller as    /// `error.WriteFailed`, and a later call can succeed.    pub fn apply(self: *CommandWorkspace, control: anytype, line: []const u8) !bool {        self.reply.clearRetainingCapacity();        return self.scratch.apply(control, line, &self.reply.writer);    }    /// Returns the reply that the last `apply` wrote. `serveSocket` sends these bytes to the client    /// after each line. The slice stays valid until the next `apply` or `deinit`.    pub fn written(self: *CommandWorkspace) []const u8 {        return self.reply.written();    }};/// Bytes read from a connection and held until they are consumed, from which whole lines are taken/// in order. `serveSocket` feeds it each read, because a read can end inside a line or carry more/// than one line. The buffer holds at most `line_capacity` bytes.pub const LineBuffer = struct {    allocator: std.mem.Allocator,    pending: std.ArrayListUnmanaged(u8) = .empty,    /// Returns an empty buffer that grows with `allocator`. `serveSocket` builds one for each    /// connection.    pub fn init(allocator: std.mem.Allocator) LineBuffer {        return .{ .allocator = allocator };    }    /// Frees the held bytes and leaves the buffer undefined. `serveSocket` calls it when a    /// connection ends. The bytes of a line that never received its newline are dropped.    pub fn deinit(self: *LineBuffer) void {        self.pending.deinit(self.allocator);        self.* = undefined;    }    /// Appends `bytes` to the held bytes. `serveSocket` feeds it each read from the connection. The    /// call returns `error.LineCapacityExceeded` when the held bytes and `bytes` together pass    /// `line_capacity`, and `error.OutOfMemory` when growing the buffer runs out of memory. The    /// bound counts every held byte, so a read that finishes one line and starts later ones can    /// pass it while each line is under 1 MiB.    pub fn feed(self: *LineBuffer, bytes: []const u8) !void {        std.debug.assert(self.pending.items.len <= line_capacity);        if (bytes.len > line_capacity - self.pending.items.len) return error.LineCapacityExceeded;        try self.pending.appendSlice(self.allocator, bytes);    }    /// Returns the first whole line, trimmed of spaces, tabs and carriage returns, or `null` when    /// the held bytes hold no newline. `serveSocket` takes lines with it until it returns `null`.    /// The call leaves the line held, and a caller calls `consume` to move past it. The line points    /// into the buffer and stays valid until the next `feed` or `consume`. A blank line comes back    /// empty.    pub fn next(self: *LineBuffer) ?[]const u8 {        const newline = std.mem.indexOfScalar(u8, self.pending.items, '\n') orelse return null;        const line = self.pending.items[0..newline];        const trimmed = std.mem.trim(u8, line, " \t\r");        return trimmed;    }    /// Drops the first line and its newline, and moves the bytes after it to the front.    /// `serveSocket` calls it after it handles each line from `next`. The call leaves the buffer as    /// it was when no newline is held. The call copies every byte after the line.    pub fn consume(self: *LineBuffer) void {        const newline = std.mem.indexOfScalar(u8, self.pending.items, '\n') orelse return;        const consumed = newline + 1;        std.mem.copyForwards(u8, self.pending.items, self.pending.items[consumed..]);        self.pending.shrinkRetainingCapacity(self.pending.items.len - consumed);    }};/// Reads lines from standard input and hands each one to `control` until the control object returns/// false or the input ends. A host that a tool starts serves the tool's commands over its standard/// input and output. The control object gets a buffered writer to standard output for its reply./// The call never flushes that writer, so the control object flushes each reply, and bytes it/// leaves unflushed are lost when the call returns. Each line is trimmed of spaces, tabs and/// carriage returns, and a line left empty is skipped. A last line without a newline is still/// served. When a line and its newline together pass 1 MiB, or a read fails, the call ends as if/// the input had ended and returns normally. The call returns any error the control object returns./// The call keeps a 1 MiB read buffer and a 64 KiB write buffer on its stack, and it allocates from/// `allocator` for the arena alone.pub fn serveStdio(allocator: std.mem.Allocator, control: anytype) !void {    var read_buffer: [line_capacity]u8 = undefined;    var reader = sys.stdio.stdin().reader(sys.stdio.debugIo(), &read_buffer);    var write_buffer: [64 * 1024]u8 = undefined;    var writer = sys.stdio.stdout().writer(sys.stdio.debugIo(), &write_buffer);    var scratch = CommandScratch.init(allocator);    defer scratch.deinit();    while (true) {        const line = reader.interface.takeDelimiter('\n') catch break orelse break;        const trimmed = std.mem.trim(u8, line, " \t\r");        if (trimmed.len == 0) continue;        const proceed = try scratch.apply(control, trimmed, &writer.interface);        if (!proceed) break;    }}/// Serves lines to `control` on a Unix socket at `path`, one connection at a time, until the/// control object returns false. A host that a tool connects to while it runs serves the tool's/// commands on a socket path. The call deletes any file at `path` before it listens, and deletes/// the socket file again when it returns. Once it listens, the call prints one JSON line with an/// `event` of `listening` and the path in `socket` on standard output. For each connection the call/// keeps a `LineBuffer` and a `CommandWorkspace`, and after each nonempty line it writes the/// control object's reply to the connection. A read error, the client closing, or a failed write/// ends that connection, and the call accepts the next one. A partial line held when the client/// closes is dropped. `error.LineCapacityExceeded` or `error.OutOfMemory` from the held bytes, an/// error from the control object, or a failed accept returns from the call and ends service for/// every later client.pub fn serveSocket(allocator: std.mem.Allocator, control: anytype, path: []const u8) !void {    sys.fs.deleteFile(path) catch {};    const address = try sys.net.Address.initUnix(path);    var server = try sys.net.Address.listen(address, .{});    defer server.deinit();    defer sys.fs.deleteFile(path) catch {};    try announceSocket(path);    accepting: while (true) {        var accepted = try server.accept();        defer accepted.stream.close();        var lines = LineBuffer.init(allocator);        defer lines.deinit();        var workspace = CommandWorkspace.init(allocator);        defer workspace.deinit();        var chunk: [16 * 1024]u8 = undefined;        reading: while (true) {            const read = accepted.stream.read(&chunk) catch break :reading;            if (read == 0) break :reading;            try lines.feed(chunk[0..read]);            while (lines.next()) |line| {                var proceed = true;                if (line.len > 0) {                    proceed = try workspace.apply(control, line);                    accepted.stream.writeAll(workspace.written()) catch {                        lines.consume();                        break :reading;                    };                }                lines.consume();                if (!proceed) break :accepting;            }        }    }}fn announceSocket(path: []const u8) !void {    var stdout_buffer: [4096]u8 = undefined;    var writer = sys.stdio.stdout().writer(sys.stdio.debugIo(), &stdout_buffer);    var json = pretty_json.Writer.init(&writer.interface, .minified);    const root = try json.object();    try root.field("event", "listening");    try root.field("socket", path);    try root.endLine();    try writer.interface.flush();}test "line buffer frames chunked input into trimmed lines" {    var lines = LineBuffer.init(std.testing.allocator);    defer lines.deinit();    try lines.feed("{\"cmd\":\"sta");    try std.testing.expect(lines.next() == null);    try lines.feed("tus\"}\r\n{\"cmd\":\"quit\"}\n");    const first = lines.next().?;    try std.testing.expectEqualStrings("{\"cmd\":\"status\"}", first);    lines.consume();    const second = lines.next().?;    try std.testing.expectEqualStrings("{\"cmd\":\"quit\"}", second);    lines.consume();    try std.testing.expect(lines.next() == null);}

Complete call list for transport.serveSocket

10 direct calls.

Audit

Definitions19
Public names19
Members5
Version26.7.0
Revisiondaab053ee433