A dead simple implementation of static dispatch interfaces in Zig that emerged from a tiny subset of ztrait. See here for some motivation.
Also included is a compatible implementation of dynamic dispatch
interfaces via comptime
generated vtables. Inspired by
interface.zig
.
Warning: Zimpl is still mostly an exploratory project.
pub fn Impl(comptime Ifc: fn (type) type, comptime T: type) type { ... }
If T
is a single-item pointer type, then define U(T)
to be the child type,
i.e. T = *U(T)
, otherwise define U(T)=T
.
The function Ifc
must always return a struct type.
If U(T)
has a declaration matching the name of a field from
Ifc(T)
that cannot coerce to the type of that field, then a
compile error will occur (and a pretty good one now, thank you Zig
Core Team).
The type Impl(Ifc, T)
is a struct type with the same fields
as Ifc(T)
, but with the default value of each field set equal to
the declaration of U(T)
of the same name, if such a declaration
exists.
// An interface
pub fn Reader(comptime T: type) type {
return struct {
ReadError: type = anyerror,
read: fn (reader_ctx: T, buffer: []u8) anyerror!usize,
};
}
// A collection of functions using the interface
pub const io = struct {
pub inline fn read(
reader_ctx: anytype,
reader_impl: Impl(Reader, @TypeOf(reader_ctx)),
buffer: []u8,
) reader_impl.ReadError!usize {
return @errorCast(reader_impl.read(reader_ctx, buffer));
}
pub inline fn readAll(
reader_ctx: anytype,
reader_impl: Impl(Reader, @TypeOf(reader_ctx)),
buffer: []u8,
) reader_impl.ReadError!usize {
return readAtLeast(reader_ctx, reader_impl, buffer, buffer.len);
}
pub inline fn readAtLeast(
reader_ctx: anytype,
reader_impl: Impl(Reader, @TypeOf(reader_ctx)),
buffer: []u8,
len: usize,
) reader_impl.ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try read(reader_ctx, reader_impl, buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
};
test "define and use a reader" {
const FixedBufferReader = struct {
buffer: []const u8,
pos: usize = 0,
pub const ReadError = error{};
pub fn read(self: *@This(), out_buffer: []u8) ReadError!usize {
const len = @min(self.buffer[self.pos..].len, out_buffer.len);
@memcpy(out_buffer[0..len], self.buffer[self.pos..][0..len]);
self.pos += len;
return len;
}
};
const in_buf: []const u8 = "I really hope that this works!";
var reader = FixedBufferReader{ .buffer = in_buf };
var out_buf: [16]u8 = undefined;
const len = try io.readAll(&reader, .{}, &out_buf);
try testing.expectEqualStrings(in_buf[0..len], out_buf[0..len]);
}
test "use std.fs.File as a reader" {
var buffer: [19]u8 = undefined;
var file = try std.fs.cwd().openFile("my_file.txt", .{});
try io.readAll(file, .{}, &buffer);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}
test "use std.os.fd_t as a reader via an explicitly defined interface" {
var buffer: [19]u8 = undefined;
const fd = try std.os.open("my_file.txt", std.os.O.RDONLY, 0);
try io.readAll(
fd,
.{ .read = std.os.read, .ReadError = std.os.ReadError, },
&buffer,
);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}
pub fn VIfc(comptime Ifc: fn (type) type) type { ... }
The Ifc
function must always return a struct type.
Returns a struct of the following form:
struct {
ctx: *anyopaque,
vtable: VTable(Ifc),
pub fn init(
comptime access: CtxAccess,
ctx: anytype,
impl: Impl(Ifc, CtxType(@TypeOf(ctx), access)),
) @This() {
return .{
.ctx = if (access == .indirect) @constCast(ctx) else ctx,
.vtable = vtable(Ifc, access, @TypeOf(ctx), impl),
};
}
};
The struct type VTable(Ifc)
contains one field for each field of
Ifc(*anyopaque)
that is a (optional) function. The type
of each vtable field is converted to a (optional) function pointer
with the same signature.
The init
function constructs a virtual interface from a given
runtime context and interface implementation. Since the
context is stored as a type-erased pointer, the access
parameter is provided
to allow vtables to be constructed for implementations that rely on
non-pointer contexts.
pub const CtxAccess = enum { direct, indirect };
fn CtxType(comptime Ctx: type, comptime access: CtxAccess) type {
return if (access == .indirect) @typeInfo(Ctx).Pointer.child else Ctx;
}
If access
is .direct
, then the type-erased ctx
pointer stored
in VIfc(Ifc)
is cast as the correct pointer type and passed directly to
concrete member function implementations.
Otherwise, if access
is .indirect
, ctx
is a pointer to the actual
context, and it is dereferenced and passed by value to member
functions.
// An interface
pub fn Reader(comptime T: type) type {
return struct {
// non-function fields are fine, but vtable interfaces ignore them
ReadError: type = anyerror,
read: fn (reader_ctx: T, buffer: []u8) anyerror!usize,
};
}
// A collection of functions using virtual 'Reader' interfaces
pub const vio = struct {
pub inline fn read(reader: VIfc(Reader), buffer: []u8) anyerror!usize {
return reader.vtable.read(reader.ctx, buffer);
}
pub inline fn readAll(reader: VIfc(Reader), buffer: []u8) anyerror!usize {
return readAtLeast(reader, buffer, buffer.len);
}
pub fn readAtLeast(
reader: VIfc(Reader),
buffer: []u8,
len: usize,
) anyerror!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try read(reader, buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
};
test "define and use a reader" {
const FixedBufferReader = struct {
buffer: []const u8,
pos: usize = 0,
pub const ReadError = error{};
pub fn read(self: *@This(), out_buffer: []u8) ReadError!usize {
const len = @min(self.buffer[self.pos..].len, out_buffer.len);
@memcpy(out_buffer[0..len], self.buffer[self.pos..][0..len]);
self.pos += len;
return len;
}
};
const in_buf: []const u8 = "I really hope that this works!";
var reader = FixedBufferReader{ .buffer = in_buf };
var out_buf: [16]u8 = undefined;
const len = try vio.readAll(Reader.init(.direct, &reader, .{}), &out_buf);
try testing.expectEqualStrings(in_buf[0..len], out_buf[0..len]);
}
test "use std.fs.File as a reader" {
var buffer: [19]u8 = undefined;
var file = try std.fs.cwd().openFile("my_file.txt", .{});
try vio.readAll(Reader.init(.indirect, &file, .{}), &buffer);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}
test "use std.os.fd_t as a reader via an explicitly defined interface" {
var buffer: [19]u8 = undefined;
const fd = try std.os.open("my_file.txt", std.os.O.RDONLY, 0);
try vio.readAll(
Reader.init(
.indirect,
&fd,
.{ .read = std.os.read, .ReadError = std.os.ReadError },
),
&buffer,
);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}