https://github.com/permutationlock/zimpl
Simple comptime generic interfaces for Zig
https://github.com/permutationlock/zimpl
generics interfaces polymorphism traits vtables zig
Last synced: 21 days ago
JSON representation
Simple comptime generic interfaces for Zig
- Host: GitHub
- URL: https://github.com/permutationlock/zimpl
- Owner: permutationlock
- License: mit
- Created: 2023-11-13T12:08:52.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-03-22T21:35:02.000Z (about 1 month ago)
- Last Synced: 2025-03-22T22:31:36.403Z (about 1 month ago)
- Topics: generics, interfaces, polymorphism, traits, vtables, zig
- Language: Zig
- Homepage:
- Size: 201 KB
- Stars: 35
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.MIT
Awesome Lists containing this project
README
# Zimpl Zig interfaces
A dead simple implementation of [static dispatch][2] interfaces in Zig
that emerged from a tiny subset of [ztrait][1]. See [here][3]
for some motivation.Also included is a compatible implementation of [dynamic dispatch][4]
interfaces via `comptime` generated [vtables][5]. Inspired by
[`interface.zig`][6].*Warning: Zimpl is still mostly an exploratory project.*
## Static dispatch
### `Impl`
```Zig
pub fn Impl(comptime Ifc: fn (type) type, comptime T: type) type { ... }
```### Definitions
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`.### Arguments
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).### Return value
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.### Example
```Zig
// 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);
}
```## Dynamic dispatch
### `VIfc`
```Zig
pub fn VIfc(comptime Ifc: fn (type) type) type { ... }
```
### ArgumentsThe `Ifc` function must always return a struct type.
### Return value
Returns a struct of the following form:
```Zig
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.```Zig
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.### Example
```Zig
// 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);
}
```[1]: https://github.com/permutationlock/ztrait
[2]: https://en.wikipedia.org/wiki/Static_dispatch
[3]: https://github.com/permutationlock/zimpl/blob/main/why.md
[4]: https://en.wikipedia.org/wiki/Dynamic_dispatch
[5]: https://en.wikipedia.org/wiki/Virtual_method_table
[6]: https://github.com/alexnask/interface.zig