From f96df2aed73d15cff5861f052175c1aee7f8f9de Mon Sep 17 00:00:00 2001 From: sirlilpanda Date: Fri, 29 May 2026 09:13:38 +1200 Subject: [PATCH] init --- .gitignore | 2 + README.md | 158 ++++++++++++++++++++++++++++++++++ build.zig | 56 ++++++++++++ build.zig.zon | 81 ++++++++++++++++++ src/main.zig | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 149 ++++++++++++++++++++++++++++++++ 6 files changed, 677 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/main.zig create mode 100644 src/root.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ea71a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/ +zig-out/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae2c636 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# zig interface helper + +first why does this exist i didnt like having to write `*anyopaque` for all the functions that are in the implemention of an interface so i made wrapper do it for you. This now hides all the nasty polymorphism shenanigans within the interface where it belongs. +Additionally it also allows you to use your implementation normally rather than having to pass it through the interface first. + +# how does this lib work + +This lib basically removes the annoying conversion back to the original type in your implementation and instead does that conversion in a wrapper functions instead. This should now allow you to write more code and less boilerplate. + +In essence all this lib does is +```zig +... +somefn : *const fn(*anyopaque, u8, []const u8) !?f32 +... +``` +to using (`VtableFn`) +```zig +... +somefn : *const fn(struct {*anyopaque, u8, []const u8}) !?f32 +... +``` + +and your implementation functions from + +```zig +... +pub fn area(self : Self, times : f32) f32 { + return self.h * self.w * times; +} +... +``` +to using (`ToVtableFn`) +```zig +pub fn areaWrapper(args : struct {*anyopaque, f32}) f32 { + const self : orignal_type = @ptrCast(@alignCast(args[0])); + + return @call(.auto, area, { + self, + args[1], + }); +} +``` + + +# example + +```zig + +const Iface = @import("zig_interface_testing"); + + +// implemention +const Circle = struct { + const Self = @This(); + radius: f32, + + pub fn init(r: f32) Self { + return Self{ + .radius = r, + }; + } + + pub fn perimeter(self: Self) f32 { + return std.math.pi * self.radius * 2; + } + + pub fn area(self: Self) f32 { + return std.math.pi * self.radius * self.radius; + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + try writer.print("Circle : {}", .{self.radius}); + } +}; + +// implemention +const Rect = struct { + const Self = @This(); + + w: f32, + h: f32, + + pub fn init(w: f32, h: f32) Self { + return Self{ + .w = w, + .h = h, + }; + } + + pub fn perimeter(self: Rect) f32 { + return self.h * 2 + self.w * 2; + } + + pub fn area(self: Self) f32 { + return self.h * self.w; + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + try writer.print("sqaure : {}, {}", .{ self.h, self.w }); + } +}; + +// interface +const Shape = struct { + const Self = @This(); + data: *anyopaque, + vtable: *const Vtable, + + // create your vtable + const Vtable = struct { + // convert your vtable functions with Iface.VtableFn + perimeter_fn: *const Iface.VtableFn(fn (*anyopaque) f32), // -> *const fn (tuple) returnType + area_fn: *const Iface.VtableFn(fn (*anyopaque) f32), + format_fn: *const Iface.VtableFn(fn (self: *anyopaque, writer: *std.Io.Writer) anyerror!void), + }; + + pub fn init(shape: anytype) Self { + std.debug.print("shape : {*}\n", .{shape}); + const self = Self{ + .data = @ptrCast(shape), + .vtable = &Vtable{ + // convert your implemation functions to that of the vtable + .perimeter_fn = Iface.ToVtableFn(@field(@TypeOf(shape.*), "perimeter")), + .area_fn = Iface.ToVtableFn(@field(@TypeOf(shape.*), "area")), + .format_fn = Iface.ToVtableFn(@field(@TypeOf(shape.*), "format")), + }, + }; + + return self; + } + + + pub fn perimeter(self: Self) f32 { + // run them functions + return self.vtable.perimeter_fn(.{self.data}); + } + + pub fn area(self: Self) f32 { + return self.vtable.area_fn(.{self.data}); + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + self.vtable.format_fn(.{ self.data, writer }) catch { + return std.Io.Writer.Error.WriteFailed; + }; + } +}; + +``` \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..7ed4cc4 --- /dev/null +++ b/build.zig @@ -0,0 +1,56 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.addModule( + "interface", + .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }, + ); + + // const exe = b.addExecutable(.{ + // .name = "zig_interface_testing", + // .root_module = b.createModule(.{ + // .root_source_file = b.path("src/main.zig"), + // .target = target, + // .optimize = optimize, + // .imports = &.{ + // .{ .name = "zig_interface_testing", .module = mod }, + // }, + // }), + // }); + + // b.installArtifact(exe); + + // const run_step = b.step("run", "Run the app"); + + // const run_cmd = b.addRunArtifact(exe); + // run_step.dependOn(&run_cmd.step); + + // run_cmd.step.dependOn(b.getInstallStep()); + + // if (b.args) |args| { + // run_cmd.addArgs(args); + // } + + // const mod_tests = b.addTest(.{ + // .root_module = mod, + // }); + + // const run_mod_tests = b.addRunArtifact(mod_tests); + + // const exe_tests = b.addTest(.{ + // .root_module = exe.root_module, + // }); + + // const run_exe_tests = b.addRunArtifact(exe_tests); + + // const test_step = b.step("test", "Run tests"); + // test_step.dependOn(&run_mod_tests.step); + // test_step.dependOn(&run_exe_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..48e084b --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,81 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .zig_interface_testing, + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0xea4a93eaf5dec5a2, // Changing this has security and trust implications. + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.16.0", + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..f480de4 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,231 @@ +const std = @import("std"); +const Io = std.Io; +const Iface = @import("zig_interface_testing"); + +const HelperIMPL = struct { + const Circle = struct { + const Self = @This(); + radius: f32, + + pub fn init(r: f32) Self { + return Self{ + .radius = r, + }; + } + + pub fn perimeter(self: Self) f32 { + return std.math.pi * self.radius * 2; + } + + pub fn area(self: Self) f32 { + return std.math.pi * self.radius * self.radius; + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + try writer.print("Circle : {}", .{self.radius}); + } + }; + + const Rect = struct { + const Self = @This(); + + w: f32, + h: f32, + + pub fn init(w: f32, h: f32) Self { + return Self{ + .w = w, + .h = h, + }; + } + + pub fn perimeter(self: Rect) f32 { + return self.h * 2 + self.w * 2; + } + + pub fn area(self: Self) f32 { + return self.h * self.w; + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + try writer.print("sqaure : {}, {}", .{ self.h, self.w }); + } + }; + + const Shape = struct { + const Self = @This(); + data: *anyopaque, + vtable: *const Vtable, + + const Vtable = struct { + perimeter_fn: *const Iface.VtableFn(fn (*anyopaque) f32), // -> *const fn (tuple) returnType + area_fn: *const Iface.VtableFn(fn (*anyopaque) f32), + format_fn: *const Iface.VtableFn(fn (self: *anyopaque, writer: *std.Io.Writer) anyerror!void), + }; + + pub fn init(shape: anytype) Self { + std.debug.print("shape : {*}\n", .{shape}); + const self = Self{ + .data = @ptrCast(shape), + .vtable = &Vtable{ + .perimeter_fn = Iface.ToVtableFn(@field(@TypeOf(shape.*), "perimeter")), + .area_fn = Iface.ToVtableFn(@field(@TypeOf(shape.*), "area")), + .format_fn = Iface.ToVtableFn(@field(@TypeOf(shape.*), "format")), + }, + }; + + return self; + } + + pub fn perimeter(self: Self) f32 { + return self.vtable.perimeter_fn(.{self.data}); + } + + pub fn area(self: Self) f32 { + return self.vtable.area_fn(.{self.data}); + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + self.vtable.format_fn(.{ self.data, writer }) catch { + return std.Io.Writer.Error.WriteFailed; + }; + } + }; +}; + +const NormalIMPL = struct { + const Circle = struct { + const Self = @This(); + radius: f32, + + pub fn init(r: f32) Self { + return Self{ + .radius = r, + }; + } + + pub fn perimeter(self: *anyopaque) f32 { + const cric: *Self = @ptrCast(@alignCast(self)); + return std.math.pi * cric.radius * 2; + } + + pub fn area(self: *anyopaque) f32 { + const cric: *Self = @ptrCast(@alignCast(self)); + return std.math.pi * cric.radius * cric.radius; + } + + pub fn format( + self: *anyopaque, + writer: *std.Io.Writer, + ) !void { + const cric: *Self = @ptrCast(@alignCast(self)); + try writer.print("Circle : {}", .{cric.radius}); + } + }; + + const Rect = struct { + const Self = @This(); + + w: f32, + h: f32, + + pub fn init(w: f32, h: f32) Self { + return Self{ + .w = w, + .h = h, + }; + } + + pub fn perimeter(self: *anyopaque) f32 { + const rect: *Self = @ptrCast(@alignCast(self)); + return rect.h * 2 + rect.w * 2; + } + + pub fn area(self: *anyopaque) f32 { + const rect: *Self = @ptrCast(@alignCast(self)); + + return rect.h * rect.w; + } + + pub fn format( + self: *anyopaque, + writer: *std.Io.Writer, + ) !void { + const rect: *Self = @ptrCast(@alignCast(self)); + try writer.print("sqaure : {}, {}", .{ rect.h, rect.w }); + } + }; + + const Shape = struct { + const Self = @This(); + data: *anyopaque, + vtable: *const Vtable, + + const Vtable = struct { + perimeter_fn: *const fn (*anyopaque) f32, // -> *const fn (tuple) returnType + area_fn: *const fn (*anyopaque) f32, + format_fn: *const fn (self: *anyopaque, writer: *std.Io.Writer) anyerror!void, + }; + + pub fn init(shape: anytype) Self { + std.debug.print("shape : {*}\n", .{shape}); + const self = Self{ + .data = @ptrCast(shape), + .vtable = &Vtable{ + .perimeter_fn = @field(@TypeOf(shape.*), "perimeter"), + .area_fn = @field(@TypeOf(shape.*), "area"), + .format_fn = @field(@TypeOf(shape.*), "format"), + }, + }; + + return self; + } + + pub fn perimeter(self: Self) f32 { + return self.vtable.perimeter_fn(self.data); + } + + pub fn area(self: Self) f32 { + return self.vtable.area_fn(self.data); + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + self.vtable.format_fn(self.data, writer) catch { + return std.Io.Writer.Error.WriteFailed; + }; + } + }; +}; + +pub fn main(init: std.process.Init) !void { + _ = init; + var rect = HelperIMPL.Rect{ + .h = 5, + .w = 10, + }; + + var circle = HelperIMPL.Circle{ + .radius = 10, + }; + + var rect_shape = HelperIMPL.Shape.init(&rect); + var circle_shape = HelperIMPL.Shape.init(&circle); + + std.debug.print("rect : area {}, prim : {}, {any} {*}.\n", .{ rect.area(), rect.perimeter(), rect, &rect }); + std.debug.print("rect_shape : area {}, prim : {}, {f}.\n", .{ rect_shape.area(), rect_shape.perimeter(), rect_shape }); + + std.debug.print("circle : area {}, prim : {}, {any} {*}.\n", .{ circle.area(), circle.perimeter(), circle, &circle }); + std.debug.print("circle_shape : area {}, prim : {}, {f}.\n", .{ circle_shape.area(), circle_shape.perimeter(), circle_shape }); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..1df4fc6 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,149 @@ +//! This a is a meta library that implements 2 functions: +//! - VtableFn which simply converts a given function like `fn(arg1: u32, arg2: u8) f32` -> `fn(args : struct{u32, u8}) f32` +//! - ToVtableFn which wraps the passed function to be the form of VtableFn +//! using these you can implement interface with less boilerplate +//! +//! +//! an example can be seen below: +//! ```zig +//! +//! const A = struct { +//! f: u8, +//! +//! pub fn z(a: A) u8 { +//! return a + 2; +//! } +//! }; +//! +//! const B = struct { +//! f: u8, +//! +//! pub fn z(b: B) u8 { +//! return b + 3; +//! } +//! }; +//! +//! const C = struct { +//! data: *anyopaque, +//! vtable: *const VTable, +//! +//! const VTable = struct { +//! z_fn: *const VtableFn(fn (*anyopaque) u8), +//! }; +//! +//! pub fn init(letter: anytype) C { +//! return C{ +//! .data = @ptrCast(letter), +//! .vtable = VTable{ +//! .z_fn = ToVtableFn(@field(@TypeOf(letter.*), "z")), +//! }, +//! }; +//! } +//! +//! pub fn z(c: C) u8 { +//! return c.vtable.z_fn(.{c.data}); +//! } +//! }; +//! +//! +//! fn usage() void { +//! var a = A{ +//! .f = 2; +//! } +//! +//! var b = B{ +//! .f = 2; +//! } +//! // yes this has to be a pointer +//! var c : C = C.init(&a); +//! _ = c.z(); +//! } +//! +//! ``` +//! +//! both structs A & B implementing a function z +//! C is an interface for A & B that has some pointer to the data (data) and a vtable +//! to hold the function pointers. Within the vtable you call VtableFn on the function +//! signature you plan to implement. +//! within the init function, first the given pointer to the letter is cast to the anyopaque +//! yes this value must be passed in to the function as a pointer if you dont want to deal with @constcast +//! next the `z` function is gotten from the namespace of the letter `@field(@TypeOf(letter.*), "z")` +//! this gotten `z` function is now passed to `ToVtableFn` which returns the wrapped function. +//! +//! finally to call this function use the signature `{interface}.vtable.{function_name}(.{function args})` +//! where the all args for the function must be passed through as a tuple. +//! +//! +//! how `@field(@TypeOf(letter.*), "z")` works: +//! - `letter.*` gets the plain type not the pointer to it +//! - `@TypeOf(_)` get the type that the defernced letter is +//! - `@field(_. "z")` finally gets the function (no the pointer to the function the function itself) +//! +const std = @import("std"); + +/// all this function +pub fn VtableFn(function: anytype) type { + return @Fn( + &.{std.meta.ArgsTuple(function)}, + &.{.{}}, + @typeInfo(function).@"fn".return_type orelse void, + .{ + .@"callconv" = @typeInfo(function).@"fn".calling_convention, + .varargs = @typeInfo(function).@"fn".is_var_args, + }, + ); +} + +fn VtableFnArgs(function: anytype) []const type { + var new_tuple_type: []const type = &.{ + *anyopaque, + }; + + // @compileLog(@TypeOf(function)); + + inline for (@typeInfo(std.meta.ArgsTuple(function)).@"struct".fields, 0..) |field, i| { + if (i == 0) continue; // skip the first one + new_tuple_type = new_tuple_type ++ .{field.type}; + } + + return new_tuple_type; +} + +fn ToVtableFnType(function: anytype) type { + // still have to swap all args to *anyopaque + + return @Fn( + &.{@Tuple(VtableFnArgs(function))}, + &.{.{}}, + @typeInfo(function).@"fn".return_type orelse void, + .{ + .@"callconv" = @typeInfo(function).@"fn".calling_convention, + .varargs = @typeInfo(function).@"fn".is_var_args, + }, + ); +} + +/// wraps a given function to be in the form of `fn(tuple) return type` where the tuple will always have the first field being of type `*anyopaque` +/// this wrapper internally then converts the type back to that as in the orignal function and calls it +pub fn ToVtableFn(comptime function: anytype) fn (@Tuple(VtableFnArgs(@TypeOf(function)))) (@typeInfo(@TypeOf(function)).@"fn".return_type orelse void) { + // @compileLog(@Tuple(VtableFnArgs(@TypeOf(function)))); + + return struct { // | these args are the ones that replace the @this with *anyopaque + pub fn functionWrapper(args: @Tuple(VtableFnArgs(@TypeOf(function)))) (@typeInfo(@TypeOf(function)).@"fn".return_type orelse void) { + const orignal_type = @typeInfo(@TypeOf(function)).@"fn".params[0].type orelse @compileError("function need at least one type"); + + var type_casted_args: std.meta.ArgsTuple(@TypeOf(function)) = undefined; + + // yeah i have to do this as args isnt a pointer according to zig (even though it is) + type_casted_args[0] = @as(**orignal_type, @ptrCast(@alignCast(@constCast(&args[0])))).*.*; + + inline for (args, 0..) |arg, i| { + if (i == 0) continue; // skip the first one + type_casted_args[i] = arg; + } + + // | these args are the ones where its converted back to the original type + return @call(.auto, function, type_casted_args); + } + }.functionWrapper; +}