├── .gitattributes ├── .github ├── assets │ └── field_parent_ptr.svg └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── data └── multi_array_list.zig └── typing ├── distinct_integers.zig ├── field_parent_ptr.zig ├── inline_switch.zig ├── type_function.zig └── vtable.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text=auto eol=lf 3 | *.zon text=auto eol=lf -------------------------------------------------------------------------------- /.github/assets/field_parent_ptr.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - ".github/workflows/main.yml" 7 | - "**.zig" 8 | - "build.zig.zon" 9 | pull_request: 10 | paths: 11 | - ".github/workflows/main.yml" 12 | - "**.zig" 13 | - "build.zig.zon" 14 | schedule: 15 | - cron: "0 0 * * *" 16 | workflow_dispatch: 17 | 18 | jobs: 19 | build: 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | - uses: goto-bus-stop/setup-zig@v2 29 | with: 30 | version: master 31 | 32 | - name: Get Zig version 33 | id: zig_version 34 | run: echo "zig_version=$(zig version)" >> $GITHUB_OUTPUT 35 | 36 | - run: zig env 37 | 38 | - name: Run zig fmt 39 | run: zig fmt --check . 40 | 41 | - name: Run tests 42 | run: zig build all 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Auguste Rame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig Patterns 2 | 3 | - [Zig Patterns](#zig-patterns) 4 | - [About](#about) 5 | - [The Patterns](#the-patterns) 6 | - [License](#license) 7 | 8 | ## About 9 | 10 | This repository contains examples of common patterns found in Zig's standard library and many community projects. 11 | 12 | Note that copying (one of) these patterns verbatim into your project may not be useful; it's important to weigh the pros and cons of different patterns. If you come from another language, especially a non-systems one, you'll often find that the more unfamiliar ideas featured here may be more useful. 13 | 14 | For example, I've seldom used interface-like patterns when designing systems in Zig, despite using such patterns dozens of times a day at my old job writing TypeScript or Go. 15 | 16 | ## The Patterns 17 | 18 | All examples are annotated with comments including recommendations regarding when to use the patterns shown. 19 | 20 | Example tests can be run with `zig build [name_of_pattern]`. For example, to run the tests in `typing/type_function.zig`, run `zig build type_function`. 21 | 22 | Patterns are grouped into the following categories: 23 | - `data` - data layout and organization techniques 24 | - `typing` - `comptime`, runtime, or mixed `type` or type safety techniques 25 | 26 | Some patterns may belong in multiple categories; I've selected the most fitting one in my opinion. 27 | 28 | ## License 29 | 30 | MIT 31 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const examples = .{ 4 | .{ "field_parent_ptr", "src/typing/field_parent_ptr.zig" }, 5 | .{ "inline_switch", "src/typing/inline_switch.zig" }, 6 | .{ "multi_array_list", "src/data/multi_array_list.zig" }, 7 | .{ "type_function", "src/typing/type_function.zig" }, 8 | .{ "vtable", "src/typing/vtable.zig" }, 9 | .{ "distinct_integers", "src/typing/distinct_integers.zig" }, 10 | }; 11 | 12 | pub fn build(b: *std.Build) void { 13 | const target = b.standardTargetOptions(.{}); 14 | const optimize = b.standardOptimizeOption(.{}); 15 | 16 | // Not really useful, moreso for CI 17 | const run_all = b.step("all", "Run all unit tests"); 18 | 19 | inline for (examples) |example| { 20 | const step_name, const source_file = example; 21 | 22 | const @"test" = b.addTest(.{ 23 | .root_source_file = .{ .path = source_file }, 24 | .target = target, 25 | .optimize = optimize, 26 | }); 27 | 28 | const run_test = b.addRunArtifact(@"test"); 29 | run_test.has_side_effects = true; 30 | 31 | const test_step = b.step(step_name, "Run unit tests for " ++ step_name); 32 | test_step.dependOn(&run_test.step); 33 | run_all.dependOn(&run_test.step); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "zig-design-patterns", 3 | .version = "0.0.0", 4 | .paths = .{""}, 5 | } 6 | -------------------------------------------------------------------------------- /src/data/multi_array_list.zig: -------------------------------------------------------------------------------- 1 | //! Let's say that we want to store a list of `struct {a: u8, b: u16}`s. 2 | //! 3 | //! We could use a normal ArrayList; we'd call this an Array of Structs (AoS) design, 4 | //! though in Zig "Slice of Structs" would be a more correct name, because we'd be 5 | //! storing a list where each entry in the list stores the whole struct contiguously. 6 | //! 7 | //! Alternatively, we could use MultiArrayList, where the list is split into 8 | //! `Struct.fields.len` segments, each storing all the values of a certain field 9 | //! contiguously. We'd call this a Struct of Arrays (SoA) design, which is 10 | //! also a misnomer when applied to Zig. :^) 11 | //! 12 | //! This is a little odd to wrap one's head around at first, so here's a diagram 13 | //! of how each design looks in memory with the example struct mentioned above: 14 | //! 15 | //! ArrayList (AoS): { {aaaaaaaabbbbbbbbbbbbbbbb} {aaaaaaaabbbbbbbbbbbbbbbb} {aaaaaaaabbbbbbbbbbbbbbbb} } 16 | //! MultiArrayList (SoA): { {aaaaaaaa} {aaaaaaaa} {aaaaaaaa} {bbbbbbbbbbbbbbbb} {bbbbbbbbbbbbbbbb} {bbbbbbbbbbbbbbbb} } 17 | //! 18 | //! Both example lists show three entries and each letter represents a bit of memory for 19 | //! that field. Note that this diagram is simplified, and that we do not represent 20 | //! any padding that may be introduced by the compiler. 21 | //! 22 | //! When to use: 23 | //! - You only access certain fields most of the time, and loading 24 | //! the entire struct would be a waste 25 | //! - You want to help the compiler autovectorize / perform some 26 | //! manual vectorization using Zig's SIMD features 27 | //! 28 | //! When not to use: 29 | //! - If you're always accessing every field in your list entries, 30 | //! then you won't find much use in MultiArrayLists 31 | 32 | const std = @import("std"); 33 | 34 | pub const Player = struct { 35 | name: []const u8, 36 | health: u16, 37 | }; 38 | 39 | const PlayerList = std.MultiArrayList(Player); 40 | 41 | test { 42 | const allocator = std.testing.allocator; 43 | 44 | var players = PlayerList{}; 45 | defer players.deinit(allocator); 46 | 47 | try players.append(allocator, .{ .name = "Auguste", .health = 100 }); 48 | try players.append(allocator, .{ .name = "Techatrix", .health = 125 }); 49 | try players.append(allocator, .{ .name = "ZLS Premium User", .health = 50_000 }); 50 | 51 | // We're about to call `.items(...)` a bunch, 52 | // so we can save some performance by 53 | // calling `.slice()` and calling `.items(...)` 54 | // on that instead. 55 | var slice = players.slice(); 56 | 57 | // I just want to increase every player's health! 58 | // This pattern allows LLVM to easily autovectorize 59 | // this modification code; I challenge you to look 60 | // at this in Compiler Explorer! :) 61 | for (slice.items(.health)) |*health| { 62 | health.* += 10; 63 | } 64 | 65 | // Let's access both fields and see where our players' 66 | // health is at now! 67 | for (slice.items(.name), slice.items(.health)) |name, health| { 68 | std.debug.print("{s} has {d} health!\n", .{ name, health }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/typing/distinct_integers.zig: -------------------------------------------------------------------------------- 1 | //! When designing systems in Zig that take indices, it's important to 2 | //! ensure indices are not accidentally mixed up. For example, if I have 3 | //! a list `A` of length 10 and a list `B` of length 20, it is safe to 4 | //! access `A` with any number representing an index into `A`, and `B` 5 | //! with any number representing an index into `B`, but if we were to, 6 | //! for example, access `A` with a number representing an index into `B`, 7 | //! we could access garbage information or cause an out of bounds accesss. 8 | //! 9 | //! A real world example I've run into while working on ZLS is accidentally 10 | //! swapping `TokenIndex`s and `Ast.Node.Index`s, which are both `= u32`. 11 | //! 12 | //! ```zig 13 | //! pub fn accessANodeNotDistinct(node_index: u32) void { 14 | //! // ... 15 | //! } 16 | //! 17 | //! const token_index_not_a_node: u32 = 10; 18 | //! accessANodeNotDistinct(token_index_not_a_node); 19 | //! 20 | //! // kabloom! 21 | //! ``` 22 | //! 23 | //! How can we avoid this? Distinct integers! 24 | 25 | const std = @import("std"); 26 | 27 | pub const TokenIndex = enum(u32) { _ }; 28 | pub const NodeIndex = enum(u32) { _ }; 29 | 30 | pub fn lastToken() TokenIndex { 31 | return @enumFromInt(0); 32 | } 33 | 34 | pub fn lastNode() NodeIndex { 35 | return @enumFromInt(0); 36 | } 37 | 38 | pub fn accessANodeDistinct(node_index: NodeIndex) void { 39 | // do node things 40 | _ = node_index; 41 | } 42 | 43 | test { 44 | accessANodeDistinct(lastNode()); 45 | // uncomment the line below this one and the program won't compile: 46 | // accessANodeDistinct(lastToken()); 47 | } 48 | -------------------------------------------------------------------------------- /src/typing/field_parent_ptr.zig: -------------------------------------------------------------------------------- 1 | //! See ![this diagram](../.github/assets/field_parent_ptr.svg) 2 | //! 3 | //! When to use: 4 | //! - When you want to call one function with a unified interface 5 | //! whose implementers share common fields. 6 | //! 7 | //! When not to use: 8 | //! - If you have a lot of common functions and few / no common fields 9 | //! - You could use a vtable. 10 | //! - You could use an `inline switch`. 11 | //! - If you want type correctness guarantees at compile time, use 12 | //! a type function instead. 13 | 14 | const std = @import("std"); 15 | 16 | pub const Animal = struct { 17 | name: []const u8, 18 | sayHello: *const fn (animal: *const Animal) void, 19 | }; 20 | 21 | pub const Cat = struct { 22 | animal: Animal, 23 | 24 | favorite_food_brand: []const u8, 25 | 26 | pub fn sayHello(animal: *const Animal) void { 27 | const cat: *const Cat = @fieldParentPtr("animal", animal); 28 | std.debug.print("Hi my name is {s} and I only eat {s}\n", .{ animal.name, cat.favorite_food_brand }); 29 | } 30 | }; 31 | 32 | pub const Dog = struct { 33 | animal: Animal, 34 | 35 | favorite_dog_toy: []const u8, 36 | 37 | pub fn sayHello(animal: *const Animal) void { 38 | const dog: *const Dog = @fieldParentPtr("animal", animal); 39 | std.debug.print("Hi my name is {s} and my favorite dog toy is {s}\n", .{ animal.name, dog.favorite_dog_toy }); 40 | } 41 | }; 42 | 43 | test { 44 | const grisounette = Cat{ 45 | .animal = .{ 46 | .name = "Grisounette", 47 | .sayHello = &Cat.sayHello, 48 | }, 49 | .favorite_food_brand = "grass", 50 | }; 51 | const peanuts = Dog{ 52 | .animal = Animal{ 53 | .name = "Peanuts", 54 | .sayHello = &Dog.sayHello, 55 | }, 56 | .favorite_dog_toy = "bone", 57 | }; 58 | 59 | const my_animals = &[_]*const Animal{ &grisounette.animal, &peanuts.animal }; 60 | 61 | for (my_animals) |animal| { 62 | animal.sayHello(animal); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/typing/inline_switch.zig: -------------------------------------------------------------------------------- 1 | //! When to use: 2 | //! - When you don't want runtime overhead. 3 | //! - When you want reasonably strong type correctness guarantees. 4 | //! - This avoids most runtime errors potentially found in other techniques. 5 | //! 6 | //! When not to use: 7 | //! - If you want extensibility! This solution is not extensible in any way, i.e. 8 | //! a user would have to modify the structure directly. 9 | 10 | const std = @import("std"); 11 | 12 | pub const Player = union(enum) { 13 | pub const Crewmate = struct { 14 | pub fn speak(crewmate: Crewmate) void { 15 | _ = crewmate; 16 | 17 | std.debug.print("Red is sus!\n", .{}); 18 | } 19 | 20 | pub fn doTask(crewmate: Crewmate) !void { 21 | _ = crewmate; 22 | 23 | std.debug.print("Crewmate did task!\n", .{}); 24 | } 25 | 26 | pub fn doMurder(crewmate: Crewmate) !void { 27 | _ = crewmate; 28 | 29 | return error.IllegalAction; 30 | } 31 | }; 32 | 33 | pub const Impostor = struct { 34 | pub fn speak(impostor: Impostor) void { 35 | _ = impostor; 36 | 37 | std.debug.print("Green is sus!\n", .{}); 38 | } 39 | 40 | pub fn doTask(impostor: Impostor) !void { 41 | _ = impostor; 42 | 43 | return error.IllegalAction; 44 | } 45 | 46 | pub fn doMurder(impostor: Impostor) !void { 47 | _ = impostor; 48 | 49 | std.debug.print("Impostor did murder!\n", .{}); 50 | } 51 | }; 52 | 53 | crewmate: Crewmate, 54 | impostor: Impostor, 55 | 56 | pub fn speak(player: Player) void { 57 | switch (player) { 58 | inline else => |p| p.speak(), 59 | } 60 | } 61 | 62 | pub fn doTask(player: Player) !void { 63 | return switch (player) { 64 | inline else => |p| p.doTask(), 65 | }; 66 | } 67 | 68 | pub fn doMurder(player: Player) !void { 69 | return switch (player) { 70 | inline else => |p| p.doMurder(), 71 | }; 72 | } 73 | }; 74 | 75 | test { 76 | const crewmate = Player{ .crewmate = .{} }; 77 | const impostor = Player{ .impostor = .{} }; 78 | 79 | crewmate.speak(); 80 | impostor.speak(); 81 | 82 | try std.testing.expectError(error.IllegalAction, crewmate.doMurder()); 83 | try impostor.doMurder(); 84 | 85 | try crewmate.doTask(); 86 | try std.testing.expectError(error.IllegalAction, impostor.doTask()); 87 | } 88 | -------------------------------------------------------------------------------- /src/typing/type_function.zig: -------------------------------------------------------------------------------- 1 | //! In Zig, types are first class values, which means you can 2 | //! manipulate types like any other value at compile time. 3 | //! 4 | //! This fact is commonly used in Zig to create type functions (or as I like 5 | //! to call them, typefuncs), which are functions that take in compile 6 | //! time values and return a type. 7 | //! 8 | //! Here, we create an all-encompassing type function, Contribution, 9 | //! that given a summarize function and associated types, will call that 10 | //! function with additional logic. 11 | //! 12 | //! Each call to Contribution with unique parameters returns 13 | //! a unique type. 14 | //! 15 | //! When to use: 16 | //! - When you don't want runtime overhead. 17 | //! - When you want incredibly strong type correctness guarantees. 18 | //! - This avoids most runtime errors potentially found in other techniques. 19 | //! - You can also use @compileError to make certain behaviors illegal 20 | //! 100% at compile time. 21 | //! - When you want your structure to be easily extended from Zig code. 22 | //! 23 | //! When not to use: 24 | //! - If you want to store all instances in a variable of a single type 25 | //! (there is no single type like in the vtable or @fieldParentPtr examples, 26 | //! so this is not possible). 27 | 28 | const std = @import("std"); 29 | 30 | pub fn Contribution( 31 | comptime Context: type, 32 | comptime Error: type, 33 | comptime summarizeFn: fn (context: Context, file: std.fs.File) Error!void, 34 | ) type { 35 | return struct { 36 | context: Context, 37 | 38 | pub fn summarize(contribution: @This()) Error!void { 39 | const file = std.io.getStdErr(); 40 | try file.writer().writeAll("Contribution: "); 41 | return summarizeFn(contribution.context, file); 42 | } 43 | }; 44 | } 45 | 46 | pub const Issue = struct { 47 | is_proposal: bool, 48 | 49 | const Error = std.fs.File.WriteError; 50 | pub fn summarize(issue: Issue, file: std.fs.File) Error!void { 51 | if (issue.is_proposal) { 52 | try file.writer().writeAll("We don't accept proposals!!!\n"); 53 | } else { 54 | try file.writer().writeAll("Oh cool, an issue :)\n"); 55 | } 56 | } 57 | 58 | const C = Contribution(Issue, Error, summarize); 59 | pub fn contribution(issue: Issue) Contribution(Issue, Error, summarize) { 60 | return .{ .context = issue }; 61 | } 62 | }; 63 | 64 | pub const PullRequest = struct { 65 | add_count: usize, 66 | delete_count: usize, 67 | 68 | const Error = std.fs.File.WriteError; 69 | pub fn summarize(pull_request: PullRequest, file: std.fs.File) Error!void { 70 | try file.writer().print("This PR adds {d} and deletes {d}\n", .{ 71 | pull_request.add_count, 72 | pull_request.delete_count, 73 | }); 74 | } 75 | 76 | const C = Contribution(PullRequest, Error, summarize); 77 | pub fn contribution(pull_request: PullRequest) C { 78 | return .{ .context = pull_request }; 79 | } 80 | }; 81 | 82 | /// To accept any value of a type created by Contribution, we cannot 83 | /// simply specify a single type due to Contribution returning unique 84 | /// types for unique inputs. 85 | /// 86 | /// Zig's solution to this is `anytype`, which allows you 87 | /// to ducktype. As Contribution(X, Y, Z) returns a type 88 | /// with a "summarize" function regardless of the inputs, 89 | /// so we can simply call summarize on our `contrib` parameter. 90 | /// 91 | /// As we call summarizeContrib with both a value of type `Issue.C` 92 | /// and `PullRequest.C`, Zig will create two instances of `summarizeContrib`: 93 | /// `summarizeContrib_Issue_C(contrib: Issue.C) Issue.Error!void` and 94 | /// `summarizeContrib_PullRequest_C(contrib: PullRequest.C) PullRequest.Error!void` 95 | /// 96 | /// This adds binary size overhead, especially if you have a lot of `summarizeContrib` 97 | /// calls with different input types, but it removes all runtime overhead relating 98 | /// to virtual calls, which many of the polymorphic examples in this repo use, 99 | /// as Zig will simply replace the `summarizeContrib` call with the correct typed call 100 | /// at compile time. 101 | pub fn summarizeContrib(contrib: anytype) !void { 102 | try contrib.summarize(); 103 | } 104 | 105 | test { 106 | var issue = Issue{ .is_proposal = true }; 107 | const issue_contrib = issue.contribution(); 108 | 109 | var pull_request = PullRequest{ .add_count = 25, .delete_count = 50 }; 110 | const pull_request_contrib = pull_request.contribution(); 111 | 112 | try summarizeContrib(issue_contrib); 113 | try summarizeContrib(pull_request_contrib); 114 | } 115 | -------------------------------------------------------------------------------- /src/typing/vtable.zig: -------------------------------------------------------------------------------- 1 | //! When to use: 2 | //! - When you want to call multiple functions over a single, unified interface 3 | //! with very few / no common fields. 4 | //! 5 | //! When not to use: 6 | //! - If you have a lot of common fields, you could use `@fieldParentPtr` instead. 7 | //! - If you have only one common function, you could replace the vtable pointer 8 | //! with a function pointer (I'd call this a "type erased"). 9 | 10 | const std = @import("std"); 11 | 12 | pub const Extension = struct { 13 | ctx: *anyopaque, 14 | vtable: *const VTable, 15 | 16 | pub const VTable = struct { 17 | activate: *const fn (ctx: *anyopaque) anyerror!void, 18 | deactivate: *const fn (ctx: *anyopaque) anyerror!void, 19 | run: *const fn (ctx: *anyopaque, n: usize) anyerror!void, 20 | }; 21 | 22 | pub fn activate(extension: Extension) anyerror!void { 23 | return extension.vtable.activate(extension.ctx); 24 | } 25 | 26 | pub fn deactivate(extension: Extension) anyerror!void { 27 | return extension.vtable.deactivate(extension.ctx); 28 | } 29 | 30 | pub fn run(extension: Extension, n: usize) anyerror!void { 31 | return extension.vtable.run(extension.ctx, n); 32 | } 33 | }; 34 | 35 | pub const HelloWorldSayer = struct { 36 | activated: bool = false, 37 | file: std.fs.File, 38 | 39 | fn activate(ctx: *anyopaque) anyerror!void { 40 | const hws: *HelloWorldSayer = @alignCast(@ptrCast(ctx)); 41 | hws.activated = true; 42 | } 43 | 44 | fn deactivate(ctx: *anyopaque) anyerror!void { 45 | const hws: *HelloWorldSayer = @alignCast(@ptrCast(ctx)); 46 | hws.activated = false; 47 | } 48 | 49 | fn run(ctx: *anyopaque, n: usize) anyerror!void { 50 | const hws: *HelloWorldSayer = @alignCast(@ptrCast(ctx)); 51 | 52 | if (!hws.activated) 53 | return; 54 | 55 | for (0..n) |_| { 56 | std.debug.print("Hello world!\n", .{}); 57 | } 58 | } 59 | 60 | const vtable = Extension.VTable{ 61 | .activate = &activate, 62 | .deactivate = &deactivate, 63 | .run = &run, 64 | }; 65 | 66 | pub fn extension(hws: *HelloWorldSayer) Extension { 67 | return .{ 68 | .ctx = @ptrCast(hws), 69 | .vtable = &vtable, 70 | }; 71 | } 72 | }; 73 | 74 | pub const BruhSayer = struct { 75 | activated: bool = false, 76 | file: std.fs.File, 77 | 78 | fn activate(ctx: *anyopaque) anyerror!void { 79 | const bs: *BruhSayer = @alignCast(@ptrCast(ctx)); 80 | bs.activated = true; 81 | } 82 | 83 | fn deactivate(ctx: *anyopaque) anyerror!void { 84 | const bs: *BruhSayer = @alignCast(@ptrCast(ctx)); 85 | bs.activated = false; 86 | } 87 | 88 | fn run(ctx: *anyopaque, n: usize) anyerror!void { 89 | const bs: *BruhSayer = @alignCast(@ptrCast(ctx)); 90 | 91 | if (!bs.activated) 92 | return; 93 | 94 | for (0..n) |_| { 95 | std.debug.print("bruh\n", .{}); 96 | } 97 | } 98 | 99 | const vtable = Extension.VTable{ 100 | .activate = &activate, 101 | .deactivate = &deactivate, 102 | .run = &run, 103 | }; 104 | 105 | pub fn extension(bs: *BruhSayer) Extension { 106 | return .{ 107 | .ctx = @ptrCast(bs), 108 | .vtable = &vtable, 109 | }; 110 | } 111 | }; 112 | 113 | test { 114 | var hws = HelloWorldSayer{ .file = std.io.getStdOut() }; 115 | const hello_world_extension = hws.extension(); 116 | 117 | var bs = BruhSayer{ .file = std.io.getStdOut() }; 118 | const bruh_extension = bs.extension(); 119 | 120 | const extensions = &[_]Extension{ hello_world_extension, bruh_extension }; 121 | 122 | for (extensions) |extension| { 123 | try extension.activate(); 124 | try extension.run(3); 125 | try extension.deactivate(); 126 | try extension.run(3); 127 | } 128 | } 129 | --------------------------------------------------------------------------------