zul
zul copied to clipboard
zig utility library
Zig Utility Library
The purpose of this library is to enhance Zig's standard library. Much of zul wraps Zig's std to provide simpler APIs for common tasks (e.g. reading lines from a file). In other cases, new functionality has been added (e.g. a UUID type).
Besides Zig's standard library, there are no dependencies. Most functionality is contained within its own file and can be copy and pasted into an existing library or project.
Full documentation is available at: https://www.goblgobl.com/zul/.
(This readme is auto-generated from docs/src/readme.njk)
Usage
In your build.zig.zon add a reference to Zul:
.{
.name = "my-app",
.paths = .{""},
.version = "0.0.0",
.dependencies = .{
.zul = .{
.url = "https://github.com/karlseguin/zul/archive/master.tar.gz",
.hash = "$INSERT_HASH_HERE"
},
},
}
To get the hash, run:
zig fetch https://github.com/karlseguin/zul/archive/master.tar.gz
Instead of master
you can use a specific commit/tag.
Next, in your build.zig
, you should already have an executable, something like:
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
Add the following line:
exe.root_module.addImport("zul", b.dependency("zul", .{}).module("zul"));
You can now const zul = @import("zul");
in your project.
zul.benchmark.run
Simple benchmarking function.
const HAYSTACK = "abcdefghijklmnopqrstvuwxyz0123456789";
pub fn main() !void {
(try zul.benchmark.run(indexOfScalar, .{})).print("indexOfScalar");
(try zul.benchmark.run(lastIndexOfScalar, .{})).print("lastIndexOfScalar");
}
fn indexOfScalar(_: Allocator, _: *std.time.Timer) !void {
const i = std.mem.indexOfScalar(u8, HAYSTACK, '9').?;
if (i != 35) {
@panic("fail");
}
}
fn lastIndexOfScalar(_: Allocator, _: *std.time.Timer) !void {
const i = std.mem.lastIndexOfScalar(u8, HAYSTACK, 'a').?;
if (i != 0) {
@panic("fail");
}
}
// indexOfScalar
// 49882322 iterations 59.45ns per iterations
// worst: 167ns median: 42ns stddev: 20.66ns
//
// lastIndexOfScalar
// 20993066 iterations 142.15ns per iterations
// worst: 292ns median: 125ns stddev: 23.13ns
zul.CommandLineArgs
A simple command line parser.
var args = try zul.CommandLineArgs.parse(allocator);
defer args.deinit();
if (args.contains("version")) {
//todo: print the version
os.exit(0);
}
// Values retrieved from args.get are valid until args.deinit()
// is called. Dupe the value if needed.
const host = args.get("host") orelse "127.0.0.1";
...
zul.DateTime
Simple (no leap seconds, UTC-only), DateTime, Date and Time types.
// Currently only supports RFC3339
const dt = try zul.DateTime.parse("2028-11-05T23:29:10Z", .rfc3339);
const next_week = try dt.add(7, .days);
std.debug.assert(next_week.order(dt) == .gt);
// 1857079750000 == 2028-11-05T23:29:10Z
std.debug.print("{d} == {s}", .{dt.unix(.milliseconds), dt});
zul.fs.readDir
Iterates, non-recursively, through a directory.
// Parameters:
// 1- Absolute or relative directory path
var it = try zul.fs.readDir("/tmp/dir");
defer it.deinit();
// can iterate through the files
while (try it.next()) |entry| {
std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
}
// reset the iterator
it.reset();
// or can collect them into a slice, optionally sorted:
const sorted_entries = try it.all(allocator, .dir_first);
for (sorted_entries) |entry| {
std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
}
zul.fs.readJson
Reads and parses a JSON file.
// Parameters:
// 1- The type to parse the JSON data into
// 2- An allocator
// 3- Absolute or relative path
// 4- std.json.ParseOptions
const managed_user = try zul.fs.readJson(User, allocator, "/tmp/data.json", .{});
// readJson returns a zul.Managed(T)
// managed_user.value is valid until managed_user.deinit() is called
defer managed_user.deinit();
const user = managed_user.value;
zul.fs.readLines
Iterate over the lines in a file.
// create a buffer large enough to hold the longest valid line
var line_buffer: [1024]u8 = undefined;
// Parameters:
// 1- an absolute or relative path to the file
// 2- the line buffer
// 3- options (here we're using the default)
var it = try zul.fs.readLines("/tmp/data.txt", &line_buffer, .{});
defer it.deinit();
while (try it.next()) |line| {
// line is only valid until the next call to
// it.next() or it.deinit()
std.debug.print("line: {s}\n", .{line});
}
zul.http.Client
A wrapper around std.http.Client to make it easier to create requests and consume responses.
// The client is thread-safe
var client = zul.http.Client.init(allocator);
defer client.deinit();
// Not thread safe, method defaults to .GET
var req = try client.request("https://api.github.com/search/topics");
defer req.deinit();
// Set the querystring, can also be set in the URL passed to client.request
// or a mix of setting in client.request and programmatically via req.query
try req.query("q", "zig");
try req.header("Authorization", "Your Token");
// The lifetime of res is tied to req
var res = try req.getResponse(.{});
if (res.status != 200) {
// TODO: handle error
return;
}
// On success, this is a zul.Managed(SearchResult), its lifetime is detached
// from the req, allowing it to outlive req.
const managed = try res.json(SearchResult, allocator, .{});
// Parsing the JSON and creating SearchResult [probably] required some allocations.
// Internally an arena was created to manage this from the allocator passed to
// res.json.
defer managed.deinit();
const search_result = managed.value;
zul.JsonString
Allows the embedding of already-encoded JSON strings into objects in order to avoid double encoded values.
const an_encoded_json_value = "{\"over\": 9000}";
const str = try std.json.stringifyAlloc(allocator, .{
.name = "goku",
.power = zul.jsonString(an_encoded_json_value),
}, .{});
zul.pool
A thread-safe object pool which will dynamically grow when empty and revert to the configured size.
// create a pool for our Expensive class.
// Our Expensive class takes a special initializing context, here an usize which
// we set to 10_000. This is just to pass data from the pool into Expensive.init
var pool = try zul.pool.Growing(Expensive, usize).init(allocator, 10_000, .{.count = 100});
defer pool.deinit();
// acquire will either pick an item from the pool
// if the pool is empty, it'll create a new one (hence, "Growing")
var exp1 = try pool.acquire();
defer pool.release(exp1);
...
// pooled object must have 3 functions
const Expensive = struct {
// an init function
pub fn init(allocator: Allocator, size: usize) !Expensive {
return .{
// ...
};
}
// a deinit method
pub fn deinit(self: *Expensive) void {
// ...
}
// a reset method, called when the item is released back to the pool
pub fn reset(self: *Expensive) void {
// ...
}
};
zul.Scheduler
Ephemeral thread-based task scheduler used to run tasks at a specific time.
// Where multiple types of tasks can be scheduled using the same schedule,
// a tagged union is ideal.
const Task = union(enum) {
say: []const u8,
// Whether T is a tagged union (as here) or another type, a public
// run function must exist
pub fn run(task: Task, ctx: void, at: i64) void {
// the original time the task was scheduled for
_ = at;
// application-specific context that will be passed to each task
_ ctx;
switch (task) {
.say => |msg| {std.debug.print("{s}\n", .{msg}),
}
}
}
...
// This example doesn't use a app-context, so we specify it's
// type as void
var s = zul.Scheduler(Task, void).init(allocator);
defer s.deinit();
// Starts the scheduler, launching a new thread
// We pass our context. Since we have a null context
// we pass a null value, i.e. {}
try s.start({});
// will run the say task in 5 seconds
try s.scheduleIn(.{.say = "world"}, std.time.ms_per_s * 5);
// will run the say task in 100 milliseconds
try s.schedule(.{.say = "hello"}, std.time.milliTimestamp() + 100);
zul.StringBuilder
Efficiently create/concat strings or binary data, optionally using a thread-safe pool with pre-allocated static buffers.
// StringBuilder can be used to efficiently concatenate strings
// But it can also be used to craft binary payloads.
var sb = zul.StringBuilder.init(allocator);
defer sb.deinit();
// We're going to generate a 4-byte length-prefixed message.
// We don't know the length yet, so we'll skip 4 bytes
// We get back a "view" which will let us backfill the length
var view = try sb.skip(4);
// Writes a single byte
try sb.writeByte(10);
// Writes a []const u8
try sb.write("hello");
// Using our view, which points to where the view was taken,
// fill in the length.
view.writeU32Big(@intCast(sb.len() - 4));
std.debug.print("{any}\n", .{sb.string()});
// []u8{0, 0, 0, 6, 10, 'h', 'e', 'l', 'l', 'o'}
zul.testing
Helpers for writing tests.
const t = zul.testing;
test "memcpy" {
// clear's the arena allocator
defer t.reset();
// In addition to exposing std.testing.allocator as zul.testing.allocator
// zul.testing.arena is an ArenaAllocator. An ArenaAllocator can
// make managing test-specific allocations a lot simpler.
// Just stick a `defer zul.testing.reset()` atop your test.
var buf = try t.arena.allocator().alloc(u8, 5);
// unlike std.testing.expectEqual, zul's expectEqual
// will coerce expected to actual's type, so this is valid:
try t.expectEqual(5, buf.len);
@memcpy(buf[0..5], "hello");
// zul's expectEqual also works with strings.
try t.expectEqual("hello", buf);
}
zul.ThreadPool
Lightweight thread pool with back-pressure and zero allocations after initialization.
var tp = try zul.ThreadPool(someTask).init(allocator, .{.count = 4, .backlog = 500});
defer tp.deinit(allocator);
// This will block if the threadpool has 500 pending jobs
// where 500 is the configured backlog
tp.spawn(.{1, true});
fn someTask(i: i32, allow: bool) void {
// process
}
zul.UUID
Parse and generate version 4 and version 7 UUIDs.
// v4() returns a zul.UUID
const uuid1 = zul.UUID.v4();
// toHex() returns a [36]u8
const hex = uuid1.toHex(.lower);
// returns a zul.UUID (or an error)
const uuid2 = try zul.UUID.parse("761e3a9d-4f92-4e0d-9d67-054425c2b5c3");
std.debug.print("{any}\n", uuid1.eql(uuid2));
// create a UUIDv7
const uuid3 = zul.UUID.v7();
// zul.UUID can be JSON serialized
try std.json.stringify(.{.id = uuid3}, .{}, writer);