Skip to content

tadejg/Zig-Zen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zen

Zero-allocation declarative Zig framework



Usage

Add Zen as a dependency:

zig fetch --save git+https://gitlab.com/tadej3/zig-zen.git

Link against Zen in your build.zig:

const zen = b.dependency("zen", .{ .target = target, .optimize = optimize });
mod.addImport("zen", zen.module("zen"));

Building

zig build

Testing

zig build test

Architecture

Zen 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.

API Documentation

Zen API documentation can be found on Gitlab Pages or built using zig build docs.

Quick Start

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);
}

Configuration

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;

Value Coercion

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.

Sources

Static

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 });

System Environment

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 });

Dotenv Format

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

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 });

Groups

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", .{});
    };
}

About

This is a mirror of https://gitlab.com/tadej3/zig-zen

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages