From 8967477f39ff6a8aeb709a8d4c3212d66449131c Mon Sep 17 00:00:00 2001 From: Dominic Grimm Date: Sun, 1 Jun 2025 19:40:58 +0200 Subject: [PATCH] Write documentation --- build.zig | 78 ++++++++++++++++++++++++--------- src/dispatchers/BlockUpdate.zig | 10 +++++ src/dispatchers/root.zig | 1 + src/models/BlockUpdateMsg.zig | 9 ++++ src/mpsc.zig | 25 ++++++++++- src/root.zig | 8 ++++ src/web/endpoints/Block.zig | 4 ++ src/web/endpoints/root.zig | 1 + src/web/models/Block.zig | 1 + src/web/root.zig | 5 +++ src/web/stores/root.zig | 1 + 11 files changed, 122 insertions(+), 21 deletions(-) diff --git a/build.zig b/build.zig index b8780dd..414a426 100644 --- a/build.zig +++ b/build.zig @@ -6,30 +6,48 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const material_max_len = b.option( - usize, - "material-max-len", - "Maximum length a material name can have", - ) orelse material_max_len_default; - const lib_options = b.addOptions(); - lib_options.addOption(usize, "material_max_len", material_max_len); + // comptime config options + const lib_options = blk: { + const material_max_len = b.option( + usize, + "material-max-len", + "Maximum length a material name can have", + ) orelse material_max_len_default; + + const options = b.addOptions(); + options.addOption(usize, "material_max_len", material_max_len); + + break :blk options; + }; const zap_dep = b.dependency("zap", .{ .openssl = false }); const clap_dep = b.dependency("clap", .{}); - const lib_mod = b.addModule("craftflut", .{ - .root_source_file = b.path("src/root.zig"), - }); - lib_mod.addOptions("build_options", lib_options); - lib_mod.addImport("zap", zap_dep.module("zap")); + // craftflut library + const lib_mod = blk: { + const mod = b.addModule("craftflut", .{ + .root_source_file = b.path("src/root.zig"), + }); + mod.addOptions("build_options", lib_options); - const exe_mod = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - exe_mod.addImport("craftflut", lib_mod); - exe_mod.addImport("clap", clap_dep.module("clap")); + mod.addImport("zap", zap_dep.module("zap")); + + break :blk mod; + }; + + // craftflut executable + const exe_mod = blk: { + const mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + mod.addImport("craftflut", lib_mod); + + mod.addImport("clap", clap_dep.module("clap")); + + break :blk mod; + }; const exe = b.addExecutable(.{ .name = "craftflut", @@ -37,12 +55,32 @@ pub fn build(b: *std.Build) void { }); b.installArtifact(exe); + // run step const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } - const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + + // const lib_unit_tests = b.addTest(.{ + // .root_module = lib_mod, + // }); + + // const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + // const exe_unit_tests = b.addTest(.{ + // .root_module = exe_mod, + // }); + + // const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // // Similar to creating the run step earlier, this exposes a `test` step to + // // the `zig build --help` menu, providing a way for the user to request + // // running the unit tests. + // const test_step = b.step("test", "Run unit tests"); + // test_step.dependOn(&run_lib_unit_tests.step); + // test_step.dependOn(&run_exe_unit_tests.step); + } diff --git a/src/dispatchers/BlockUpdate.zig b/src/dispatchers/BlockUpdate.zig index 8cef5fa..38d71d3 100644 --- a/src/dispatchers/BlockUpdate.zig +++ b/src/dispatchers/BlockUpdate.zig @@ -7,14 +7,21 @@ const Self = @This(); const log = std.log.scoped(.block_update_dispatcher); +/// Block update dispatcher config pub const Config = struct { + /// Queue capacity capacity: usize, }; allocator: std.mem.Allocator, +/// Dispatcher config config: *const Config, + +/// Block update queue queue: *msg_queue.MsgQueueUnmanaged(models.BlockUpdateMsg), + +/// Payload stream resource stream: ?std.net.Stream = null, pub fn init( @@ -31,6 +38,7 @@ pub fn init( }; } +/// Sends one block update payload to the stream, terminates it with a newline. fn send(self: *Self, update: *const models.BlockUpdateMsg) !void { var buf: [models.BlockUpdateMsg.csv_max_len]u8 = undefined; const stream = self.stream.?; @@ -39,7 +47,9 @@ fn send(self: *Self, update: *const models.BlockUpdateMsg) !void { try stream.writeAll(&.{'\n'}); } +/// Starts dispatcher. Receives new queue entries and sends them to the stream. pub fn start(self: *Self) !void { + // buffer for `tryDequeueAll` with maximum queue capacity as size const buf = try self.allocator.alloc(models.BlockUpdateMsg, self.queue.capacity()); defer self.allocator.free(buf); diff --git a/src/dispatchers/root.zig b/src/dispatchers/root.zig index ccbedae..4dbca15 100644 --- a/src/dispatchers/root.zig +++ b/src/dispatchers/root.zig @@ -1 +1,2 @@ +/// Block update dispatcher for generic stream (`std.net.Stream`) pub const BlockUpdate = @import("BlockUpdate.zig"); diff --git a/src/models/BlockUpdateMsg.zig b/src/models/BlockUpdateMsg.zig index 9fbbdb6..741f0fe 100644 --- a/src/models/BlockUpdateMsg.zig +++ b/src/models/BlockUpdateMsg.zig @@ -7,7 +7,10 @@ const Self = @This(); pub const Dimension = i32; pub const Coordinate = i32; +/// Maximum length of a material name pub const material_max_len = build_options.material_max_len; + +/// Maximum CSV payload size for a block update pub const csv_max_len: usize = blk: { const str = std.fmt.comptimePrint( "-{d},-{d},-{d},-{d},", @@ -25,9 +28,15 @@ dimension: Dimension, x: Coordinate, y: Coordinate, z: Coordinate, + +/// Material name buffer material: [material_max_len]u8 = undefined, + +/// Length of material name buffer `material` material_len: usize = 0, +/// Converts block update message to CSV payload +/// Format: *`,,,,`* (`,,,,`) pub fn toCsv(self: *const Self, buf: *[csv_max_len]u8) ![]u8 { return try std.fmt.bufPrint(buf, "{d},{d},{d},{d},{s}", .{ self.dimension, diff --git a/src/mpsc.zig b/src/mpsc.zig index 5b5439c..bf59cea 100644 --- a/src/mpsc.zig +++ b/src/mpsc.zig @@ -1,21 +1,34 @@ const std = @import("std"); +/// Generic slot with value type `T` for a ring buffer pub fn Slot(comptime T: type) type { return struct { + /// Value value: T, + + /// Slot version version: std.atomic.Value(usize), }; } +/// Ring buffer with value type `T` on a slice of slots pub fn RingBuffer(comptime T: type) type { return struct { const Self = @This(); + /// Ring buffer capacity capacity: usize, + + /// Buffer slice buffer: []Slot(T), + + /// Atomic head index of buffer (write index) head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), + + /// Tail index of buffer (read index) tail: usize = 0, + /// Initializes ring buffer with a given buffer, throws `error.CapacityTooSmall` if the buffer capacity is less or equal than 1. pub fn init(buffer: []Slot(T)) error{CapacityTooSmall}!Self { if (buffer.len <= 1) { return error.CapacityTooSmall; @@ -31,49 +44,59 @@ pub fn RingBuffer(comptime T: type) type { }; } + /// Enqueues element. Throws `error.Overflow` if no more elements fit. pub fn enqueue(self: *Self, value: T) error{Overflow}!void { while (true) { + // Acquire write slot const head = self.head.load(.acquire); const index = head % self.capacity; const slot = &self.buffer[index]; + // Check if slot has been read (empty) const expected_version = head; if (slot.version.load(.acquire) != expected_version) { return error.Overflow; } + // Compare and swap head index if (self.head.cmpxchgStrong( head, head + 1, .seq_cst, .seq_cst, )) |_| { - std.atomic.spinLoopHint(); + std.atomic.spinLoopHint(); // Retry again next cycle continue; } + // Slot versioning slot.value = value; slot.version.store(expected_version + 1, .release); return; } } + /// Dequeues element. Throws `error.Underflow` if ring buffer is empty. pub fn dequeue(self: *Self) error{Underflow}!T { + // Acquire read slot const tail = self.tail; const index = tail % self.capacity; const slot = &self.buffer[index]; + // Check is slot has been written to (full) const expected_version = tail + 1; if (slot.version.load(.acquire) != expected_version) { return error.Underflow; } + // Slot versioning const value = slot.value; slot.version.store(tail +% self.capacity, .release); self.tail +%= 1; return value; } + /// Dequeues all stores elements into a buffer. It must at least have a size of the buffer capacity. pub fn dequeueAll(self: *Self, buf: []T) ![]T { if (buf.len < self.capacity) { return error.BufferTooSmall; diff --git a/src/root.zig b/src/root.zig index c91ef84..f0b7d4c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -7,16 +7,24 @@ const web = @import("web/root.zig"); const log = std.log.scoped(.craftflut); +/// Craftflut gateway configuration options pub const Config = struct { + /// Path to Unix socket that receives update data unix_socket_path: []const u8, + + /// Dispatcher config dispatcher: dispatchers.BlockUpdate.Config, + + /// Web server config web: web.Config, }; +/// Block update dispatch wrapper. Panics if error occurs. fn receiver(dispatcher: *dispatchers.BlockUpdate) void { dispatcher.start() catch unreachable; } +/// Starts craftflut gateway pub fn start(allocator: std.mem.Allocator, config: *const Config) !void { log.info("Starting craftflut gateway", .{}); diff --git a/src/web/endpoints/Block.zig b/src/web/endpoints/Block.zig index 1701bb7..21a1962 100644 --- a/src/web/endpoints/Block.zig +++ b/src/web/endpoints/Block.zig @@ -36,17 +36,21 @@ pub fn post(_: *Self, r: zap.Request) !void { r.markAsFinished(true); } +/// Enqueues block update message if payload is correct pub fn put(self: *Self, r: zap.Request) !void { blk: { if (r.body) |body| { + // Parse JSON body const maybe_block: ?std.json.Parsed(web_models.Block) = std.json.parseFromSlice(web_models.Block, self.allocator, body, .{}) catch null; if (maybe_block) |parsed| { defer parsed.deinit(); const block = parsed.value; + // Check if material name is valid if (block.material.len > models.BlockUpdateMsg.material_max_len or !web_models.Block.materialIsValid(block.material)) { break :blk; } + // Enqueue message var msg = models.BlockUpdateMsg{ .dimension = block.dimension, .x = block.x, diff --git a/src/web/endpoints/root.zig b/src/web/endpoints/root.zig index 6a7030d..f136ad0 100644 --- a/src/web/endpoints/root.zig +++ b/src/web/endpoints/root.zig @@ -1 +1,2 @@ +/// Block API endpoint pub const Block = @import("Block.zig"); diff --git a/src/web/models/Block.zig b/src/web/models/Block.zig index 2df4710..d9dbc16 100644 --- a/src/web/models/Block.zig +++ b/src/web/models/Block.zig @@ -8,6 +8,7 @@ y: models.BlockUpdateMsg.Coordinate, z: models.BlockUpdateMsg.Coordinate, material: []const u8, +/// Checks if material name characters are valid. Doesn't check if maximum length is exceeded. pub fn materialIsValid(material: []const u8) bool { for (material) |ch| { if (!std.ascii.isAlphabetic(ch)) diff --git a/src/web/root.zig b/src/web/root.zig index f6cb7f6..f99bd9e 100644 --- a/src/web/root.zig +++ b/src/web/root.zig @@ -9,8 +9,12 @@ const stores = @import("stores/root.zig"); const log = std.log.scoped(.web); +/// Web server configuration options pub const Config = struct { + /// Web server port port: u16, + + /// Web server thread count threads: u8, }; @@ -18,6 +22,7 @@ fn onRequest(r: zap.Request) !void { _ = r; } +/// Will start web server with API. Needs block update queue. pub fn start( allocator: std.mem.Allocator, config: *const Config, diff --git a/src/web/stores/root.zig b/src/web/stores/root.zig index 415b052..117b8d0 100644 --- a/src/web/stores/root.zig +++ b/src/web/stores/root.zig @@ -1 +1,2 @@ +/// Block update queue store for endpoints pub const BlockUpdateQueue = @import("BlockUpdateQueue.zig");