interfaces#
zig has no interfaces or traits. use comptime patterns instead.
anytype with validation#
accept anytype and validate at comptime:
fn process(db: anytype) !void {
const T = @TypeOf(db);
if (!@hasDecl(T, "exec")) {
@compileError("db must have exec method");
}
try db.exec("SELECT 1", .{});
}
this is the simplest approach - compiler verifies the type has required methods.
type-returning functions#
for generic wrappers, return a type that delegates to an implementation:
pub fn DatabaseInterface(comptime Impl: type) type {
return struct {
impl: Impl,
const Self = @This();
pub fn exec(self: Self, sql: []const u8, args: anytype) !void {
return self.impl.exec(sql, args);
}
};
}
useful when you want a consistent outer API regardless of implementation.
vtables for runtime dispatch#
when you need runtime polymorphism (rare in zig):
pub const Database = struct {
ptr: *anyopaque,
vtable: *const VTable,
const VTable = struct {
exec: *const fn (*anyopaque, []const u8) anyerror!void,
};
pub fn exec(self: Database, sql: []const u8) !void {
return self.vtable.exec(self.ptr, sql);
}
};
this is the std.io.Writer pattern. more boilerplate, but allows swapping implementations at runtime.
comptime-parameterized adapter#
when you need a struct that bridges between a library's callback interface and a user's handler type, return a type parameterized on the handler:
fn WsHandler(comptime H: type) type {
return struct {
allocator: Allocator,
handler: *H,
client_state: *FirehoseClient,
const Self = @This();
pub fn serverMessage(self: *Self, data: []const u8) !void {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
const event = decodeFrame(arena.allocator(), data) catch return;
self.handler.onEvent(event);
}
pub fn close(_: *Self) void {}
};
}
this adapts the websocket library's serverMessage callback to our firehose handler's onEvent interface. the comptime parameter H is the user's handler type — the compiler generates a specialized struct for each handler type used. no vtables, no allocation, full inlining.
see: zat/firehose.zig
practical: module boundary pattern#
for most apps, just separate interface documentation from implementation:
db/
mod.zig -- re-exports, expected methods documented in comments
sqlite.zig -- sqlite implementation
the "interface" is implicit - documented expectations in mod.zig, verified by usage at compile time.