I’m writing a DBUS library for zig. DBUS allows you to call methods by sending messages over a socket. These methods encode the parameter types in a string signature, i.e. “uus” would be two u32’s and a string.
One goal with this library is to allow the caller to maintain control as its reading data. I don’t want to force the caller to have to allocate memory and read the message into memory. We can achieve this with an API like this:
const a = try msg.readU32(reader);
const b = try msg.readU32(reader);
const string_size = try msg.readStringSize(reader);
// caller can read the string however they like, i.e.
try reader.stream(writer, string_size);
// let the library know we read the string
msg.notifyConsumed(.string);
try msg.finish(); // enforce that we've read the entire message
This kind of API has a disadvantage over one that just reads an entire message into memory because a mistake in the caller’s code won’t be caught until runtime. However, I found a technique to catch this mistake at comptime, check it out:
const sig = "uus";
comptime var sig_index: usize = 0;
try msg.enforceSignature(sig);
const a = try msg.readU32(sig, &sig_index)(reader);
const b = try msg.readU32(sig, &sig_index)(reader);
const string_size = try msg.readStringSize(sig, &sig_index)(reader);
// stream from reader same as before
try reader.stream(writer, string_size);
// let library know we read the string just as before
msg.notifyConsumed(.string);
// enforce we've read the entire message again, but now we'll get a
// comptime error if our code doesn't agree with the signature!
try msg.finish(sig, &sig_index)();
We’ve introduced a signature check and at the same time, enforced that our code agrees with the signature. If you remove one of the reads or add a new one, you’ll get a compile error. Note the lines that are reading parameters now have two sets of parameters:
const a = try msg.readU32(sig, &sig_index)(reader);
^
two sets of parameters
readU32 takes two comptime args which track where we our in the signature. If they disagree then we hit a @compileError, otherwise, we return a function that will take the runtime-known reader and read the value.
I tried just making
readU32take the reader as well but for some reason Zig doesn’t like it when you mix comptime var pointers with runtime-known parameters?
Here’s a full code example to experiment with yourself:
fn ReadFn(comptime signature: []const u8, comptime index: usize) type {
if (index >= signature.len) @compileError("signature has no more types");
return switch (signature[index]) {
'u' => fn (*Reader) error{ ReadFailed, EndOfStream }!u32,
's' => fn (*Reader) error{ ReadFailed, EndOfStream }![]const u8,
else => @compileError("unknown signature char: '" ++ signature[index .. index + 1] ++ "'"),
};
}
// NOTE: this function returns a function that reads the value rather than just reading
// the value itself because Zig doesn't support comptime pointers when they are
// mixed with runtime values.
fn nextReadFn(
comptime signature: []const u8,
comptime signature_index: *usize,
) ReadFn(signature, signature_index.*) {
const start = signature_index.*;
signature_index.* = signature_index.* + 1;
return comptime switch (signature[start]) {
'u' => readU,
's' => readS,
else => unreachable,
};
}
fn finish(comptime signature: []const u8, comptime index: usize) void {
if (index != signature.len) @compileError("the remaining signature has not been read: " ++ signature[index..]);
}
fn readU(r: *Reader) error{ ReadFailed, EndOfStream }!u32 {
return r.takeInt(u32, .big);
}
fn readS(r: *Reader) error{ ReadFailed, EndOfStream }![]const u8 {
return r.take(11);
}
pub fn main() !void {
var r: Reader = .fixed("\x12\x34\x56\x78" ++ "\x9a\xbc\xde\xf0" ++ "hello there");
try example(&r);
}
fn example(r: *Reader) !void {
// This is an example signature that represents two u32 values and a string.
// This API enforces that's always what's read in that order at compile time.
const signature = "uus";
comptime var signature_index: usize = 0;
// If you comment out this read (or the other reads below), then you'll get
// a compile error.
const first_u32: u32 = try nextReadFn(signature, &signature_index)(r);
std.debug.assert(first_u32 == 0x12345678);
const second_u32: u32 = try nextReadFn(signature, &signature_index)(r);
std.debug.assert(second_u32 == 0x9abcdef0);
const string: []const u8 = try nextReadFn(signature, &signature_index)(r);
std.debug.assert(std.mem.eql(u8, string, "hello there"));
finish(signature, signature_index);
}
const std = @import("std");
const Reader = std.Io.Reader;
