/zimpl

Simple comptime generic interfaces for Zig

Primary LanguageZigMIT LicenseMIT

Zimpl Zig interfaces

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. 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

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

// 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

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:

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.

Example

// 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);
}