Hi all,
Newbie here, I’m currently trying out/ learning zig, so please bear with me. This might have been answered before, but I couldn’t find anything related to my specific scenario.
As an exercise (interfaces, comptime etc.), I’m trying to create small logging facility. There is a single interface LogTarget which can be implemented by various log sinks, e.g. file, stdout, syslog etc. And it looks like this:
/// This is interface representing log target (e.g. file, std etc.)
pub const LogTarget = struct {
impl: *anyopaque,
vtable: *const VTable,
// vtable for dispatching
const VTable = struct {
// should be implemented by log target
op_write: *const fn(*anyopaque, Level, comptime []const u8, args: anytype) anyerror!void,
};
fn init(from: anytype) LogTarget {
const T = @TypeOf(from);
const proxy = struct {
pub fn write(ptr: *anyopaque, lvl: Level, comptime fmt: []const u8, args: anytype) anyerror!void {
try unwrap(ptr, T).write(lvl, fmt, args);
}
};
return .{
.impl = from,
.vtable = &VTable{
.op_write = proxy.write
}
};
}
pub fn write(self: LogTarget, lvl: Level, comptime fmt: []const u8, args: anytype) anyerror!void {
try self.vtable.op_write(self.impl, lvl, fmt, args);
}
};
Now, I have another type Logger which can have multiple log targets and each one of them should invoke write when I try to log something. Here is how Logger type looks like:
pub const Logger = struct {
alloc: Alloc,
targets: std.ArrayList(LogTarget),
pub fn create(alloc: Alloc, name: []const u8) Logger {
return .{
.alloc = alloc,
.targets = .empty,
};
}
// register log target
pub fn register(self: *Logger, target: anytype) !void {
try self.targets.append(
self.alloc,
LogTarget.init(target)
);
}
// log to all registered targets
pub fn log(self: Logger, lvl: Level, comptime fmt: []const u8, args: anytype) !void {
for (self.targets.items) |target| {
try target.write(lvl, fmt, args);
}
}
pub fn deinit(self: *Logger) void {
self.targets.deinit(self.alloc);
}
};
One possible implementations of the LogTarget:
// simple stdout target
pub const Console = struct {
level: Level = .debug,
//
pub fn write(self: *Console, lvl: Level, comptime fmt: []const u8, args: anytype) anyerror!void {
_ = self;
_ = args;
// I would like to be able to use 'args' passed from Logger to construct a message
// and then manipulate further prior to writing to stdout
std.debug.print("[{s}] - {s}\n", .{ lvl.str(), fmt });
}
};
Here is a simple test:
test "logger:noop" {
const allocator = std.testing.allocator;
var logger = Logger.create(allocator, "noop-logger");
defer logger.deinit();
try logger.log(.info, "we shouldn't see this", .{});
}
Problem arises when I change LogTarget interface and add args:anytype argument to the LogTarget.write function. I would like to be able to forward all args from Logger.log to LogTarget.write (implementation), but when I try to do so, compiler yells at me:
[2026-08-05T16:08:27.585Z] Running test: logger.zig - logger:noop
Command failed: /home/civa/.asdf/shims/zig test --test-filter logger:noop /home/civa/dev/projects/zig/demo/src/lib/logger.zig
src/lib/logger.zig:109:7: error: variable of type 'logger.Logger' must be const or comptime
var logger = Logger.create(allocator, "noop-logger");
^~~~~~
src/lib/logger.zig:62:25: note: struct requires comptime because of this field
targets: std.ArrayList(LogTarget),
~~~~~~~~~~~~~^~~~~~~~~~~
/home/civa/.asdf/installs/zig/0.15.1/lib/std/array_list.zig:615:16: note: struct requires comptime because of this field
items: Slice = &[_]T{},
^~~~~
src/lib/logger.zig:31:11: note: struct requires comptime because of this field
vtable: *const VTable,
^~~~~~~~~~~~~
src/lib/logger.zig:34:15: note: struct requires comptime because of this field
op_write: *const fn(*anyopaque, Level, comptime []const u8, args: anytype) anyerror!void,
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/lib/logger.zig:34:15: note: function is generic
Now, I can’t say I fully understand entire compiler error, but I know for sure anytype must be comptime. I cant wrap my head around how to combine comptime with function pointers inside vtable (is it possible?) and should LogTarget interface become comptime entirely?
Sorry for the long post and thanks for the help.
civa