├── .gitignore ├── LICENSE.MIT ├── README.md ├── benchmarks └── buffered_io.zig ├── build.zig ├── build.zig.zon ├── examples ├── count.zig ├── io.zig ├── io │ ├── FixedBufferReader.zig │ ├── FixedBufferStream.zig │ ├── buffered_reader.zig │ ├── buffered_writer.zig │ └── counting_writer.zig ├── iterator.zig ├── read_file.zig ├── read_file │ └── test.txt ├── vcount.zig ├── vcount2.zig └── vio.zig ├── src ├── zimpl.zig └── ztable.zig └── why.md /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | zig-cache/ 3 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Daniel Aven Bross 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zimpl Zig interfaces 2 | 3 | A dead simple implementation of [static dispatch][2] interfaces in Zig 4 | that emerged from a tiny subset of [ztrait][1]. See [here][3] 5 | for some motivation. 6 | 7 | Also included is a compatible implementation of [dynamic dispatch][4] 8 | interfaces via `comptime` generated [vtables][5]. Inspired by 9 | [`interface.zig`][6]. 10 | 11 | *Warning: Zimpl is still mostly an exploratory project.* 12 | 13 | ## Static dispatch 14 | 15 | ### `Impl` 16 | 17 | ```Zig 18 | pub fn Impl(comptime Ifc: fn (type) type, comptime T: type) type { ... } 19 | ``` 20 | 21 | ### Definitions 22 | 23 | If `T` is a single-item pointer type, then define `U(T)` to be the child type, 24 | i.e. `T = *U(T)`, otherwise define `U(T)=T`. 25 | 26 | ### Arguments 27 | 28 | The function `Ifc` must always return a struct type. 29 | If `U(T)` has a declaration matching the name of a field from 30 | `Ifc(T)` that cannot coerce to the type of that field, then a 31 | compile error will occur (and a pretty good one now, thank you Zig 32 | Core Team). 33 | 34 | ### Return value 35 | 36 | The type `Impl(Ifc, T)` is a struct type with the same fields 37 | as `Ifc(T)`, but with the default value of each field set equal to 38 | the declaration of `U(T)` of the same name, if such a declaration 39 | exists. 40 | 41 | ### Example 42 | 43 | ```Zig 44 | // An interface 45 | pub fn Reader(comptime T: type) type { 46 | return struct { 47 | ReadError: type = anyerror, 48 | read: fn (reader_ctx: T, buffer: []u8) anyerror!usize, 49 | }; 50 | } 51 | 52 | // A collection of functions using the interface 53 | pub const io = struct { 54 | pub inline fn read( 55 | reader_ctx: anytype, 56 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 57 | buffer: []u8, 58 | ) reader_impl.ReadError!usize { 59 | return @errorCast(reader_impl.read(reader_ctx, buffer)); 60 | } 61 | 62 | pub inline fn readAll( 63 | reader_ctx: anytype, 64 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 65 | buffer: []u8, 66 | ) reader_impl.ReadError!usize { 67 | return readAtLeast(reader_ctx, reader_impl, buffer, buffer.len); 68 | } 69 | 70 | pub inline fn readAtLeast( 71 | reader_ctx: anytype, 72 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 73 | buffer: []u8, 74 | len: usize, 75 | ) reader_impl.ReadError!usize { 76 | assert(len <= buffer.len); 77 | var index: usize = 0; 78 | while (index < len) { 79 | const amt = try read(reader_ctx, reader_impl, buffer[index..]); 80 | if (amt == 0) break; 81 | index += amt; 82 | } 83 | return index; 84 | } 85 | }; 86 | 87 | test "define and use a reader" { 88 | const FixedBufferReader = struct { 89 | buffer: []const u8, 90 | pos: usize = 0, 91 | 92 | pub const ReadError = error{}; 93 | 94 | pub fn read(self: *@This(), out_buffer: []u8) ReadError!usize { 95 | const len = @min(self.buffer[self.pos..].len, out_buffer.len); 96 | @memcpy(out_buffer[0..len], self.buffer[self.pos..][0..len]); 97 | self.pos += len; 98 | return len; 99 | } 100 | }; 101 | const in_buf: []const u8 = "I really hope that this works!"; 102 | var reader = FixedBufferReader{ .buffer = in_buf }; 103 | 104 | var out_buf: [16]u8 = undefined; 105 | const len = try io.readAll(&reader, .{}, &out_buf); 106 | 107 | try testing.expectEqualStrings(in_buf[0..len], out_buf[0..len]); 108 | } 109 | 110 | test "use std.fs.File as a reader" { 111 | var buffer: [19]u8 = undefined; 112 | var file = try std.fs.cwd().openFile("my_file.txt", .{}); 113 | try io.readAll(file, .{}, &buffer); 114 | 115 | try std.testing.expectEqualStrings("Hello, I am a file!", &buffer); 116 | } 117 | 118 | test "use std.os.fd_t as a reader via an explicitly defined interface" { 119 | var buffer: [19]u8 = undefined; 120 | const fd = try std.os.open("my_file.txt", std.os.O.RDONLY, 0); 121 | try io.readAll( 122 | fd, 123 | .{ .read = std.os.read, .ReadError = std.os.ReadError, }, 124 | &buffer, 125 | ); 126 | 127 | try std.testing.expectEqualStrings("Hello, I am a file!", &buffer); 128 | } 129 | ``` 130 | 131 | ## Dynamic dispatch 132 | 133 | ### `VIfc` 134 | 135 | ```Zig 136 | pub fn VIfc(comptime Ifc: fn (type) type) type { ... } 137 | ``` 138 | ### Arguments 139 | 140 | The `Ifc` function must always return a struct type. 141 | 142 | ### Return value 143 | 144 | Returns a struct of the following form: 145 | ```Zig 146 | struct { 147 | ctx: *anyopaque, 148 | vtable: VTable(Ifc), 149 | 150 | pub fn init( 151 | comptime access: CtxAccess, 152 | ctx: anytype, 153 | impl: Impl(Ifc, CtxType(@TypeOf(ctx), access)), 154 | ) @This() { 155 | return .{ 156 | .ctx = if (access == .indirect) @constCast(ctx) else ctx, 157 | .vtable = vtable(Ifc, access, @TypeOf(ctx), impl), 158 | }; 159 | } 160 | }; 161 | ``` 162 | The struct type `VTable(Ifc)` contains one field for each field of 163 | `Ifc(*anyopaque)` that is a (optional) function. The type 164 | of each vtable field is converted to a (optional) function pointer 165 | with the same signature. 166 | 167 | The `init` function constructs a virtual interface from a given 168 | runtime context and interface implementation. Since the 169 | context is stored as a type-erased pointer, the `access` parameter is provided 170 | to allow vtables to be constructed for implementations that rely on 171 | non-pointer contexts. 172 | 173 | ```Zig 174 | pub const CtxAccess = enum { direct, indirect }; 175 | 176 | fn CtxType(comptime Ctx: type, comptime access: CtxAccess) type { 177 | return if (access == .indirect) @typeInfo(Ctx).Pointer.child else Ctx; 178 | } 179 | ``` 180 | 181 | If `access` is `.direct`, then the type-erased `ctx` pointer stored 182 | in `VIfc(Ifc)` is cast as the correct pointer type and passed directly to 183 | concrete member function implementations. 184 | 185 | Otherwise, if `access` is `.indirect`, `ctx` is a pointer to the actual 186 | context, and it is dereferenced and passed by value to member 187 | functions. 188 | 189 | ### Example 190 | 191 | ```Zig 192 | // An interface 193 | pub fn Reader(comptime T: type) type { 194 | return struct { 195 | // non-function fields are fine, but vtable interfaces ignore them 196 | ReadError: type = anyerror, 197 | read: fn (reader_ctx: T, buffer: []u8) anyerror!usize, 198 | }; 199 | } 200 | 201 | // A collection of functions using virtual 'Reader' interfaces 202 | pub const vio = struct { 203 | pub inline fn read(reader: VIfc(Reader), buffer: []u8) anyerror!usize { 204 | return reader.vtable.read(reader.ctx, buffer); 205 | } 206 | 207 | pub inline fn readAll(reader: VIfc(Reader), buffer: []u8) anyerror!usize { 208 | return readAtLeast(reader, buffer, buffer.len); 209 | } 210 | 211 | pub fn readAtLeast( 212 | reader: VIfc(Reader), 213 | buffer: []u8, 214 | len: usize, 215 | ) anyerror!usize { 216 | assert(len <= buffer.len); 217 | var index: usize = 0; 218 | while (index < len) { 219 | const amt = try read(reader, buffer[index..]); 220 | if (amt == 0) break; 221 | index += amt; 222 | } 223 | return index; 224 | } 225 | }; 226 | 227 | test "define and use a reader" { 228 | const FixedBufferReader = struct { 229 | buffer: []const u8, 230 | pos: usize = 0, 231 | 232 | pub const ReadError = error{}; 233 | 234 | pub fn read(self: *@This(), out_buffer: []u8) ReadError!usize { 235 | const len = @min(self.buffer[self.pos..].len, out_buffer.len); 236 | @memcpy(out_buffer[0..len], self.buffer[self.pos..][0..len]); 237 | self.pos += len; 238 | return len; 239 | } 240 | }; 241 | const in_buf: []const u8 = "I really hope that this works!"; 242 | var reader = FixedBufferReader{ .buffer = in_buf }; 243 | 244 | var out_buf: [16]u8 = undefined; 245 | const len = try vio.readAll(Reader.init(.direct, &reader, .{}), &out_buf); 246 | 247 | try testing.expectEqualStrings(in_buf[0..len], out_buf[0..len]); 248 | } 249 | 250 | test "use std.fs.File as a reader" { 251 | var buffer: [19]u8 = undefined; 252 | var file = try std.fs.cwd().openFile("my_file.txt", .{}); 253 | try vio.readAll(Reader.init(.indirect, &file, .{}), &buffer); 254 | 255 | try std.testing.expectEqualStrings("Hello, I am a file!", &buffer); 256 | } 257 | 258 | test "use std.os.fd_t as a reader via an explicitly defined interface" { 259 | var buffer: [19]u8 = undefined; 260 | const fd = try std.os.open("my_file.txt", std.os.O.RDONLY, 0); 261 | try vio.readAll( 262 | Reader.init( 263 | .indirect, 264 | &fd, 265 | .{ .read = std.os.read, .ReadError = std.os.ReadError }, 266 | ), 267 | &buffer, 268 | ); 269 | 270 | try std.testing.expectEqualStrings("Hello, I am a file!", &buffer); 271 | } 272 | ``` 273 | 274 | [1]: https://github.com/permutationlock/ztrait 275 | [2]: https://en.wikipedia.org/wiki/Static_dispatch 276 | [3]: https://github.com/permutationlock/zimpl/blob/main/why.md 277 | [4]: https://en.wikipedia.org/wiki/Dynamic_dispatch 278 | [5]: https://en.wikipedia.org/wiki/Virtual_method_table 279 | [6]: https://github.com/alexnask/interface.zig 280 | -------------------------------------------------------------------------------- /benchmarks/buffered_io.zig: -------------------------------------------------------------------------------- 1 | // Wrote this with inspiration from a Karl Seguin article, 2 | // it's just playing around, not to be taken too seriously. 3 | 4 | const std = @import("std"); 5 | const io = @import("io"); 6 | const vio = io.vio; 7 | 8 | const LOOPS = 1000; 9 | 10 | pub fn main() !void { 11 | var in: [100000]u8 = undefined; 12 | try std.posix.getrandom(&in); 13 | 14 | { 15 | // time buffered generic stream 16 | var fbr = io.FixedBufferReader{ .buffer = &in }; 17 | var out: [10000]u8 = undefined; 18 | var out_stream = io.FixedBufferStream{ .buffer = &out }; 19 | 20 | var found: usize = 0; 21 | var bytes: usize = 0; 22 | 23 | asm volatile ("" ::: "memory"); 24 | var timer = try std.time.Timer.start(); 25 | asm volatile ("" ::: "memory"); 26 | 27 | for (0..LOOPS) |_| { 28 | fbr.pos = 0; 29 | 30 | while (true) { 31 | io.streamUntilDelimiter( 32 | &fbr, 33 | .{}, 34 | &out_stream, 35 | .{}, 36 | '\n', 37 | out.len, 38 | ) catch |err| switch (err) { 39 | error.EndOfStream => break, 40 | else => return err, 41 | }; 42 | 43 | found += 1; 44 | bytes += out_stream.getWritten().len; 45 | out_stream.pos = 0; 46 | } 47 | } 48 | 49 | asm volatile ("" ::: "memory"); 50 | const elapsed = timer.lap(); 51 | asm volatile ("" ::: "memory"); 52 | 53 | std.debug.print("buffered zimpl io\n", .{}); 54 | std.debug.print( 55 | "Took: {d}us ({d}ns / iteration) {d} entries, {d} bytes\n", 56 | .{ elapsed / 1000, elapsed / LOOPS, found, bytes }, 57 | ); 58 | } 59 | 60 | { 61 | // time unbuffered generic stream 62 | var fbr = io.FixedBufferReader{ .buffer = &in }; 63 | var out: [10000]u8 = undefined; 64 | var out_stream = io.FixedBufferStream{ .buffer = &out }; 65 | var found: usize = 0; 66 | var bytes: usize = 0; 67 | 68 | asm volatile ("" ::: "memory"); 69 | var timer = try std.time.Timer.start(); 70 | asm volatile ("" ::: "memory"); 71 | 72 | for (0..LOOPS) |_| { 73 | fbr.pos = 0; 74 | 75 | while (true) { 76 | // use as unbuffered reader by setting 'readBuffer = null' 77 | io.streamUntilDelimiter( 78 | &fbr, 79 | .{ .readBuffer = null }, 80 | &out_stream, 81 | .{}, 82 | '\n', 83 | out.len, 84 | ) catch |err| switch (err) { 85 | error.EndOfStream => break, 86 | else => return err, 87 | }; 88 | 89 | found += 1; 90 | bytes += out_stream.getWritten().len; 91 | out_stream.pos = 0; 92 | } 93 | } 94 | 95 | asm volatile ("" ::: "memory"); 96 | const elapsed = timer.lap(); 97 | asm volatile ("" ::: "memory"); 98 | 99 | std.debug.print("unbuffered zimpl io\n", .{}); 100 | std.debug.print( 101 | "Took: {d}us ({d}ns / iteration) {d} entries, {d} bytes\n", 102 | .{ elapsed / 1000, elapsed / LOOPS, found, bytes }, 103 | ); 104 | } 105 | 106 | { 107 | // time virtual buffered generic stream 108 | var fbr = io.FixedBufferReader{ .buffer = &in }; 109 | var out: [10000]u8 = undefined; 110 | var out_stream = io.FixedBufferStream{ .buffer = &out }; 111 | 112 | const reader = vio.Reader.init(.direct, &fbr, .{}); 113 | const writer = vio.Writer.init(.direct, &out_stream, .{}); 114 | 115 | var found: usize = 0; 116 | var bytes: usize = 0; 117 | 118 | asm volatile ("" ::: "memory"); 119 | var timer = try std.time.Timer.start(); 120 | asm volatile ("" ::: "memory"); 121 | 122 | for (0..LOOPS) |_| { 123 | fbr.pos = 0; 124 | 125 | while (true) { 126 | vio.streamUntilDelimiter( 127 | reader, 128 | writer, 129 | '\n', 130 | out.len, 131 | ) catch |err| switch (err) { 132 | error.EndOfStream => break, 133 | else => return err, 134 | }; 135 | 136 | found += 1; 137 | bytes += out_stream.getWritten().len; 138 | out_stream.pos = 0; 139 | } 140 | } 141 | 142 | asm volatile ("" ::: "memory"); 143 | const elapsed = timer.lap(); 144 | asm volatile ("" ::: "memory"); 145 | 146 | std.debug.print("buffered zimpl vio\n", .{}); 147 | std.debug.print( 148 | "Took: {d}us ({d}ns / iteration) {d} entries, {d} bytes\n", 149 | .{ elapsed / 1000, elapsed / LOOPS, found, bytes }, 150 | ); 151 | } 152 | 153 | { 154 | // time virtual unbuffered generic stream 155 | var fbr = io.FixedBufferReader{ .buffer = &in }; 156 | var out: [10000]u8 = undefined; 157 | var out_stream = io.FixedBufferStream{ .buffer = &out }; 158 | 159 | const reader = vio.Reader.init(.direct, &fbr, .{ .readBuffer = null }); 160 | const writer = vio.Writer.init(.direct, &out_stream, .{}); 161 | 162 | var found: usize = 0; 163 | var bytes: usize = 0; 164 | 165 | asm volatile ("" ::: "memory"); 166 | var timer = try std.time.Timer.start(); 167 | asm volatile ("" ::: "memory"); 168 | 169 | for (0..LOOPS) |_| { 170 | fbr.pos = 0; 171 | 172 | while (true) { 173 | vio.streamUntilDelimiter( 174 | reader, 175 | writer, 176 | '\n', 177 | out.len, 178 | ) catch |err| switch (err) { 179 | error.EndOfStream => break, 180 | else => return err, 181 | }; 182 | 183 | found += 1; 184 | bytes += out_stream.getWritten().len; 185 | out_stream.pos = 0; 186 | } 187 | } 188 | 189 | asm volatile ("" ::: "memory"); 190 | const elapsed = timer.lap(); 191 | asm volatile ("" ::: "memory"); 192 | 193 | std.debug.print("unbuffered zimpl vio\n", .{}); 194 | std.debug.print( 195 | "Took: {d}us ({d}ns / iteration) {d} entries, {d} bytes\n", 196 | .{ elapsed / 1000, elapsed / LOOPS, found, bytes }, 197 | ); 198 | } 199 | 200 | { 201 | // time std.io stream 202 | var found: usize = 0; 203 | var bytes: usize = 0; 204 | var fbr = std.io.fixedBufferStream(&in); 205 | var out: [10000]u8 = undefined; 206 | var out_stream = std.io.fixedBufferStream(&out); 207 | 208 | const reader = fbr.reader(); 209 | const anyreader = reader.any(); 210 | const writer = out_stream.writer(); 211 | 212 | asm volatile ("" ::: "memory"); 213 | var timer = try std.time.Timer.start(); 214 | asm volatile ("" ::: "memory"); 215 | 216 | for (0..LOOPS) |_| { 217 | fbr.pos = 0; 218 | 219 | while (true) { 220 | anyreader.streamUntilDelimiter( 221 | writer, 222 | '\n', 223 | out.len, 224 | ) catch |err| switch (err) { 225 | error.EndOfStream => break, 226 | else => return err, 227 | }; 228 | 229 | found += 1; 230 | bytes += out_stream.getWritten().len; 231 | out_stream.pos = 0; 232 | } 233 | } 234 | 235 | asm volatile ("" ::: "memory"); 236 | const elapsed = timer.lap(); 237 | asm volatile ("" ::: "memory"); 238 | 239 | std.debug.print("std.io fixedBufferStream\n", .{}); 240 | std.debug.print( 241 | "Took: {d}us ({d}ns / iteration) {d} entries, {d} bytes\n", 242 | .{ elapsed / 1000, elapsed / LOOPS, found, bytes }, 243 | ); 244 | } 245 | 246 | //{ 247 | // // time std.io stream 248 | // var found: usize = 0; 249 | // var bytes: usize = 0; 250 | // var fbr = std.io.fixedBufferStream(&in); 251 | // var out: [10000]u8 = undefined; 252 | // var out_stream = std.io.fixedBufferStream(&out); 253 | 254 | // var bfbr = std.io.bufferedReader(fbr.reader()); 255 | // var boutstream = std.io.bufferedWriter(out_stream.writer()); 256 | 257 | // const reader = bfbr.reader(); 258 | // const anyreader = reader.any(); 259 | // const writer = boutstream.writer(); 260 | 261 | // var timer = try std.time.Timer.start(); 262 | 263 | // for (0..LOOPS) |_| { 264 | // fbr.pos = 0; 265 | 266 | // while (true) { 267 | // anyreader.streamUntilDelimiter( 268 | // writer, 269 | // '\n', 270 | // out.len, 271 | // ) catch |err| switch (err) { 272 | // error.EndOfStream => break, 273 | // else => return err, 274 | // }; 275 | 276 | // try boutstream.flush(); 277 | // found += 1; 278 | // bytes += out_stream.getWritten().len; 279 | // out_stream.pos = 0; 280 | // } 281 | // } 282 | // const elapsed = timer.lap(); 283 | // std.debug.print("buffered std.io fixedBufferStream\n", .{}); 284 | // std.debug.print( 285 | // "Took: {d}us ({d}ns / iteration) {d} entries, {d} bytes\n", 286 | // .{ elapsed / 1000, elapsed / LOOPS, found, bytes }, 287 | // ); 288 | //} 289 | } 290 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Build = std.Build; 3 | const Import = Build.Module.Import; 4 | 5 | const BuildFile = struct { 6 | name: []const u8, 7 | path: []const u8, 8 | imports: []const Import = &.{}, 9 | }; 10 | 11 | pub fn build(b: *Build) !void { 12 | const target = b.standardTargetOptions(.{}); 13 | const optimize = b.standardOptimizeOption(.{}); 14 | 15 | const zimpl = b.addModule("zimpl", .{ 16 | .root_source_file = .{ 17 | .src_path = .{ .owner = b, .sub_path = "src/zimpl.zig" }, 18 | }, 19 | }); 20 | 21 | const examples = [_]BuildFile{ 22 | .{ .name = "count", .path = "examples/count.zig" }, 23 | .{ .name = "iterator", .path = "examples/iterator.zig" }, 24 | .{ .name = "io", .path = "examples/io.zig" }, 25 | .{ .name = "read_file", .path = "examples/read_file.zig" }, 26 | .{ .name = "vcount", .path = "examples/vcount.zig" }, 27 | .{ .name = "vcount2", .path = "examples/vcount2.zig" }, 28 | }; 29 | 30 | const test_step = b.step("test", &.{}); 31 | 32 | inline for (examples) |example| { 33 | const ex_test = b.addTest(.{ 34 | .name = example.name, 35 | .root_source_file = .{ 36 | .src_path = .{ .owner = b, .sub_path = example.path }, 37 | }, 38 | .target = target, 39 | .optimize = optimize, 40 | }); 41 | ex_test.root_module.addImport("zimpl", zimpl); 42 | for (example.imports) |dep| { 43 | ex_test.root_moduel.addImport(dep.name, dep.module); 44 | } 45 | const run = b.addRunArtifact(ex_test); 46 | test_step.dependOn(&run.step); 47 | } 48 | 49 | const tlib = b.addStaticLibrary(.{ 50 | .name = "zimpl", 51 | .root_source_file = .{ 52 | .src_path = .{ .owner = b, .sub_path = "src/zimpl.zig" }, 53 | }, 54 | .target = target, 55 | .optimize = optimize, 56 | }); 57 | const docs_step = b.step("docs", "Emit docs"); 58 | const docs_install = b.addInstallDirectory(.{ 59 | .install_dir = .prefix, 60 | .install_subdir = "docs", 61 | .source_dir = tlib.getEmittedDocs(), 62 | }); 63 | docs_step.dependOn(&docs_install.step); 64 | 65 | const io = b.addModule("io", .{ 66 | .root_source_file = .{ 67 | .src_path = .{ .owner = b, .sub_path = "examples/io.zig" }, 68 | }, 69 | .imports = &.{.{ .name = "zimpl", .module = zimpl }}, 70 | }); 71 | 72 | const benchmarks = [_]BuildFile{.{ 73 | .name = "buffered_io", 74 | .path = "benchmarks/buffered_io.zig", 75 | .imports = &.{.{ .name = "io", .module = io }}, 76 | }}; 77 | 78 | const benchmark_step = b.step("benchmark", &.{}); 79 | 80 | inline for (benchmarks) |benchmark| { 81 | const bench = b.addExecutable(.{ 82 | .name = benchmark.name, 83 | .root_source_file = .{ 84 | .src_path = .{ .owner = b, .sub_path = benchmark.path }, 85 | }, 86 | .target = target, 87 | .optimize = .ReleaseFast, 88 | }); 89 | for (benchmark.imports) |dep| { 90 | bench.root_module.addImport(dep.name, dep.module); 91 | } 92 | const install = b.addInstallArtifact(bench, .{}); 93 | benchmark_step.dependOn(&install.step); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zimpl, 3 | .version = "1.1.0", 4 | .fingerprint = 0x185abb0c49b12026, 5 | .paths = .{ "build.zig", "src/zimpl.zig" }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/count.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const Impl = @import("zimpl").Impl; 5 | 6 | pub fn Counter(comptime T: type) type { 7 | return struct { 8 | increment: fn (T) void, 9 | read: fn (T) usize, 10 | }; 11 | } 12 | 13 | pub fn countToTen( 14 | ctr_ctx: anytype, 15 | ctr_impl: Impl(Counter, @TypeOf(ctr_ctx)), 16 | ) void { 17 | while (ctr_impl.read(ctr_ctx) < 10) { 18 | ctr_impl.increment(ctr_ctx); 19 | } 20 | } 21 | 22 | test "explicit implementation" { 23 | const USize = struct { 24 | pub fn inc(i: *usize) void { 25 | i.* += 1; 26 | } 27 | pub fn deref(i: *const usize) usize { 28 | return i.*; 29 | } 30 | }; 31 | var count: usize = 0; 32 | countToTen(&count, .{ .increment = USize.inc, .read = USize.deref }); 33 | try testing.expectEqual(@as(usize, 10), count); 34 | } 35 | 36 | const MyCounter = struct { 37 | count: usize, 38 | 39 | pub fn increment(self: *@This()) void { 40 | self.count += 1; 41 | } 42 | 43 | pub fn read(self: *const @This()) usize { 44 | return self.count; 45 | } 46 | }; 47 | 48 | test "infer implementation" { 49 | var counter: MyCounter = .{ .count = 0 }; 50 | countToTen(&counter, .{}); 51 | try testing.expectEqual(@as(usize, 10), counter.count); 52 | } 53 | 54 | fn otherInc(self: *MyCounter) void { 55 | self.count = 1 + self.count * 2; 56 | } 57 | 58 | test "override implementation" { 59 | var counter: MyCounter = .{ .count = 0 }; 60 | countToTen(&counter, .{ .increment = otherInc }); 61 | try testing.expectEqual(@as(usize, 15), counter.count); 62 | } 63 | -------------------------------------------------------------------------------- /examples/io.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const native_endian = @import("builtin").target.cpu.arch.endian(); 3 | const mem = std.mem; 4 | const assert = std.debug.assert; 5 | 6 | const Impl = @import("zimpl").Impl; 7 | 8 | pub const vio = @import("vio.zig"); 9 | 10 | pub const FixedBufferReader = @import("io/FixedBufferReader.zig"); 11 | pub const FixedBufferStream = @import("io/FixedBufferStream.zig"); 12 | pub const CountingWriter = @import("io/counting_writer.zig").CountingWriter; 13 | pub const countingWriter = @import("io/counting_writer.zig").countingWriter; 14 | pub const BufferedReader = @import("io/buffered_reader.zig").BufferedReader; 15 | pub const bufferedReader = @import("io/buffered_reader.zig").bufferedReader; 16 | pub const BufferedWriter = @import("io/buffered_writer.zig").BufferedWriter; 17 | pub const bufferedWriter = @import("io/buffered_writer.zig").bufferedWriter; 18 | 19 | pub const null_writer = NullWriter{}; 20 | 21 | pub const NullWriter = struct { 22 | pub const WriteError = error{}; 23 | pub fn write(_: NullWriter, data: []const u8) WriteError!usize { 24 | return data.len; 25 | } 26 | }; 27 | 28 | test "null_writer" { 29 | writeAll(null_writer, .{}, "yay" ** 10) catch |err| switch (err) {}; 30 | } 31 | 32 | pub fn Reader(comptime T: type) type { 33 | return struct { 34 | ReadError: type = anyerror, 35 | read: fn (reader_ctx: T, buffer: []u8) anyerror!usize, 36 | readBuffer: ?fn (reader_ctx: T) anyerror![]const u8 = null, 37 | }; 38 | } 39 | 40 | pub inline fn read( 41 | reader_ctx: anytype, 42 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 43 | buffer: []u8, 44 | ) reader_impl.ReadError!usize { 45 | return @errorCast(reader_impl.read(reader_ctx, buffer)); 46 | } 47 | 48 | pub inline fn isBufferedReader( 49 | comptime ReaderCtx: type, 50 | reader_impl: Impl(Reader, ReaderCtx), 51 | ) bool { 52 | return !(reader_impl.readBuffer == null); 53 | } 54 | 55 | pub inline fn readBuffer( 56 | reader_ctx: anytype, 57 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 58 | ) reader_impl.ReadError![]const u8 { 59 | if (reader_impl.readBuffer) |readBufferFn| { 60 | return @errorCast(readBufferFn(reader_ctx)); 61 | } 62 | @compileError("called 'readBuffer' on unbuffered reader"); 63 | } 64 | 65 | pub inline fn readAll( 66 | reader_ctx: anytype, 67 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 68 | buffer: []u8, 69 | ) reader_impl.ReadError!usize { 70 | return readAtLeast(reader_ctx, reader_impl, buffer, buffer.len); 71 | } 72 | 73 | pub inline fn readAtLeast( 74 | reader_ctx: anytype, 75 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 76 | buffer: []u8, 77 | len: usize, 78 | ) reader_impl.ReadError!usize { 79 | assert(len <= buffer.len); 80 | var index: usize = 0; 81 | while (index < len) { 82 | const amt = try read(reader_ctx, reader_impl, buffer[index..]); 83 | if (amt == 0) break; 84 | index += amt; 85 | } 86 | return index; 87 | } 88 | 89 | pub inline fn readNoEof( 90 | reader_ctx: anytype, 91 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 92 | buf: []u8, 93 | ) (reader_impl.ReadError || error{EndOfStream})!void { 94 | const amt_read = try readAll(reader_ctx, reader_impl, buf); 95 | if (amt_read < buf.len) return error.EndOfStream; 96 | } 97 | 98 | pub inline fn streamUntilDelimiter( 99 | reader_ctx: anytype, 100 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 101 | writer_ctx: anytype, 102 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 103 | delimiter: u8, 104 | optional_max_size: ?usize, 105 | ) (reader_impl.ReadError || writer_impl.WriteError || error{ 106 | EndOfStream, 107 | StreamTooLong, 108 | })!void { 109 | if (isBufferedReader(@TypeOf(reader_ctx), reader_impl)) { 110 | while (true) { 111 | const buffer = try readBuffer(reader_ctx, reader_impl); 112 | if (buffer.len == 0) { 113 | return error.EndOfStream; 114 | } 115 | const len = std.mem.indexOfScalar( 116 | u8, 117 | buffer, 118 | delimiter, 119 | ) orelse buffer.len; 120 | if (optional_max_size) |max| { 121 | if (len > max) { 122 | return error.StreamTooLong; 123 | } 124 | } 125 | 126 | try writeAll(writer_ctx, writer_impl, buffer[0..len]); 127 | if (len != buffer.len) { 128 | return skipBytes(reader_ctx, reader_impl, len + 1, .{}); 129 | } 130 | try skipBytes(reader_ctx, reader_impl, len, .{}); 131 | } 132 | } else { 133 | if (optional_max_size) |max_size| { 134 | for (0..max_size) |_| { 135 | const byte: u8 = try readByte(reader_ctx, reader_impl); 136 | if (byte == delimiter) return; 137 | try writeByte(writer_ctx, writer_impl, byte); 138 | } 139 | return error.StreamTooLong; 140 | } else { 141 | while (true) { 142 | const byte: u8 = try readByte(reader_ctx, reader_impl); 143 | if (byte == delimiter) return; 144 | try writeByte(writer_ctx, writer_impl, byte); 145 | } 146 | } 147 | } 148 | } 149 | 150 | pub inline fn skipUntilDelimiterOrEof( 151 | reader_ctx: anytype, 152 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 153 | delimiter: u8, 154 | ) reader_impl.ReadError!void { 155 | if (isBufferedReader(@TypeOf(reader_ctx), reader_impl)) { 156 | while (true) { 157 | const buffer = try readBuffer(reader_ctx, reader_impl); 158 | if (buffer.len == 0) { 159 | return; 160 | } 161 | const len = std.mem.indexOfScalar( 162 | u8, 163 | buffer, 164 | delimiter, 165 | ) orelse buffer.len; 166 | if (len != buffer.len) { 167 | skipBytes( 168 | reader_ctx, 169 | reader_impl, 170 | len + 1, 171 | .{}, 172 | ) catch unreachable; 173 | return; 174 | } 175 | skipBytes(reader_ctx, reader_impl, len, .{}) catch unreachable; 176 | } 177 | } else { 178 | while (true) { 179 | const byte = readByte( 180 | reader_ctx, 181 | reader_impl, 182 | ) catch |err| switch (err) { 183 | error.EndOfStream => return, 184 | else => |e| return e, 185 | }; 186 | if (byte == delimiter) return; 187 | } 188 | } 189 | } 190 | 191 | pub inline fn readByte( 192 | reader_ctx: anytype, 193 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 194 | ) (reader_impl.ReadError || error{EndOfStream})!u8 { 195 | var result: [1]u8 = undefined; 196 | const amt_read = try read(reader_ctx, reader_impl, result[0..]); 197 | if (amt_read < 1) return error.EndOfStream; 198 | return result[0]; 199 | } 200 | 201 | pub inline fn readByteSigned( 202 | reader_ctx: anytype, 203 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 204 | ) (reader_impl.ReadError || error{EndOfStream})!i8 { 205 | return @as(i8, @bitCast(try readByte(reader_ctx, reader_impl))); 206 | } 207 | 208 | pub inline fn readBytesNoEof( 209 | reader_ctx: anytype, 210 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 211 | comptime num_bytes: usize, 212 | ) (reader_impl.ReadError || error{EndOfStream})![num_bytes]u8 { 213 | var bytes: [num_bytes]u8 = undefined; 214 | try readNoEof(reader_ctx, reader_impl, &bytes); 215 | return bytes; 216 | } 217 | 218 | pub inline fn readInt( 219 | reader_ctx: anytype, 220 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 221 | comptime T: type, 222 | endian: std.builtin.Endian, 223 | ) (reader_impl.ReadError || error{EndOfStream})!T { 224 | const bytes = try readBytesNoEof( 225 | reader_ctx, 226 | reader_impl, 227 | @divExact(@typeInfo(T).Int.bits, 8), 228 | ); 229 | return mem.readInt(T, &bytes, endian); 230 | } 231 | 232 | pub inline fn readVarInt( 233 | reader_ctx: anytype, 234 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 235 | comptime ReturnType: type, 236 | endian: std.builtin.Endian, 237 | size: usize, 238 | ) (reader_impl.ReadError || error{EndOfStream})!ReturnType { 239 | assert(size <= @sizeOf(ReturnType)); 240 | var bytes_buf: [@sizeOf(ReturnType)]u8 = undefined; 241 | const bytes = bytes_buf[0..size]; 242 | try readNoEof(reader_ctx, reader_impl, bytes); 243 | return mem.readVarInt(ReturnType, bytes, endian); 244 | } 245 | 246 | pub inline fn skipBytes( 247 | reader_ctx: anytype, 248 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 249 | num_bytes: u64, 250 | comptime options: struct { 251 | buf_size: usize = 512, 252 | }, 253 | ) (reader_impl.ReadError || error{EndOfStream})!void { 254 | var buf: [options.buf_size]u8 = undefined; 255 | var remaining = num_bytes; 256 | 257 | while (remaining > 0) { 258 | const amt = @min(remaining, options.buf_size); 259 | try readNoEof(reader_ctx, reader_impl, buf[0..amt]); 260 | remaining -= amt; 261 | } 262 | } 263 | 264 | pub inline fn isBytes( 265 | reader_ctx: anytype, 266 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 267 | slice: []const u8, 268 | ) (reader_impl.ReadError || error{EndOfStream})!bool { 269 | var i: usize = 0; 270 | var matches = true; 271 | while (i < slice.len) { 272 | if (isBufferedReader(@TypeOf(reader_ctx), reader_impl)) { 273 | const buffer = try readBuffer(reader_ctx, reader_impl); 274 | const len = @min(buffer.len, slice.len - i); 275 | if (len == 0) { 276 | return error.EndOfStream; 277 | } 278 | if (!std.mem.eql(u8, slice[i..][0..len], buffer[0..len])) { 279 | matches = false; 280 | } 281 | try skipBytes(reader_ctx, reader_impl, len, .{}); 282 | i += len; 283 | } else { 284 | if (slice[i] != try readByte(reader_ctx, reader_impl)) { 285 | matches = false; 286 | } 287 | i += 1; 288 | } 289 | } 290 | return matches; 291 | } 292 | 293 | pub inline fn readStruct( 294 | reader_ctx: anytype, 295 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 296 | comptime T: type, 297 | ) (reader_impl.ReadError || error{EndOfStream})!T { 298 | comptime assert(@typeInfo(T).Struct.layout != .Auto); 299 | var res: [1]T = undefined; 300 | try readNoEof(reader_ctx, reader_impl, mem.sliceAsBytes(res[0..])); 301 | return res[0]; 302 | } 303 | 304 | pub inline fn readStructBig( 305 | reader_ctx: anytype, 306 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 307 | comptime T: type, 308 | ) (reader_impl.ReadError || error{EndOfStream})!T { 309 | var res = try readStruct(reader_ctx, reader_impl, T); 310 | if (native_endian != std.builtin.Endian.big) { 311 | mem.byteSwapAllFields(T, &res); 312 | } 313 | return res; 314 | } 315 | 316 | pub inline fn readEnum( 317 | reader_ctx: anytype, 318 | reader_impl: Impl(Reader, @TypeOf(reader_ctx)), 319 | comptime Enum: type, 320 | endian: std.builtin.Endian, 321 | ) (reader_impl.ReadError || error{ EndOfStream, InvalidValue })!Enum { 322 | const type_info = @typeInfo(Enum).Enum; 323 | const tag = try readInt( 324 | reader_ctx, 325 | reader_impl, 326 | type_info.tag_type, 327 | endian, 328 | ); 329 | 330 | inline for (std.meta.fields(Enum)) |field| { 331 | if (tag == field.value) { 332 | return @field(Enum, field.name); 333 | } 334 | } 335 | 336 | return error.InvalidValue; 337 | } 338 | 339 | pub fn Writer(comptime T: type) type { 340 | return struct { 341 | WriteError: type = anyerror, 342 | write: fn (writer_ctx: T, bytes: []const u8) anyerror!usize, 343 | flushBuffer: ?fn (writer_ctx: T) anyerror!void = null, 344 | }; 345 | } 346 | 347 | pub inline fn write( 348 | writer_ctx: anytype, 349 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 350 | bytes: []const u8, 351 | ) writer_impl.WriteError!usize { 352 | return @errorCast(writer_impl.write(writer_ctx, bytes)); 353 | } 354 | 355 | pub inline fn flushBuffer( 356 | writer_ctx: anytype, 357 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 358 | ) writer_impl.WriteError!void { 359 | if (writer_impl.flushBuffer) |flushFn| { 360 | return @errorCast(flushFn(writer_ctx)); 361 | } 362 | } 363 | 364 | pub fn writeAll( 365 | writer_ctx: anytype, 366 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 367 | bytes: []const u8, 368 | ) writer_impl.WriteError!void { 369 | var index: usize = 0; 370 | while (index != bytes.len) { 371 | index += try write(writer_ctx, writer_impl, bytes[index..]); 372 | } 373 | } 374 | 375 | pub fn writeByte( 376 | writer_ctx: anytype, 377 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 378 | byte: u8, 379 | ) writer_impl.WriteError!void { 380 | const array = [1]u8{byte}; 381 | return writeAll(writer_ctx, writer_impl, &array); 382 | } 383 | 384 | pub fn writeByteNTimes( 385 | writer_ctx: anytype, 386 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 387 | byte: u8, 388 | n: usize, 389 | ) writer_impl.WriteError!void { 390 | var bytes: [256]u8 = undefined; 391 | @memset(bytes[0..], byte); 392 | 393 | var remaining: usize = n; 394 | while (remaining > 0) { 395 | const to_write = @min(remaining, bytes.len); 396 | try writeAll(writer_ctx, writer_impl, bytes[0..to_write]); 397 | remaining -= to_write; 398 | } 399 | } 400 | 401 | pub inline fn writeInt( 402 | writer_ctx: anytype, 403 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 404 | comptime T: type, 405 | value: T, 406 | endian: std.builtin.Endian, 407 | ) writer_impl.WriteError!void { 408 | var bytes: [@divExact(@typeInfo(T).Int.bits, 8)]u8 = undefined; 409 | mem.writeInt( 410 | std.math.ByteAlignedInt(@TypeOf(value)), 411 | &bytes, 412 | value, 413 | endian, 414 | ); 415 | return writeAll(writer_ctx, writer_impl, &bytes); 416 | } 417 | 418 | pub fn writeStruct( 419 | writer_ctx: anytype, 420 | writer_impl: Impl(Writer, @TypeOf(writer_ctx)), 421 | value: anytype, 422 | ) writer_impl.WriteError!void { 423 | comptime assert(@typeInfo(@TypeOf(value)).Struct.layout != .Auto); 424 | return writeAll(writer_ctx, writer_impl, mem.asBytes(&value)); 425 | } 426 | 427 | pub fn Seekable(comptime T: type) type { 428 | return struct { 429 | SeekError: type = anyerror, 430 | 431 | seekTo: fn (seek_ctx: T, pos: u64) anyerror!void, 432 | seekBy: fn (seek_ctx: T, amt: i64) anyerror!void, 433 | 434 | GetSeekPosError: type = anyerror, 435 | 436 | getPos: fn (seek_ctx: T) anyerror!u64, 437 | getEndPos: fn (seek_ctx: T) anyerror!u64, 438 | }; 439 | } 440 | 441 | pub fn seekTo( 442 | seek_ctx: anytype, 443 | seek_impl: Impl(Seekable, @TypeOf(seek_ctx)), 444 | pos: u64, 445 | ) seek_impl.SeekError!void { 446 | return @errorCast(seek_impl.seekTo(seek_ctx, pos)); 447 | } 448 | 449 | pub fn seekBy( 450 | seek_ctx: anytype, 451 | seek_impl: Impl(Seekable, @TypeOf(seek_ctx)), 452 | amt: i64, 453 | ) seek_impl.SeekError!void { 454 | return @errorCast(seek_impl.seekBy(seek_ctx, amt)); 455 | } 456 | 457 | pub fn getPos( 458 | seek_ctx: anytype, 459 | seek_impl: Impl(Seekable, @TypeOf(seek_ctx)), 460 | ) seek_impl.GetSeekPosError!u64 { 461 | return @errorCast(seek_impl.getPos(seek_ctx)); 462 | } 463 | 464 | pub fn getEndPos( 465 | seek_ctx: anytype, 466 | seek_impl: Impl(Seekable, @TypeOf(seek_ctx)), 467 | ) seek_impl.GetSeekPosError!u64 { 468 | return @errorCast(seek_impl.getEndPos(seek_ctx)); 469 | } 470 | 471 | test { 472 | std.testing.refAllDecls(@This()); 473 | } 474 | -------------------------------------------------------------------------------- /examples/io/FixedBufferReader.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const io = @import("../io.zig"); 5 | const vio = @import("../vio.zig"); 6 | 7 | buffer: []const u8, 8 | pos: usize = 0, 9 | 10 | pub const ReadError = error{}; 11 | 12 | pub fn read(self: *@This(), out_buffer: []u8) ReadError!usize { 13 | const len = @min(self.buffer[self.pos..].len, out_buffer.len); 14 | @memcpy( 15 | out_buffer[0..len], 16 | self.buffer[self.pos..][0..len], 17 | ); 18 | self.pos += len; 19 | return len; 20 | } 21 | 22 | pub fn readBuffer(self: *const @This()) ReadError![]const u8 { 23 | return self.buffer[self.pos..]; 24 | } 25 | 26 | pub fn seekTo(self: *@This(), pos: u64) error{}!void { 27 | if (std.math.cast(usize, pos)) |usize_pos| { 28 | self.pos = @min(self.buffer.len, usize_pos); 29 | } 30 | } 31 | 32 | pub fn seekBy(self: *@This(), amt: i64) error{}!void { 33 | const negate = amt < 0; 34 | if (std.math.cast(usize, @abs(amt))) |abs_amt| { 35 | if (negate) { 36 | if (abs_amt > self.pos) { 37 | self.pos = 0; 38 | } else { 39 | self.pos -= abs_amt; 40 | } 41 | } else { 42 | self.pos += abs_amt; 43 | } 44 | } 45 | } 46 | 47 | pub fn getPos(self: *const @This()) error{}!u64 { 48 | return self.pos; 49 | } 50 | 51 | pub fn getEndPos(self: *const @This()) error{}!u64 { 52 | return self.buffer.len; 53 | } 54 | 55 | test "read and seek" { 56 | const buffer: []const u8 = "I really hope that this works!"; 57 | var stream = @This(){ .buffer = buffer, .pos = 0 }; 58 | 59 | var out_buf: [buffer.len]u8 = undefined; 60 | const len1 = try io.readAll(&stream, .{}, &out_buf); 61 | try testing.expectEqual(buffer.len, len1); 62 | try testing.expectEqualSlices(u8, buffer, &out_buf); 63 | 64 | try io.seekTo(&stream, .{}, 0); 65 | try testing.expectEqual(@as(u64, 0), try io.getPos(&stream, .{})); 66 | 67 | const len2 = try io.readAll(&stream, .{}, &out_buf); 68 | try testing.expectEqual(buffer.len, len2); 69 | try testing.expectEqualSlices(u8, buffer, &out_buf); 70 | } 71 | 72 | test "virtual read" { 73 | const buffer: []const u8 = "I really hope that this works!"; 74 | var stream = @This(){ .buffer = buffer, .pos = 0 }; 75 | const reader = vio.Reader.init(.direct, &stream, .{}); 76 | 77 | var out_buf: [buffer.len]u8 = undefined; 78 | const len1 = try vio.readAll(reader, &out_buf); 79 | try testing.expectEqual(buffer.len, len1); 80 | try testing.expectEqualSlices(u8, buffer, &out_buf); 81 | } 82 | 83 | test "FixedBufferReader is a buffered io.Reader" { 84 | try std.testing.expect(io.isBufferedReader(*@This(), .{})); 85 | } 86 | -------------------------------------------------------------------------------- /examples/io/FixedBufferStream.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const io = @import("../io.zig"); 5 | const vio = @import("../vio.zig"); 6 | 7 | buffer: []u8, 8 | pos: usize = 0, 9 | 10 | pub const ReadError = error{}; 11 | pub const WriteError = error{NoSpaceLeft}; 12 | pub const SeekError = error{}; 13 | pub const GetSeekPosError = error{}; 14 | 15 | pub fn read(self: *@This(), out_buffer: []u8) ReadError!usize { 16 | const len = @min(self.buffer[self.pos..].len, out_buffer.len); 17 | @memcpy( 18 | out_buffer[0..len], 19 | self.buffer[self.pos..][0..len], 20 | ); 21 | self.pos += len; 22 | return len; 23 | } 24 | 25 | pub fn readBuffer(self: *const @This()) ReadError![]u8 { 26 | return self.buffer[self.pos..]; 27 | } 28 | 29 | pub fn write(self: *@This(), in_buffer: []const u8) WriteError!usize { 30 | const len = @min(self.buffer[self.pos..].len, in_buffer.len); 31 | if (len == 0) { 32 | return WriteError.NoSpaceLeft; 33 | } 34 | @memcpy( 35 | self.buffer[self.pos..][0..len], 36 | in_buffer[0..len], 37 | ); 38 | self.pos += len; 39 | return len; 40 | } 41 | 42 | pub fn seekTo(self: *@This(), pos: u64) SeekError!void { 43 | if (std.math.cast(usize, pos)) |usize_pos| { 44 | self.pos = @min(self.buffer.len, usize_pos); 45 | } 46 | } 47 | 48 | pub fn seekBy(self: *@This(), amt: i64) SeekError!void { 49 | const negate = amt < 0; 50 | if (std.math.cast(usize, @abs(amt))) |abs_amt| { 51 | if (negate) { 52 | if (abs_amt > self.pos) { 53 | self.pos = 0; 54 | } else { 55 | self.pos -= abs_amt; 56 | } 57 | } else { 58 | self.pos += abs_amt; 59 | } 60 | } 61 | } 62 | 63 | pub fn getPos(self: *const @This()) GetSeekPosError!u64 { 64 | return self.pos; 65 | } 66 | 67 | pub fn getEndPos(self: *const @This()) GetSeekPosError!u64 { 68 | return self.buffer.len; 69 | } 70 | 71 | pub fn getWritten(self: *const @This()) []u8 { 72 | return self.buffer[0..self.pos]; 73 | } 74 | 75 | test "write, seek 0, and read back" { 76 | const in_buf: []const u8 = "I really hope that this works!"; 77 | 78 | var stream_buf: [in_buf.len]u8 = undefined; 79 | var stream = @This(){ .buffer = &stream_buf, .pos = 0 }; 80 | 81 | try io.writeAll(&stream, .{}, in_buf); 82 | 83 | var out_buf: [in_buf.len]u8 = undefined; 84 | try io.seekTo(&stream, .{}, 0); 85 | try testing.expectEqual(@as(u64, 0), try io.getPos(&stream, .{})); 86 | 87 | const rlen = try io.readAll(&stream, .{}, &out_buf); 88 | try testing.expectEqual(in_buf.len, rlen); 89 | try testing.expectEqualSlices(u8, in_buf, &out_buf); 90 | } 91 | 92 | test "virtual write and read back" { 93 | const in_buf: []const u8 = "I really hope that this works!"; 94 | 95 | var stream_buf: [in_buf.len]u8 = undefined; 96 | var stream = @This(){ .buffer = &stream_buf, .pos = 0 }; 97 | const writer = vio.Writer.init(.direct, &stream, .{}); 98 | 99 | try vio.writeAll(writer, in_buf); 100 | 101 | stream.pos = 0; 102 | 103 | const reader = vio.Reader.init(.direct, &stream, .{}); 104 | var out_buf: [in_buf.len]u8 = undefined; 105 | const rlen = try vio.readAll(reader, &out_buf); 106 | 107 | try testing.expectEqual(in_buf.len, rlen); 108 | try testing.expectEqualSlices(u8, in_buf, &out_buf); 109 | } 110 | 111 | test "FixedBufferStream is a buffered io.Reader" { 112 | const impl = @import("zimpl").Impl(io.Reader, *@This()){}; 113 | try std.testing.expect(!(impl.readBuffer == null)); 114 | } 115 | -------------------------------------------------------------------------------- /examples/io/buffered_reader.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const Impl = @import("zimpl").Impl; 5 | 6 | const io = @import("../io.zig"); 7 | const vio = @import("../vio.zig"); 8 | 9 | pub fn BufferedReader( 10 | comptime buffer_size: usize, 11 | comptime ChildCtx: type, 12 | comptime child_impl: Impl(io.Reader, ChildCtx), 13 | ) type { 14 | return struct { 15 | child_ctx: ChildCtx, 16 | buffer: [buffer_size]u8 = undefined, 17 | start: usize = 0, 18 | end: usize = 0, 19 | 20 | pub const ReadError = child_impl.ReadError; 21 | 22 | fn fillBuffer(self: *@This()) ReadError!void { 23 | self.start = 0; 24 | self.end = try io.read( 25 | self.child_ctx, 26 | child_impl, 27 | self.buffer[0..], 28 | ); 29 | } 30 | 31 | pub fn read(self: *@This(), dest: []u8) ReadError!usize { 32 | var dest_index: usize = 0; 33 | while (dest_index < dest.len) { 34 | const written = @min( 35 | dest.len - dest_index, 36 | self.end - self.start, 37 | ); 38 | @memcpy( 39 | dest[dest_index..][0..written], 40 | self.buffer[self.start..][0..written], 41 | ); 42 | if (written == 0) { 43 | try self.fillBuffer(); 44 | if (self.start == self.end) { 45 | return dest_index; 46 | } 47 | } 48 | self.start += written; 49 | dest_index += written; 50 | } 51 | return dest.len; 52 | } 53 | 54 | pub fn readBuffer(self: *@This()) ReadError![]const u8 { 55 | if (self.start == self.end) { 56 | try self.fillBuffer(); 57 | } 58 | return self.buffer[self.start..self.end]; 59 | } 60 | }; 61 | } 62 | 63 | pub fn bufferedReader( 64 | comptime buffer_size: usize, 65 | child_ctx: anytype, 66 | child_impl: Impl(io.Reader, @TypeOf(child_ctx)), 67 | ) BufferedReader(buffer_size, @TypeOf(child_ctx), child_impl) { 68 | return .{ .child_ctx = child_ctx }; 69 | } 70 | 71 | test "buffered fixed buffer reader" { 72 | const buffer = "Hello! Is anybody there?"; 73 | var fb_reader = io.FixedBufferReader{ .buffer = buffer }; 74 | var buff_reader = bufferedReader(8, &fb_reader, .{}); 75 | try std.testing.expect(io.isBufferedReader(@TypeOf(&buff_reader), .{})); 76 | 77 | var out_bytes: [buffer.len]u8 = undefined; 78 | const len = try io.readAll(&buff_reader, .{}, &out_bytes); 79 | try std.testing.expectEqual(buffer.len, len); 80 | try std.testing.expectEqualStrings(buffer, &out_bytes); 81 | } 82 | 83 | test "virtual buffered fixed buffer reader" { 84 | const buffer = "Hello! Is anybody there?"; 85 | var fb_reader = io.FixedBufferReader{ .buffer = buffer }; 86 | var buff_reader = bufferedReader(8, &fb_reader, .{}); 87 | const reader = vio.Reader.init(.direct, &buff_reader, .{}); 88 | try std.testing.expect(vio.isBufferedReader(reader)); 89 | 90 | var out_bytes: [buffer.len]u8 = undefined; 91 | const len = try vio.readAll(reader, &out_bytes); 92 | try std.testing.expectEqual(buffer.len, len); 93 | try std.testing.expectEqualStrings(buffer, &out_bytes); 94 | } 95 | -------------------------------------------------------------------------------- /examples/io/buffered_writer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const Impl = @import("zimpl").Impl; 5 | 6 | const io = @import("../io.zig"); 7 | const vio = @import("../vio.zig"); 8 | 9 | pub fn BufferedWriter( 10 | comptime buffer_size: usize, 11 | comptime ChildCtx: type, 12 | comptime child_impl: Impl(io.Writer, ChildCtx), 13 | ) type { 14 | return struct { 15 | child_ctx: ChildCtx, 16 | buffer: [buffer_size]u8 = undefined, 17 | end: usize = 0, 18 | 19 | pub const WriteError = child_impl.WriteError; 20 | 21 | pub fn flushBuffer(self: *@This()) WriteError!void { 22 | try io.writeAll( 23 | self.child_ctx, 24 | child_impl, 25 | self.buffer[0..self.end], 26 | ); 27 | self.end = 0; 28 | } 29 | 30 | pub fn write(self: *@This(), bytes: []const u8) WriteError!usize { 31 | if (self.end + bytes.len > self.buffer.len) { 32 | try self.flushBuffer(); 33 | if (bytes.len > self.buffer.len) { 34 | return io.write(self.child_ctx, child_impl, bytes); 35 | } 36 | } 37 | const new_end = self.end + bytes.len; 38 | @memcpy(self.buffer[self.end..new_end], bytes); 39 | self.end = new_end; 40 | return bytes.len; 41 | } 42 | }; 43 | } 44 | 45 | pub fn bufferedWriter( 46 | comptime buffer_size: usize, 47 | child_ctx: anytype, 48 | child_impl: Impl(io.Writer, @TypeOf(child_ctx)), 49 | ) BufferedWriter(buffer_size, @TypeOf(child_ctx), child_impl) { 50 | return .{ .child_ctx = child_ctx }; 51 | } 52 | 53 | test "count bytes written to null_writer" { 54 | var count_writer = io.countingWriter(io.null_writer, .{}); 55 | var buff_writer = bufferedWriter(8, &count_writer, .{}); 56 | try io.writeAll(&buff_writer, .{}, "Hello!"); 57 | try std.testing.expectEqual(@as(usize, 0), count_writer.bytes_written); 58 | try io.writeAll(&buff_writer, .{}, "Is anybody there?"); 59 | try std.testing.expectEqual(@as(usize, 23), count_writer.bytes_written); 60 | } 61 | 62 | test "virtual count bytes written to null_writer" { 63 | var count_writer = io.countingWriter(io.null_writer, .{}); 64 | var buff_writer = bufferedWriter(8, &count_writer, .{}); 65 | const writer = vio.Writer.init(.direct, &buff_writer, .{}); 66 | try vio.writeAll(writer, "Hello!"); 67 | try std.testing.expectEqual(@as(usize, 0), count_writer.bytes_written); 68 | try vio.writeAll(writer, "Is anybody there?"); 69 | try std.testing.expectEqual(@as(usize, 23), count_writer.bytes_written); 70 | } 71 | -------------------------------------------------------------------------------- /examples/io/counting_writer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const Impl = @import("zimpl").Impl; 5 | 6 | const io = @import("../io.zig"); 7 | 8 | pub fn CountingWriter( 9 | comptime ChildCtx: type, 10 | comptime child_impl: Impl(io.Writer, ChildCtx), 11 | ) type { 12 | return struct { 13 | child_ctx: ChildCtx, 14 | bytes_written: u64 = 0, 15 | 16 | pub const WriteError = child_impl.WriteError; 17 | 18 | pub fn write(self: *@This(), bytes: []const u8) WriteError!usize { 19 | const len = try io.write(self.child_ctx, child_impl, bytes); 20 | self.bytes_written += len; 21 | return len; 22 | } 23 | }; 24 | } 25 | 26 | pub fn countingWriter( 27 | child_ctx: anytype, 28 | child_impl: Impl(io.Writer, @TypeOf(child_ctx)), 29 | ) CountingWriter(@TypeOf(child_ctx), child_impl) { 30 | return .{ .child_ctx = child_ctx }; 31 | } 32 | 33 | test "count bytes written to null_writer" { 34 | var writer = countingWriter(io.null_writer, .{}); 35 | try io.writeAll(&writer, .{}, "Hello!"); 36 | try io.writeAll(&writer, .{}, "Is anybody there?"); 37 | try std.testing.expectEqual(@as(usize, 23), writer.bytes_written); 38 | } 39 | -------------------------------------------------------------------------------- /examples/iterator.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const Impl = @import("zimpl").Impl; 5 | 6 | fn Iterator(comptime Data: type) fn (type) type { 7 | return struct { 8 | pub fn Ifc(comptime T: type) type { 9 | return struct { 10 | next: fn (*T) ?*Data, 11 | }; 12 | } 13 | }.Ifc; 14 | } 15 | 16 | pub fn apply( 17 | comptime T: type, 18 | comptime f: fn (*T) void, 19 | iter: anytype, 20 | impl: Impl(Iterator(T), @TypeOf(iter)), 21 | ) void { 22 | var mut_iter = iter; 23 | while (impl.next(&mut_iter)) |t| { 24 | f(t); 25 | } 26 | } 27 | 28 | fn addThree(n: *i32) void { 29 | n.* += 3; 30 | } 31 | 32 | fn SliceIter(comptime T: type) type { 33 | return struct { 34 | slice: []T, 35 | 36 | pub fn init(s: []T) @This() { 37 | return .{ 38 | .slice = s, 39 | }; 40 | } 41 | 42 | pub fn next(self: *@This()) ?*T { 43 | if (self.slice.len == 0) { 44 | return null; 45 | } 46 | const head = &self.slice[0]; 47 | self.slice = self.slice[1..]; 48 | return head; 49 | } 50 | }; 51 | } 52 | 53 | test "slice iterator" { 54 | var fibo = [_]i32{ 1, 1, 2, 3, 5, 8, 13, 21 }; 55 | apply(i32, addThree, SliceIter(i32).init(&fibo), .{}); 56 | try testing.expectEqualSlices( 57 | i32, 58 | &[_]i32{ 4, 4, 5, 6, 8, 11, 16, 24 }, 59 | &fibo, 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /examples/read_file.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const io = @import("io.zig"); 3 | const vio = @import("vio.zig"); 4 | const FixedBufferStream = io.FixedBufferStream; 5 | 6 | test "read file with std.fs.File" { 7 | const file = try std.fs.cwd().openFile("examples/read_file/test.txt", .{}); 8 | var buffer: [32]u8 = undefined; 9 | var fbs: FixedBufferStream = .{ .buffer = &buffer }; 10 | try io.streamUntilDelimiter(file, .{}, &fbs, .{}, '\n', 32); 11 | try std.testing.expectEqualStrings( 12 | "Hello, I am a file!", 13 | fbs.getWritten(), 14 | ); 15 | } 16 | 17 | test "read file with std.posix.fd_t" { 18 | const fd = try std.posix.open("examples/read_file/test.txt", .{}, 0); 19 | var buffer: [32]u8 = undefined; 20 | var fbs: FixedBufferStream = .{ .buffer = &buffer }; 21 | try io.streamUntilDelimiter( 22 | fd, 23 | .{ 24 | .read = std.posix.read, 25 | .ReadError = std.posix.ReadError, 26 | }, 27 | &fbs, 28 | .{}, 29 | '\n', 30 | 32, 31 | ); 32 | try std.testing.expectEqualStrings( 33 | "Hello, I am a file!", 34 | fbs.getWritten(), 35 | ); 36 | } 37 | 38 | test "read file with buffered std.fs.File" { 39 | const file = try std.fs.cwd().openFile("examples/read_file/test.txt", .{}); 40 | var buffered_file = io.bufferedReader(256, file, .{}); 41 | var buffer: [32]u8 = undefined; 42 | var fbs: FixedBufferStream = .{ .buffer = &buffer }; 43 | try io.streamUntilDelimiter(&buffered_file, .{}, &fbs, .{}, '\n', 32); 44 | try std.testing.expectEqualStrings( 45 | "Hello, I am a file!", 46 | fbs.getWritten(), 47 | ); 48 | } 49 | 50 | test "read file with buffered std.posix.fd_t" { 51 | const fd = try std.posix.open( 52 | "examples/read_file/test.txt", .{}, 0); 53 | var buffered_fd = io.bufferedReader(256, fd, .{ 54 | .read = std.posix.read, 55 | .ReadError = std.posix.ReadError, 56 | }); 57 | var buffer: [32]u8 = undefined; 58 | var fbs: FixedBufferStream = .{ .buffer = &buffer }; 59 | try io.streamUntilDelimiter(&buffered_fd, .{}, &fbs, .{}, '\n', 32); 60 | try std.testing.expectEqualStrings( 61 | "Hello, I am a file!", 62 | fbs.getWritten(), 63 | ); 64 | } 65 | 66 | test "virtual read file with std.fs.File" { 67 | const file = try std.fs.cwd().openFile("examples/read_file/test.txt", .{}); 68 | var buffer: [32]u8 = undefined; 69 | var fbs: FixedBufferStream = .{ .buffer = &buffer }; 70 | try vio.streamUntilDelimiter( 71 | vio.Reader.init(.indirect, &file, .{}), 72 | vio.Writer.init(.direct, &fbs, .{}), 73 | '\n', 74 | 32, 75 | ); 76 | try std.testing.expectEqualStrings( 77 | "Hello, I am a file!", 78 | fbs.getWritten(), 79 | ); 80 | } 81 | 82 | test "virtual read file with std.posix.fd_t" { 83 | const fd = try std.posix.open("examples/read_file/test.txt", .{}, 0); 84 | var buffer: [32]u8 = undefined; 85 | var fbs: FixedBufferStream = .{ .buffer = &buffer }; 86 | try vio.streamUntilDelimiter( 87 | vio.Reader.init(.indirect, &fd, .{ 88 | .read = std.posix.read, 89 | .ReadError = std.posix.ReadError, 90 | }), 91 | vio.Writer.init(.direct, &fbs, .{}), 92 | '\n', 93 | 32, 94 | ); 95 | try std.testing.expectEqualStrings( 96 | "Hello, I am a file!", 97 | fbs.getWritten(), 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /examples/read_file/test.txt: -------------------------------------------------------------------------------- 1 | Hello, I am a file! 2 | 3 | -------------------------------------------------------------------------------- /examples/vcount.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const ztable = @import("zimpl").ztable; 5 | const VIfc = ztable.VIfc; 6 | 7 | const Counter = VIfc(@import("count.zig").Counter); 8 | 9 | pub fn countToTen(ctr: Counter) void { 10 | while (ctr.vtable.read(ctr.ctx) < 10) { 11 | ctr.vtable.increment(ctr.ctx); 12 | } 13 | } 14 | 15 | test "explicit implementation" { 16 | const USize = struct { 17 | pub fn inc(i: *usize) void { 18 | i.* += 1; 19 | } 20 | pub fn deref(i: *const usize) usize { 21 | return i.*; 22 | } 23 | }; 24 | var count: usize = 0; 25 | countToTen(Counter.init( 26 | .direct, 27 | &count, 28 | .{ .increment = USize.inc, .read = USize.deref }, 29 | )); 30 | try testing.expectEqual(@as(usize, 10), count); 31 | } 32 | 33 | const MyCounter = struct { 34 | count: usize, 35 | 36 | pub fn increment(self: *@This()) void { 37 | self.count += 1; 38 | } 39 | 40 | pub fn read(self: *const @This()) usize { 41 | return self.count; 42 | } 43 | }; 44 | 45 | test "infer implementation" { 46 | var my_counter: MyCounter = .{ .count = 0 }; 47 | countToTen(Counter.init(.direct, &my_counter, .{})); 48 | try testing.expectEqual(@as(usize, 10), my_counter.count); 49 | } 50 | 51 | fn otherInc(self: *MyCounter) void { 52 | self.count = 1 + self.count * 2; 53 | } 54 | 55 | test "override implementation" { 56 | var my_counter: MyCounter = .{ .count = 0 }; 57 | countToTen(Counter.init(.direct, &my_counter, .{ .increment = otherInc })); 58 | try testing.expectEqual(@as(usize, 15), my_counter.count); 59 | } 60 | -------------------------------------------------------------------------------- /examples/vcount2.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | const ztable = @import("zimpl").ztable; 5 | const VTable = ztable.VTable; 6 | const vtable = ztable.vtable; 7 | 8 | const Counter = @import("count.zig").Counter; 9 | 10 | pub fn countToTen(ctr_ctx: *anyopaque, ctr_vtable: VTable(Counter)) void { 11 | while (ctr_vtable.read(ctr_ctx) < 10) { 12 | ctr_vtable.increment(ctr_ctx); 13 | } 14 | } 15 | 16 | test "explicit implementation" { 17 | const USize = struct { 18 | pub fn inc(i: *usize) void { 19 | i.* += 1; 20 | } 21 | pub fn deref(i: *const usize) usize { 22 | return i.*; 23 | } 24 | }; 25 | var count: usize = 0; 26 | countToTen(&count, vtable( 27 | Counter, 28 | .direct, 29 | @TypeOf(&count), 30 | .{ .increment = USize.inc, .read = USize.deref }, 31 | )); 32 | try testing.expectEqual(@as(usize, 10), count); 33 | } 34 | 35 | const MyCounter = struct { 36 | count: usize, 37 | 38 | pub fn increment(self: *@This()) void { 39 | self.count += 1; 40 | } 41 | 42 | pub fn read(self: *const @This()) usize { 43 | return self.count; 44 | } 45 | }; 46 | 47 | test "infer implementation" { 48 | var my_counter: MyCounter = .{ .count = 0 }; 49 | countToTen( 50 | &my_counter, 51 | vtable(Counter, .direct, @TypeOf(&my_counter), .{}), 52 | ); 53 | try testing.expectEqual(@as(usize, 10), my_counter.count); 54 | } 55 | 56 | fn otherInc(self: *MyCounter) void { 57 | self.count = 1 + self.count * 2; 58 | } 59 | 60 | test "override implementation" { 61 | var my_counter: MyCounter = .{ .count = 0 }; 62 | countToTen(&my_counter, vtable( 63 | Counter, 64 | .direct, 65 | @TypeOf(&my_counter), 66 | .{ .increment = otherInc }, 67 | )); 68 | try testing.expectEqual(@as(usize, 15), my_counter.count); 69 | } 70 | -------------------------------------------------------------------------------- /examples/vio.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const native_endian = @import("builtin").target.cpu.arch.endian(); 3 | const mem = std.mem; 4 | const assert = std.debug.assert; 5 | 6 | const ztable = @import("zimpl").ztable; 7 | const VIfc = ztable.VIfc; 8 | 9 | const io = @import("io.zig"); 10 | 11 | pub const Reader = VIfc(io.Reader); 12 | 13 | pub inline fn read(reader: Reader, buffer: []u8) anyerror!usize { 14 | return reader.vtable.read(reader.ctx, buffer); 15 | } 16 | 17 | pub inline fn isBufferedReader(reader: Reader) bool { 18 | return !(reader.vtable.readBuffer == null); 19 | } 20 | 21 | pub inline fn readBuffer(reader: Reader) anyerror![]const u8 { 22 | return reader.vtable.readBuffer.?(reader.ctx); 23 | } 24 | 25 | pub inline fn readAll(reader: Reader, buffer: []u8) anyerror!usize { 26 | return readAtLeast(reader, buffer, buffer.len); 27 | } 28 | 29 | pub fn readAtLeast(reader: Reader, buffer: []u8, len: usize) anyerror!usize { 30 | assert(len <= buffer.len); 31 | var index: usize = 0; 32 | while (index < len) { 33 | const amt = try read(reader, buffer[index..]); 34 | if (amt == 0) break; 35 | index += amt; 36 | } 37 | return index; 38 | } 39 | 40 | pub fn readNoEof(reader: Reader, buf: []u8) anyerror!void { 41 | const amt_read = try readAll(reader, buf); 42 | if (amt_read < buf.len) return error.EndOfStream; 43 | } 44 | 45 | pub fn streamUntilDelimiter( 46 | reader: Reader, 47 | writer: Writer, 48 | delimiter: u8, 49 | optional_max_size: ?usize, 50 | ) anyerror!void { 51 | if (isBufferedReader(reader)) { 52 | while (true) { 53 | const buffer = try readBuffer(reader); 54 | if (buffer.len == 0) { 55 | return error.EndOfStream; 56 | } 57 | const len = std.mem.indexOfScalar( 58 | u8, 59 | buffer, 60 | delimiter, 61 | ) orelse buffer.len; 62 | if (optional_max_size) |max| { 63 | if (len > max) { 64 | return error.StreamTooLong; 65 | } 66 | } 67 | 68 | try writeAll(writer, buffer[0..len]); 69 | if (len != buffer.len) { 70 | return skipBytes(reader, len + 1, .{}); 71 | } 72 | try skipBytes(reader, len, .{}); 73 | } 74 | } else { 75 | if (optional_max_size) |max_size| { 76 | for (0..max_size) |_| { 77 | const byte: u8 = try readByte(reader); 78 | if (byte == delimiter) return; 79 | try writeByte(writer, byte); 80 | } 81 | return error.StreamTooLong; 82 | } else { 83 | while (true) { 84 | const byte: u8 = try readByte(reader); 85 | if (byte == delimiter) return; 86 | try writeByte(writer, byte); 87 | } 88 | } 89 | } 90 | } 91 | 92 | pub fn skipUntilDelimiterOrEof(reader: Reader, delimiter: u8) anyerror!void { 93 | if (isBufferedReader(reader)) { 94 | while (true) { 95 | const buffer = try readBuffer(reader); 96 | if (buffer.len == 0) { 97 | return; 98 | } 99 | const len = std.mem.indexOfScalar( 100 | u8, 101 | buffer, 102 | delimiter, 103 | ) orelse buffer.len; 104 | if (len != buffer.len) { 105 | skipBytes(reader, len + 1, .{}) catch unreachable; 106 | return; 107 | } 108 | skipBytes(reader, len, .{}) catch unreachable; 109 | } 110 | } else { 111 | while (true) { 112 | const byte = readByte(reader) catch |err| switch (err) { 113 | error.EndOfStream => return, 114 | else => |e| return e, 115 | }; 116 | if (byte == delimiter) return; 117 | } 118 | } 119 | } 120 | 121 | pub fn readByte(reader: Reader) anyerror!u8 { 122 | var result: [1]u8 = undefined; 123 | const amt_read = try read(reader, result[0..]); 124 | if (amt_read < 1) return error.EndOfStream; 125 | return result[0]; 126 | } 127 | 128 | pub fn readByteSigned(reader: Reader) anyerror!i8 { 129 | return @as(i8, @bitCast(try readByte(reader))); 130 | } 131 | 132 | pub fn readBytesNoEof( 133 | reader: Reader, 134 | comptime num_bytes: usize, 135 | ) anyerror![num_bytes]u8 { 136 | var bytes: [num_bytes]u8 = undefined; 137 | try readNoEof(reader, &bytes); 138 | return bytes; 139 | } 140 | 141 | pub fn readInt( 142 | reader: Reader, 143 | comptime T: type, 144 | endian: std.builtin.Endian, 145 | ) anyerror!T { 146 | const bytes = try readBytesNoEof( 147 | reader, 148 | @divExact(@typeInfo(T).Int.bits, 8), 149 | ); 150 | return mem.readInt(T, &bytes, endian); 151 | } 152 | 153 | pub fn readVarInt( 154 | reader: Reader, 155 | comptime ReturnType: type, 156 | endian: std.builtin.Endian, 157 | size: usize, 158 | ) anyerror!ReturnType { 159 | assert(size <= @sizeOf(ReturnType)); 160 | var bytes_buf: [@sizeOf(ReturnType)]u8 = undefined; 161 | const bytes = bytes_buf[0..size]; 162 | try readNoEof(reader, bytes); 163 | return mem.readVarInt(ReturnType, bytes, endian); 164 | } 165 | 166 | pub fn skipBytes( 167 | reader: Reader, 168 | num_bytes: u64, 169 | comptime options: struct { 170 | buf_size: usize = 512, 171 | }, 172 | ) anyerror!void { 173 | var buf: [options.buf_size]u8 = undefined; 174 | var remaining = num_bytes; 175 | 176 | while (remaining > 0) { 177 | const amt = @min(remaining, options.buf_size); 178 | try readNoEof(reader, buf[0..amt]); 179 | remaining -= amt; 180 | } 181 | } 182 | 183 | pub fn isBytes(reader: Reader, slice: []const u8) anyerror!bool { 184 | var i: usize = 0; 185 | var matches = true; 186 | while (i < slice.len) { 187 | if (isBufferedReader(reader)) { 188 | const buffer = try readBuffer(reader); 189 | const len = @min(buffer.len, slice.len - i); 190 | if (len == 0) { 191 | return error.EndOfStream; 192 | } 193 | if (!std.mem.eql(u8, slice[i..][0..len], buffer[0..len])) { 194 | matches = false; 195 | } 196 | try skipBytes(reader, len, .{}); 197 | i += len; 198 | } else { 199 | if (slice[i] != try readByte(reader)) { 200 | matches = false; 201 | } 202 | i += 1; 203 | } 204 | } 205 | return matches; 206 | } 207 | 208 | pub fn readStruct(reader: Reader, comptime T: type) anyerror!T { 209 | comptime assert(@typeInfo(T).Struct.layout != .Auto); 210 | var res: [1]T = undefined; 211 | try readNoEof(reader, mem.sliceAsBytes(res[0..])); 212 | return res[0]; 213 | } 214 | 215 | pub fn readStructBig(reader: Reader, comptime T: type) anyerror!T { 216 | var res = try readStruct(reader, T); 217 | if (native_endian != std.builtin.Endian.big) { 218 | mem.byteSwapAllFields(T, &res); 219 | } 220 | return res; 221 | } 222 | 223 | pub fn readEnum( 224 | reader: Reader, 225 | comptime Enum: type, 226 | endian: std.builtin.Endian, 227 | ) anyerror!Enum { 228 | const type_info = @typeInfo(Enum).Enum; 229 | const tag = try readInt(reader, type_info.tag_type, endian); 230 | 231 | inline for (std.meta.fields(Enum)) |field| { 232 | if (tag == field.value) { 233 | return @field(Enum, field.name); 234 | } 235 | } 236 | 237 | return error.InvalidValue; 238 | } 239 | 240 | pub const Writer = VIfc(io.Writer); 241 | 242 | pub inline fn write(writer: Writer, bytes: []const u8) anyerror!usize { 243 | return writer.vtable.write(writer.ctx, bytes); 244 | } 245 | 246 | pub inline fn flushBuffer(writer: Writer) anyerror!void { 247 | if (writer.vtable.flushBuffer) |flushFn| { 248 | return flushFn(writer.ctx); 249 | } 250 | } 251 | 252 | pub fn writeAll(writer: Writer, bytes: []const u8) anyerror!void { 253 | var index: usize = 0; 254 | while (index != bytes.len) { 255 | index += try write(writer, bytes[index..]); 256 | } 257 | } 258 | 259 | pub fn writeByte(writer: Writer, byte: u8) anyerror!void { 260 | const array = [1]u8{byte}; 261 | return writeAll(writer, &array); 262 | } 263 | 264 | pub fn writeByteNTimes( 265 | writer: Writer, 266 | byte: u8, 267 | n: usize, 268 | ) anyerror!void { 269 | var bytes: [256]u8 = undefined; 270 | @memset(bytes[0..], byte); 271 | 272 | var remaining: usize = n; 273 | while (remaining > 0) { 274 | const to_write = @min(remaining, bytes.len); 275 | try writeAll(writer, bytes[0..to_write]); 276 | remaining -= to_write; 277 | } 278 | } 279 | 280 | pub fn writeInt( 281 | writer: Writer, 282 | comptime T: type, 283 | value: T, 284 | endian: std.builtin.Endian, 285 | ) anyerror!void { 286 | var bytes: [@divExact(@typeInfo(T).Int.bits, 8)]u8 = undefined; 287 | mem.writeInt( 288 | std.math.ByteAlignedInt(@TypeOf(value)), 289 | &bytes, 290 | value, 291 | endian, 292 | ); 293 | return writeAll(writer, &bytes); 294 | } 295 | 296 | pub fn writeStruct(writer: Writer, value: anytype) anyerror!void { 297 | comptime assert(@typeInfo(@TypeOf(value)).Struct.layout != .Auto); 298 | return writeAll(writer, mem.asBytes(&value)); 299 | } 300 | 301 | test { 302 | std.testing.refAllDecls(@This()); 303 | } 304 | -------------------------------------------------------------------------------- /src/zimpl.zig: -------------------------------------------------------------------------------- 1 | pub const ztable = @import("ztable.zig"); 2 | 3 | pub fn Impl(comptime Ifc: fn (type) type, comptime T: type) type { 4 | const U = switch (@typeInfo(T)) { 5 | .@"pointer" => |info| if (info.size == .one) info.child else T, 6 | else => T, 7 | }; 8 | switch (@typeInfo(U)) { 9 | .@"struct", .@"union", .@"enum", .@"opaque" => {}, 10 | else => return Ifc(T), 11 | } 12 | var fields = @typeInfo(Ifc(T)).@"struct".fields[0..].*; 13 | for (&fields) |*field| { 14 | if (@hasDecl(U, field.name)) { 15 | field.*.default_value_ptr = &@as(field.type, @field(U, field.name)); 16 | } 17 | } 18 | return @Type(@import("std").builtin.Type{ .@"struct" = .{ 19 | .layout = .auto, 20 | .fields = &fields, 21 | .decls = &.{}, 22 | .is_tuple = false, 23 | } }); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/ztable.zig: -------------------------------------------------------------------------------- 1 | const Type = @import("std").builtin.Type; 2 | 3 | const zimpl = @import("zimpl.zig"); 4 | const Impl = zimpl.Impl; 5 | 6 | pub const CtxAccess = enum { direct, indirect }; 7 | 8 | pub fn VIfc(comptime Ifc: fn (type) type) type { 9 | return struct { 10 | ctx: *anyopaque, 11 | vtable: VTable(Ifc), 12 | 13 | pub fn init( 14 | comptime access: CtxAccess, 15 | ctx: anytype, 16 | impl: Impl(Ifc, CtxType(@TypeOf(ctx), access)), 17 | ) @This() { 18 | return .{ 19 | .ctx = if (access == .indirect) @constCast(ctx) else ctx, 20 | .vtable = vtable(Ifc, access, @TypeOf(ctx), impl), 21 | }; 22 | } 23 | }; 24 | } 25 | 26 | pub fn VTable(comptime Ifc: fn (type) type) type { 27 | const ifc_fields = @typeInfo(Ifc(*anyopaque)).@"struct".fields; 28 | var fields: [ifc_fields.len]Type.StructField = undefined; 29 | var i: usize = 0; 30 | for (ifc_fields) |*field| { 31 | switch (@typeInfo(field.type)) { 32 | .optional => |info| if (@typeInfo(info.child) == .@"fn") { 33 | fields[i] = .{ 34 | .name = field.name, 35 | .type = ?*const info.child, 36 | .default_value_ptr = null, 37 | .is_comptime = false, 38 | .alignment = 0, 39 | }; 40 | i += 1; 41 | }, 42 | .@"fn" => { 43 | fields[i] = .{ 44 | .name = field.name, 45 | .type = *const field.type, 46 | .default_value_ptr = null, 47 | .is_comptime = false, 48 | .alignment = 0, 49 | }; 50 | i += 1; 51 | }, 52 | else => {}, 53 | } 54 | } 55 | return @Type(Type{ .@"struct" = .{ 56 | .layout = .auto, 57 | .fields = fields[0..i], 58 | .decls = &.{}, 59 | .is_tuple = false, 60 | } }); 61 | } 62 | 63 | pub fn vtable( 64 | comptime Ifc: fn (type) type, 65 | comptime access: CtxAccess, 66 | comptime Ctx: type, 67 | comptime impl: Impl(Ifc, CtxType(Ctx, access)), 68 | ) VTable(Ifc) { 69 | var vt: VTable(Ifc) = undefined; 70 | inline for (@typeInfo(VTable(Ifc)).@"struct".fields) |fld_info| { 71 | const impl_func = @field(impl, fld_info.name); 72 | @field(vt, fld_info.name) = switch (@typeInfo(fld_info.type)) { 73 | .optional => |opt| if (impl_func) |func| &virtualize( 74 | @typeInfo(opt.child).pointer.child, 75 | access, 76 | Ctx, 77 | func, 78 | ) else null, 79 | else => &virtualize( 80 | @typeInfo(fld_info.type).pointer.child, 81 | access, 82 | Ctx, 83 | impl_func, 84 | ), 85 | }; 86 | } 87 | return vt; 88 | } 89 | 90 | fn CtxType(comptime Ctx: type, comptime access: CtxAccess) type { 91 | return if (access == .indirect) @typeInfo(Ctx).pointer.child else Ctx; 92 | } 93 | 94 | fn virtualize( 95 | comptime VFn: type, 96 | comptime access: CtxAccess, 97 | comptime Ctx: type, 98 | comptime func: anytype, 99 | ) VFn { 100 | const params = @typeInfo(@TypeOf(func)).@"fn".params; 101 | const return_type = @typeInfo(VFn).@"fn".return_type.?; 102 | 103 | return switch (params.len) { 104 | 0 => func, 105 | 1 => struct { 106 | fn impl(ctx: *anyopaque) return_type { 107 | return func(castCtx(access, Ctx, ctx)); 108 | } 109 | }.impl, 110 | 2 => struct { 111 | fn impl(ctx: *anyopaque, p1: params[1].type.?) return_type { 112 | return func(castCtx(access, Ctx, ctx), p1); 113 | } 114 | }.impl, 115 | 3 => struct { 116 | fn impl( 117 | ctx: *anyopaque, 118 | p1: params[1].type.?, 119 | p2: params[2].type.?, 120 | ) return_type { 121 | return func(castCtx(access, Ctx, ctx), p1, p2); 122 | } 123 | }.impl, 124 | 4 => struct { 125 | fn impl( 126 | ctx: *anyopaque, 127 | p1: params[1].type.?, 128 | p2: params[2].type.?, 129 | p3: params[3].type.?, 130 | ) return_type { 131 | return func(castCtx(access, Ctx, ctx), p1, p2, p3); 132 | } 133 | }.impl, 134 | 5 => struct { 135 | fn impl( 136 | ctx: *anyopaque, 137 | p1: params[1].type.?, 138 | p2: params[2].type.?, 139 | p3: params[3].type.?, 140 | p4: params[4].type.?, 141 | ) return_type { 142 | return func(castCtx(access, Ctx, ctx), p1, p2, p3, p4); 143 | } 144 | }.impl, 145 | 6 => struct { 146 | fn impl( 147 | ctx: *anyopaque, 148 | p1: params[1].type.?, 149 | p2: params[2].type.?, 150 | p3: params[3].type.?, 151 | p4: params[4].type.?, 152 | p5: params[5].type.?, 153 | ) return_type { 154 | return func(castCtx(access, Ctx, ctx), p1, p2, p3, p4, p5); 155 | } 156 | }.impl, 157 | 7 => struct { 158 | fn impl( 159 | ctx: *anyopaque, 160 | p1: params[1].type.?, 161 | p2: params[2].type.?, 162 | p3: params[3].type.?, 163 | p4: params[4].type.?, 164 | p5: params[5].type.?, 165 | p6: params[6].type.?, 166 | ) return_type { 167 | return func(castCtx(access, Ctx, ctx), p1, p2, p3, p4, p5, p6); 168 | } 169 | }.impl, 170 | else => { 171 | @compileError("can't virtualize member function: too many params"); 172 | }, 173 | }; 174 | } 175 | 176 | inline fn castCtx( 177 | comptime access: CtxAccess, 178 | comptime Ctx: type, 179 | ptr: *anyopaque, 180 | ) CtxType(Ctx, access) { 181 | return if (access == .indirect) 182 | @as(*const CtxType(Ctx, access), @alignCast(@ptrCast(ptr))).* 183 | else 184 | @alignCast(@ptrCast(ptr)); 185 | } 186 | -------------------------------------------------------------------------------- /why.md: -------------------------------------------------------------------------------- 1 | # Why Zimpl? 2 | 3 | Suppose that we have a function to poll for events in a `Server` struct 4 | that manages TCP connections. 5 | The caller passes in a generic `handler` argument 6 | to provide event callbacks. 7 | 8 | ```Zig 9 | const Server = struct { 10 | // ... 11 | pub fn poll(self: *@This(), handler: anytype) !void { 12 | try self.pollSockets(); 13 | while (self.getEvent()) |evt| { 14 | switch (evt) { 15 | .open => |handle| handler.onOpen(handle), 16 | .msg => |msg| handler.onMessage(msg.handle, msg.bytes), 17 | .close => |handle| handler.onClose(handle), 18 | } 19 | } 20 | } 21 | // ... 22 | }; 23 | ``` 24 | ```Zig 25 | // using the server library 26 | var server = Server{}; 27 | var handler = MyHandler{}; 28 | try server.listen(8080); 29 | while (true) { 30 | try server.poll(&handler); 31 | } 32 | ``` 33 | 34 | A complaint with the above implementation is that it is not clear from the 35 | function signature of `poll`, or even the full implementation, what the 36 | exact requirements are for the `handler` argument. 37 | 38 | A guaranteed way to make requirements clear is to never rely on duck 39 | typing and have the caller pass in the handler callback functions 40 | directly. 41 | 42 | ```Zig 43 | const Server = struct { 44 | // ... 45 | pub fn poll( 46 | self: *@This(), 47 | handler: anytype, 48 | comptime onOpen: fn (@TypeOf(handler), Handle) void, 49 | comptime onMessage: fn (@TypeOf(handler), Handle, []const u8) void, 50 | comptime onClose: fn (@TypeOf(handler), Handle) void 51 | ) void { 52 | try self.pollSockets(); 53 | while (self.getEvent()) |evt| { 54 | switch (evt) { 55 | .open => |handle| onOpen(handler, handle), 56 | .msg => |msg| onMessage(handler, msg.handle, msg.bytes), 57 | .close => |handle| onClose(handler, handle), 58 | } 59 | } 60 | } 61 | // ... 62 | }; 63 | ``` 64 | ```Zig 65 | // using the server library 66 | var server = Server{}; 67 | var handler = MyHandler{}; 68 | try server.listen(8080); 69 | while (true) { 70 | try server.poll( 71 | &handler, 72 | MyHandler.onOpen, 73 | MyHandler.onMessage, 74 | MyHandler.onClose 75 | ); 76 | } 77 | ``` 78 | 79 | Unfortunately, now the function signature is long and the call 80 | site is verbose. Additionally, if we need to take a `handler` 81 | somewhere else, then the callback parameters 82 | will need to be defined again separately. 83 | 84 | The logical next step would be to create a struct type to hold all of 85 | the callback functions. 86 | 87 | ```Zig 88 | const Server = struct { 89 | // ... 90 | pub fn Handler(comptime T: type) type { 91 | return struct { 92 | onOpen: fn (T, Handle) void, 93 | onMessage: fn (T, Handle, []const u8) void, 94 | onClose: fn (T, Handle) void, 95 | }; 96 | } 97 | 98 | pub fn poll( 99 | self: *Self, 100 | handler_ctx: anytype, 101 | handler_impl: Handler(@TypeOf(handler_ctx)), 102 | ) void { 103 | try self.pollSockets(); 104 | while (self.getEvent()) |evt| { 105 | switch (evt) { 106 | .open => |handle| handler_impl.onOpen(handler_ctx, handle), 107 | .msg => |msg| handler_impl.onMessage(handler_ctx, msg.handle, msg.bytes), 108 | .close => |handle| handler_impl.onClose(handler_ctx, handle), 109 | } 110 | } 111 | } 112 | // ... 113 | }; 114 | ``` 115 | 116 | This cleaned up the function signature, but calling `poll` is still 117 | needlessly verbose when the type already 118 | has valid callback member functions. 119 | 120 | ```Zig 121 | // using the server library 122 | var server = Server{}; 123 | var handler = MyHandler{}; 124 | try server.listen(8080); 125 | while (true) { 126 | try server.poll(&handler, .{ 127 | .onOpen = MyHandler.onOpen, 128 | .onMessage = MyHandler.onMessage, 129 | .onClose = MyHandler.onClose, 130 | }); 131 | } 132 | ``` 133 | 134 | The Zimpl library provides the function `Impl` that infers the default 135 | value for each member of `Handler(T)` from the declarations of `T`. 136 | 137 | ```Zig 138 | const Server = struct { 139 | // ... 140 | pub fn poll( 141 | self: *Self, 142 | handler_ctx: anytype, 143 | handler_impl: Impl(Handler, @TypeOf(handler_ctx)) 144 | ) void { 145 | try self.pollSockets(); 146 | while (self.getEvent()) |evt| { 147 | switch (evt) { 148 | .open => |handle| handler_impl.onOpen(handler_ctx, handle), 149 | .msg => |msg| handler_impl.onMessage(handler_ctx, msg.handle, msg.bytes), 150 | .close => |handle| handler_impl.onClose(handler_ctx, handle), 151 | } 152 | } 153 | } 154 | // ... 155 | ``` 156 | ```Zig 157 | // using the server library 158 | var server = Server{}; 159 | var handler = MyHandler{}; 160 | try server.listen(8080); 161 | while (true) { 162 | // Impl(Handler, *MyHandler) can be default constructed because MyHandler 163 | // has onOpen, onMessage, and onClose member functions 164 | try server.poll(&handler, .{}); 165 | } 166 | ``` 167 | 168 | For a longer discussion on the above example see [this article][1]. 169 | Go to the [Zimpl][2] page for more examples. 170 | 171 | [1]: https://musing.permutationlock.com/posts/blog-working_with_anytype.html 172 | [2]: https://github.com/permutationlock/zimpl 173 | --------------------------------------------------------------------------------