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
.
pub fn Impl(comptime Ifc: fn (type) type, comptime T: type) type { ... }
If T
is a single-item pointer type, define U
to be the child type, i.e.
T = *U
. Otherwise, define U
to be T
.
The function Ifc
must always return a struct type.
If U
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.
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
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),
};
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.
pub fn makeVIfc(
comptime Ifc: fn (type) type,
) fn (CtxAccess, anytype, anytype) VIfc(Ifc) { ... }
The Ifc
function must always return a struct type.
Returns a function to construct a VIfc(Ifc)
vtable interface from a
concrete runtime context and corresponding interface implementation.
The returned function has the the following signature.
fn (
comptime access: CtxAccess,
ctx: anytype,
impl: Impl(Ifc, CtxType(@TypeOf(ctx), access)),
) VIfc(Ifc)
Since vtable interfaces store their
context 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,
};
}
// Function to construct a virtual 'Reader' interface implementation
const makeReader = makeVIfc(Reader);
// 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(makeReader(.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(makeReader(.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(
makeReader(
.Indirect,
&fd,
.{
.read = std.os.read,
.ReadError = std.os.ReadError,
},
),
&buffer,
);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}