Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/permutationlock/zimpl

Simple comptime generic interfaces for Zig
https://github.com/permutationlock/zimpl

generics interfaces polymorphism traits vtables zig

Last synced: 2 days ago
JSON representation

Simple comptime generic interfaces for Zig

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. Although
it has some great properties, there are still problems that should be
addressed, most notably the fact that error messages can be quite bad
when types don't match interfaces; a huge problem since that is the
entire point of the library!*

## 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 define `U` to be the child type, i.e.
`T = *U`, otherwise define `U=T`.

### Arguments

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.

### 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` 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 { ... }
```
### Arguments

The `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