Write documentation
This commit is contained in:
parent
c9c2b45b2a
commit
8967477f39
11 changed files with 122 additions and 21 deletions
78
build.zig
78
build.zig
|
@ -6,30 +6,48 @@ pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const material_max_len = b.option(
|
// comptime config options
|
||||||
usize,
|
const lib_options = blk: {
|
||||||
"material-max-len",
|
const material_max_len = b.option(
|
||||||
"Maximum length a material name can have",
|
usize,
|
||||||
) orelse material_max_len_default;
|
"material-max-len",
|
||||||
const lib_options = b.addOptions();
|
"Maximum length a material name can have",
|
||||||
lib_options.addOption(usize, "material_max_len", material_max_len);
|
) 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 zap_dep = b.dependency("zap", .{ .openssl = false });
|
||||||
const clap_dep = b.dependency("clap", .{});
|
const clap_dep = b.dependency("clap", .{});
|
||||||
|
|
||||||
const lib_mod = b.addModule("craftflut", .{
|
// craftflut library
|
||||||
.root_source_file = b.path("src/root.zig"),
|
const lib_mod = blk: {
|
||||||
});
|
const mod = b.addModule("craftflut", .{
|
||||||
lib_mod.addOptions("build_options", lib_options);
|
.root_source_file = b.path("src/root.zig"),
|
||||||
lib_mod.addImport("zap", zap_dep.module("zap"));
|
});
|
||||||
|
mod.addOptions("build_options", lib_options);
|
||||||
|
|
||||||
const exe_mod = b.createModule(.{
|
mod.addImport("zap", zap_dep.module("zap"));
|
||||||
.root_source_file = b.path("src/main.zig"),
|
|
||||||
.target = target,
|
break :blk mod;
|
||||||
.optimize = optimize,
|
};
|
||||||
});
|
|
||||||
exe_mod.addImport("craftflut", lib_mod);
|
// craftflut executable
|
||||||
exe_mod.addImport("clap", clap_dep.module("clap"));
|
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(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "craftflut",
|
.name = "craftflut",
|
||||||
|
@ -37,12 +55,32 @@ pub fn build(b: *std.Build) void {
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
// run step
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
run_cmd.step.dependOn(b.getInstallStep());
|
||||||
if (b.args) |args| {
|
if (b.args) |args| {
|
||||||
run_cmd.addArgs(args);
|
run_cmd.addArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
const run_step = b.step("run", "Run the app");
|
const run_step = b.step("run", "Run the app");
|
||||||
run_step.dependOn(&run_cmd.step);
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,21 @@ const Self = @This();
|
||||||
|
|
||||||
const log = std.log.scoped(.block_update_dispatcher);
|
const log = std.log.scoped(.block_update_dispatcher);
|
||||||
|
|
||||||
|
/// Block update dispatcher config
|
||||||
pub const Config = struct {
|
pub const Config = struct {
|
||||||
|
/// Queue capacity
|
||||||
capacity: usize,
|
capacity: usize,
|
||||||
};
|
};
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
/// Dispatcher config
|
||||||
config: *const Config,
|
config: *const Config,
|
||||||
|
|
||||||
|
/// Block update queue
|
||||||
queue: *msg_queue.MsgQueueUnmanaged(models.BlockUpdateMsg),
|
queue: *msg_queue.MsgQueueUnmanaged(models.BlockUpdateMsg),
|
||||||
|
|
||||||
|
/// Payload stream resource
|
||||||
stream: ?std.net.Stream = null,
|
stream: ?std.net.Stream = null,
|
||||||
|
|
||||||
pub fn init(
|
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 {
|
fn send(self: *Self, update: *const models.BlockUpdateMsg) !void {
|
||||||
var buf: [models.BlockUpdateMsg.csv_max_len]u8 = undefined;
|
var buf: [models.BlockUpdateMsg.csv_max_len]u8 = undefined;
|
||||||
const stream = self.stream.?;
|
const stream = self.stream.?;
|
||||||
|
@ -39,7 +47,9 @@ fn send(self: *Self, update: *const models.BlockUpdateMsg) !void {
|
||||||
try stream.writeAll(&.{'\n'});
|
try stream.writeAll(&.{'\n'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts dispatcher. Receives new queue entries and sends them to the stream.
|
||||||
pub fn start(self: *Self) !void {
|
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());
|
const buf = try self.allocator.alloc(models.BlockUpdateMsg, self.queue.capacity());
|
||||||
defer self.allocator.free(buf);
|
defer self.allocator.free(buf);
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
/// Block update dispatcher for generic stream (`std.net.Stream`)
|
||||||
pub const BlockUpdate = @import("BlockUpdate.zig");
|
pub const BlockUpdate = @import("BlockUpdate.zig");
|
||||||
|
|
|
@ -7,7 +7,10 @@ const Self = @This();
|
||||||
pub const Dimension = i32;
|
pub const Dimension = i32;
|
||||||
pub const Coordinate = i32;
|
pub const Coordinate = i32;
|
||||||
|
|
||||||
|
/// Maximum length of a material name
|
||||||
pub const material_max_len = build_options.material_max_len;
|
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: {
|
pub const csv_max_len: usize = blk: {
|
||||||
const str = std.fmt.comptimePrint(
|
const str = std.fmt.comptimePrint(
|
||||||
"-{d},-{d},-{d},-{d},",
|
"-{d},-{d},-{d},-{d},",
|
||||||
|
@ -25,9 +28,15 @@ dimension: Dimension,
|
||||||
x: Coordinate,
|
x: Coordinate,
|
||||||
y: Coordinate,
|
y: Coordinate,
|
||||||
z: Coordinate,
|
z: Coordinate,
|
||||||
|
|
||||||
|
/// Material name buffer
|
||||||
material: [material_max_len]u8 = undefined,
|
material: [material_max_len]u8 = undefined,
|
||||||
|
|
||||||
|
/// Length of material name buffer `material`
|
||||||
material_len: usize = 0,
|
material_len: usize = 0,
|
||||||
|
|
||||||
|
/// Converts block update message to CSV payload
|
||||||
|
/// Format: *`<dimension>,<x>,<y>,<z>,<material>`* (`<int32>,<int32>,<int32>,<int32>,<str>`)
|
||||||
pub fn toCsv(self: *const Self, buf: *[csv_max_len]u8) ![]u8 {
|
pub fn toCsv(self: *const Self, buf: *[csv_max_len]u8) ![]u8 {
|
||||||
return try std.fmt.bufPrint(buf, "{d},{d},{d},{d},{s}", .{
|
return try std.fmt.bufPrint(buf, "{d},{d},{d},{d},{s}", .{
|
||||||
self.dimension,
|
self.dimension,
|
||||||
|
|
25
src/mpsc.zig
25
src/mpsc.zig
|
@ -1,21 +1,34 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Generic slot with value type `T` for a ring buffer
|
||||||
pub fn Slot(comptime T: type) type {
|
pub fn Slot(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
|
/// Value
|
||||||
value: T,
|
value: T,
|
||||||
|
|
||||||
|
/// Slot version
|
||||||
version: std.atomic.Value(usize),
|
version: std.atomic.Value(usize),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ring buffer with value type `T` on a slice of slots
|
||||||
pub fn RingBuffer(comptime T: type) type {
|
pub fn RingBuffer(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Ring buffer capacity
|
||||||
capacity: usize,
|
capacity: usize,
|
||||||
|
|
||||||
|
/// Buffer slice
|
||||||
buffer: []Slot(T),
|
buffer: []Slot(T),
|
||||||
|
|
||||||
|
/// Atomic head index of buffer (write index)
|
||||||
head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
|
head: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
|
||||||
|
|
||||||
|
/// Tail index of buffer (read index)
|
||||||
tail: usize = 0,
|
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 {
|
pub fn init(buffer: []Slot(T)) error{CapacityTooSmall}!Self {
|
||||||
if (buffer.len <= 1) {
|
if (buffer.len <= 1) {
|
||||||
return error.CapacityTooSmall;
|
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 {
|
pub fn enqueue(self: *Self, value: T) error{Overflow}!void {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
// Acquire write slot
|
||||||
const head = self.head.load(.acquire);
|
const head = self.head.load(.acquire);
|
||||||
const index = head % self.capacity;
|
const index = head % self.capacity;
|
||||||
const slot = &self.buffer[index];
|
const slot = &self.buffer[index];
|
||||||
|
|
||||||
|
// Check if slot has been read (empty)
|
||||||
const expected_version = head;
|
const expected_version = head;
|
||||||
if (slot.version.load(.acquire) != expected_version) {
|
if (slot.version.load(.acquire) != expected_version) {
|
||||||
return error.Overflow;
|
return error.Overflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare and swap head index
|
||||||
if (self.head.cmpxchgStrong(
|
if (self.head.cmpxchgStrong(
|
||||||
head,
|
head,
|
||||||
head + 1,
|
head + 1,
|
||||||
.seq_cst,
|
.seq_cst,
|
||||||
.seq_cst,
|
.seq_cst,
|
||||||
)) |_| {
|
)) |_| {
|
||||||
std.atomic.spinLoopHint();
|
std.atomic.spinLoopHint(); // Retry again next cycle
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slot versioning
|
||||||
slot.value = value;
|
slot.value = value;
|
||||||
slot.version.store(expected_version + 1, .release);
|
slot.version.store(expected_version + 1, .release);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dequeues element. Throws `error.Underflow` if ring buffer is empty.
|
||||||
pub fn dequeue(self: *Self) error{Underflow}!T {
|
pub fn dequeue(self: *Self) error{Underflow}!T {
|
||||||
|
// Acquire read slot
|
||||||
const tail = self.tail;
|
const tail = self.tail;
|
||||||
const index = tail % self.capacity;
|
const index = tail % self.capacity;
|
||||||
const slot = &self.buffer[index];
|
const slot = &self.buffer[index];
|
||||||
|
|
||||||
|
// Check is slot has been written to (full)
|
||||||
const expected_version = tail + 1;
|
const expected_version = tail + 1;
|
||||||
if (slot.version.load(.acquire) != expected_version) {
|
if (slot.version.load(.acquire) != expected_version) {
|
||||||
return error.Underflow;
|
return error.Underflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slot versioning
|
||||||
const value = slot.value;
|
const value = slot.value;
|
||||||
slot.version.store(tail +% self.capacity, .release);
|
slot.version.store(tail +% self.capacity, .release);
|
||||||
self.tail +%= 1;
|
self.tail +%= 1;
|
||||||
return value;
|
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 {
|
pub fn dequeueAll(self: *Self, buf: []T) ![]T {
|
||||||
if (buf.len < self.capacity) {
|
if (buf.len < self.capacity) {
|
||||||
return error.BufferTooSmall;
|
return error.BufferTooSmall;
|
||||||
|
|
|
@ -7,16 +7,24 @@ const web = @import("web/root.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.craftflut);
|
const log = std.log.scoped(.craftflut);
|
||||||
|
|
||||||
|
/// Craftflut gateway configuration options
|
||||||
pub const Config = struct {
|
pub const Config = struct {
|
||||||
|
/// Path to Unix socket that receives update data
|
||||||
unix_socket_path: []const u8,
|
unix_socket_path: []const u8,
|
||||||
|
|
||||||
|
/// Dispatcher config
|
||||||
dispatcher: dispatchers.BlockUpdate.Config,
|
dispatcher: dispatchers.BlockUpdate.Config,
|
||||||
|
|
||||||
|
/// Web server config
|
||||||
web: web.Config,
|
web: web.Config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Block update dispatch wrapper. Panics if error occurs.
|
||||||
fn receiver(dispatcher: *dispatchers.BlockUpdate) void {
|
fn receiver(dispatcher: *dispatchers.BlockUpdate) void {
|
||||||
dispatcher.start() catch unreachable;
|
dispatcher.start() catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts craftflut gateway
|
||||||
pub fn start(allocator: std.mem.Allocator, config: *const Config) !void {
|
pub fn start(allocator: std.mem.Allocator, config: *const Config) !void {
|
||||||
log.info("Starting craftflut gateway", .{});
|
log.info("Starting craftflut gateway", .{});
|
||||||
|
|
||||||
|
|
|
@ -36,17 +36,21 @@ pub fn post(_: *Self, r: zap.Request) !void {
|
||||||
r.markAsFinished(true);
|
r.markAsFinished(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enqueues block update message if payload is correct
|
||||||
pub fn put(self: *Self, r: zap.Request) !void {
|
pub fn put(self: *Self, r: zap.Request) !void {
|
||||||
blk: {
|
blk: {
|
||||||
if (r.body) |body| {
|
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;
|
const maybe_block: ?std.json.Parsed(web_models.Block) = std.json.parseFromSlice(web_models.Block, self.allocator, body, .{}) catch null;
|
||||||
if (maybe_block) |parsed| {
|
if (maybe_block) |parsed| {
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
const block = parsed.value;
|
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)) {
|
if (block.material.len > models.BlockUpdateMsg.material_max_len or !web_models.Block.materialIsValid(block.material)) {
|
||||||
break :blk;
|
break :blk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enqueue message
|
||||||
var msg = models.BlockUpdateMsg{
|
var msg = models.BlockUpdateMsg{
|
||||||
.dimension = block.dimension,
|
.dimension = block.dimension,
|
||||||
.x = block.x,
|
.x = block.x,
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
/// Block API endpoint
|
||||||
pub const Block = @import("Block.zig");
|
pub const Block = @import("Block.zig");
|
||||||
|
|
|
@ -8,6 +8,7 @@ y: models.BlockUpdateMsg.Coordinate,
|
||||||
z: models.BlockUpdateMsg.Coordinate,
|
z: models.BlockUpdateMsg.Coordinate,
|
||||||
material: []const u8,
|
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 {
|
pub fn materialIsValid(material: []const u8) bool {
|
||||||
for (material) |ch| {
|
for (material) |ch| {
|
||||||
if (!std.ascii.isAlphabetic(ch))
|
if (!std.ascii.isAlphabetic(ch))
|
||||||
|
|
|
@ -9,8 +9,12 @@ const stores = @import("stores/root.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.web);
|
const log = std.log.scoped(.web);
|
||||||
|
|
||||||
|
/// Web server configuration options
|
||||||
pub const Config = struct {
|
pub const Config = struct {
|
||||||
|
/// Web server port
|
||||||
port: u16,
|
port: u16,
|
||||||
|
|
||||||
|
/// Web server thread count
|
||||||
threads: u8,
|
threads: u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +22,7 @@ fn onRequest(r: zap.Request) !void {
|
||||||
_ = r;
|
_ = r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Will start web server with API. Needs block update queue.
|
||||||
pub fn start(
|
pub fn start(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
config: *const Config,
|
config: *const Config,
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
/// Block update queue store for endpoints
|
||||||
pub const BlockUpdateQueue = @import("BlockUpdateQueue.zig");
|
pub const BlockUpdateQueue = @import("BlockUpdateQueue.zig");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue