Zero-allocation declarative Zig framework
Add Zen as a dependency:
zig fetch --save git+https://gitlab.com/tadej3/zig-zen.gitLink against Zen in your build.zig:
const zen = b.dependency("zen", .{ .target = target, .optimize = optimize });
mod.addImport("zen", zen.module("zen"));zig buildzig build testZen makes heavy use of Zig's comptime to generate application code from a declarative specification. Every Zen
application starts with a zen.App(.{}) spec which describes the servers, background tasks, clients, and other
components which make up the application. The application can then be started with a runtime of choice (sync, async,
concurrent). At the core is the I/O reactor which listens for events and notifies the appropriate consumer.
Zen API documentation can be found on Gitlab Pages or built using zig build docs.
const std = @import("std");
const zen = @import("zen");
const log = std.log.scoped(.example);
/// How many concurrent I/O workers and consequently app instances do we want to start?
const NUM_IO_WORKERS = 3;
// Describe the shape of your config
const Config = zen.cfg.Config(struct {
TCP_BIND: []const u8,
});
// Describe the application
const App = zen.App(.{
.servers = .{
zen.tcp.Server(.{
// Components may be configured with comptime-known values or lazy configuration references
// .listen = "127.0.0.1:1355",
.listen = Config.lazy.TCP_BIND,
}),
},
});
// Prepare storage for the number of ioWorkers you plan to start. Each I/O worker gets its own event loop, listening
// sockets, and other resources, including its own app instance.
var instances: [NUM_IO_WORKERS]zen.AppInstance(App) = undefined;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
// Pick your I/O (must support concurrency)
var threaded: std.Io.Threaded = .init(allocator, .{ .environ = .empty });
defer threaded.deinit();
const io = threaded.io();
// Load the configuration from your preferred source (env, dotenv file, json, ...)
var cfg = try zen.cfg.loadEnv(Config, allocator, init.minimal.environ);
defer zen.cfg.deinit(Config, allocator, cfg);
// Prepare an I/O group to run the app in
// You can always add your own tasks to this group
var ioGroup: std.Io.Group = .init;
defer ioGroup.cancel(io);
// Start the reactor!
try zen.run(App, io, &ioGroup, cfg, &instances, .{ .ioWorkers = NUM_IO_WORKERS });
defer zen.stop(App, &instances);
// Register a SIGINT handler to enable graceful shutdown
try zen.zio.SignalHandler.register(io, &ioGroup, shutdown);
// Await the group
try ioGroup.await(io);
}
fn shutdown() void {
zen.stop(App, &instances);
}The configuration component supports different sources, like the system environment, dotenv file format, json, ... A configuration is first described as a struct - exact restrictions depend on the source, see below.
NOTE: Not all sources support zero-allocation
const Config = zen.cfg.Config(struct {
STRING: []const u8,
NUMBER: u32,
BOOL: bool,
ARRAY: [3]f64,
});
// Use lazy references to configure your app components. These get resolved to actual values during runtime
zen.tcp.Server(.{ .listen = Config.lazy.STRING })After describing the configuration shape, load it from your favorite source.
var cfg = try zen.cfg.loadEnv(Config, allocator, environ);
defer zen.cfg.deinit(Config, allocator, cfg);
// Access the loaded values
cfg.value.STRING;
cfg.value.NUMBER;
cfg.value.BOOL;
cfg.value.ARRAY;When values are loaded during runtime, their type is coerced to match the requested shape. The only exception to this
rule is zen.cfg.static() which allows any datatype and doesn't perform any coercion. Otherwise, supported data types
are strings, numbers (int and float), bool, array, slice, optional. The difference between arrays and slices is simply
that arrays must contain the exact number of elements specified in the config description, while slices behave like
variable length arrays.
NOTE: Formats which don't natively support arrays or slices use a comma separated string of values instead. When bools aren't supported, case-insensitive strings "true", "1", "false", "0" may be used.
Allows you to wrap an existing object into a Config instance. A shallow copy of the object is made. No allocations
take place, caller is responsible for managing object's lifecycle. Consequently, calling zen.cfg.deinit(Config, allocator, cfg) is completely optional, but not invalid.
This source has no limitations on the configuration shape
const Config = zen.cfg.Config(struct {
abc: []const u8,
});
var cfg = zen.cfg.static(Config, .{ .abc = "hello" });
defer zen.cfg.deinit(Config, allocator, cfg); // Optional
std.debug.print("{s}\n", .{ cfg.value.abc });Configuration can be loaded from system environment variables, though this always requires allocation. Config field names are matched against environment variable names and values are coerced to the requested type.
This source doesn't support nesting
const Config = zen.cfg.Config(struct {
PWD: []const u8,
});
var cfg = try zen.cfg.loadEnv(Config, allocator, environ);
defer zen.cfg.deinit(Config, allocator, cfg);
std.debug.print("{s}\n", .{ cfg.value.PWD });Similar to the system environment source, except values are loaded from a dotenv string.
Generally, this source is zero-allocation, except when loading slices, however the input string must outlive the config instance.
String loaded from a .env file:
ABC=123
DEF=foo
const Config = zen.cfg.Config(struct {
ABC: u16,
DEF: []const u8,
});
const dotenv = "..."; // e.g. load this from a file
var cfg = try zen.cfg.loadDotEnv(Config, dotenv);
defer zen.cfg.deinit(Config, allocator, cfg);
std.debug.print("{d} {s}\n", .{ cfg.value.ABC, cfg.value.DEF });JSON can be used for more complex configurations as it supports nesting and native arrays/slices and bools.
const Config = zen.cfg.Config(struct {
abc: u16,
def: struct { foo: bool, bar: []i32, baz: []const u8 },
});
// Configure components with lazy references to nested fields
... = Config.lazy.def.bar;
// ...or pass the entire nested object
... = Config.lazy.def;
const json = "..."; // e.g. load this from a file
var cfg = try zen.cfg.loadJson(Config, allocator, json);
defer zen.cfg.deinit(Config, allocator, cfg);
std.debug.print("{d} {s}\n", .{ cfg.value.abc, cfg.value.def.baz });Sometimes, configuration is loaded from multiple sources, but the runtime accepts a single config. Multiple
configurations may be merged into one using a Group(). The shape of the type returned by Group() is identical to the
shape of a Config(), meaning groups can be merged as well.
NOTE: The group only performs a shallow copy of each config
const std = @import("std");
const zen = @import("zen");
const log = std.log.scoped(.main);
const BUFF_LEN = 8 * 1024;
const MAX_CONN = 128;
const NUM_IO_WORKERS = 3;
var readBuffer = [_]u8{0} ** (MAX_CONN * BUFF_LEN);
var readBufferFreeStack = [_]u32{0} ** MAX_CONN;
var writeBuffer = [_]u8{0} ** (MAX_CONN * BUFF_LEN);
var writeBufferFreeStack = [_]u32{0} ** MAX_CONN;
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
var threaded = std.Io.Threaded.init(allocator, .{ .environ = .empty });
defer threaded.deinit();
const io = threaded.io();
// Create the default config and load it from the system environment
const Config = zen.cfg.Config(struct { TCP_BIND: []const u8 });
var cfg = try zen.cfg.loadEnv(Config, allocator, init.minimal.environ);
defer zen.cfg.deinit(Config, allocator, cfg);
// Create a static config for http buffers
const HttpBuffersConfig = zen.cfg.Config(zen.http.ClientBuffers);
const httpBuffersCfg = HttpBuffersConfig.static(.{
.readBuffer = .{ .data = &readBuffer, .freeStack = &readBufferFreeStack, .chunkSize = BUFF_LEN },
.writeBuffer = .{ .data = &writeBuffer, .freeStack = &writeBufferFreeStack, .chunkSize = BUFF_LEN },
});
// Create a combined type of the two configs, assigning a name to each
const ConfigGroup = zen.cfg.Group(.{
.default = Config,
.http = HttpBuffersConfig,
});
// Use the combined type to merge the two loaded configs at runtime
const groupCfg = ConfigGroup.from(.{
.default = &cfg,
.http = &httpBuffersCfg,
});
const App = zen.App(.{
.servers = .{
zen.http.Server(.{
// Configure your app using lazy references to the group config
.listen = ConfigGroup.lazy.default.TCP_BIND,
.buffers = ConfigGroup.lazy.http,
}),
},
});
var ioGroup: std.Io.Group = .init;
defer ioGroup.cancel(io);
var instances: [NUM_IO_WORKERS]zen.AppInstance(App) = undefined;
// Start the app using the group config
try zen.run(App, io, &ioGroup, groupCfg, &instances, .{ .ioWorkers = NUM_IO_WORKERS });
defer zen.stop(App, &instances);
ioGroup.await(io) catch {
log.info("I/O group cancelled", .{});
};
}