11# Zig Interfaces & Validation
22
3- A compile-time interface checker for Zig that enables interface-based design
4- with comprehensive type checking and detailed error reporting .
3+ A comprehensive interface system for Zig supporting both ** compile-time
4+ validation ** and ** runtime polymorphism ** through VTable generation .
55
66## Features
77
8- This library provides a way to define and verify interfaces in Zig at compile
9- time. It supports :
8+ This library provides two complementary approaches to interface-based design in
9+ Zig :
1010
11- - Type-safe interface definitions with detailed error reporting
11+ ** VTable-Based Runtime Polymorphism:**
12+
13+ - ** Automatic VTable wrapper generation**
14+ - Automatic VTable type generation from interface definitions
15+ - Runtime polymorphism with function pointer dispatch
16+ - Return interface types from functions and store in fields
17+
18+ ** Compile-Time Interface Validation:**
19+
20+ - Zero-overhead generic functions with compile-time type checking
21+ - Detailed error reporting for interface mismatches
1222- Interface embedding (composition)
1323- Complex type validation including structs, enums, arrays, and slices
14- - Comprehensive compile-time error messages with helpful hints
1524- Flexible error union compatibility with ` anyerror `
1625
1726## Install
1827
19- Add or update this library as a dependency in your zig project run the following command:
28+ Add or update this library as a dependency in your zig project run the following
29+ command:
2030
2131``` sh
2232zig fetch --save git+https://github.com/nilslice/zig-interface
@@ -48,27 +58,39 @@ In the end you can import the `interface` module. For example:
4858const Interface = @import("interface").Interface;
4959
5060const Repository = Interface(.{
51- .create = fn(anytype, User) anyerror!u32,
52- .findById = fn(anytype, u32) anyerror!?User,
53- .update = fn(anytype, User) anyerror!void,
54- .delete = fn(anytype, u32) anyerror!void,
61+ .create = fn(User) anyerror!u32,
62+ .findById = fn(u32) anyerror!?User,
63+ .update = fn(User) anyerror!void,
64+ .delete = fn(u32) anyerror!void,
5565}, null);
5666```
5767
5868## Usage
5969
60- 1 . Define an interface with required method signatures:
70+ ### VTable-Based Runtime Polymorphism
71+
72+ The primary use case for this library is creating type-erased interface objects
73+ that enable runtime polymorphism. This is ideal for storing different
74+ implementations in collections, returning interface types from functions, or
75+ building plugin systems.
76+
77+ ** 1. Define an interface with required method signatures:**
6178
6279``` zig
6380const Repository = Interface(.{
64- .create = fn(anytype, User) anyerror!u32,
65- .findById = fn(anytype, u32) anyerror!?User,
66- .update = fn(anytype, User) anyerror!void,
67- .delete = fn(anytype, u32) anyerror!void,
81+ .create = fn(User) anyerror!u32,
82+ .findById = fn(u32) anyerror!?User,
83+ .update = fn(User) anyerror!void,
84+ .delete = fn(u32) anyerror!void,
6885}, null);
6986```
7087
71- 2 . Implement the interface methods in your type:
88+ > Note: ` Interface() ` generates a type whose function set declared implicitly
89+ > take an ` *anyopaque ` self-reference. This saves you from needing to include it
90+ > in the declaration. However, ` anyerror ` must be included for any fallible
91+ > function, but can be omitted if your function cannot return an error.
92+
93+ ** 2. Implement the interface methods in your type:**
7294
7395``` zig
7496const InMemoryRepository = struct {
@@ -84,43 +106,108 @@ const InMemoryRepository = struct {
84106 return new_user.id;
85107 }
86108
87- // ... other Repository methods
109+ pub fn findById(self: InMemoryRepository, id: u32) !?User {
110+ return self.users.get(id);
111+ }
112+
113+ pub fn update(self: *InMemoryRepository, user: User) !void {
114+ if (!self.users.contains(user.id)) return error.UserNotFound;
115+ try self.users.put(user.id, user);
116+ }
117+
118+ pub fn delete(self: *InMemoryRepository, id: u32) !void {
119+ if (!self.users.remove(id)) return error.UserNotFound;
120+ }
88121};
89122```
90123
91- 3 . Verify the implementation at compile time:
124+ ** 3. Use the interface for runtime polymorphism: **
92125
93126``` zig
94- // In functions that accept interface implementations:
127+ // Create different repository implementations
128+ var in_memory_repo = InMemoryRepository.init(allocator);
129+ var sql_repo = SqlRepository.init(allocator, db_connection);
130+
131+ // Convert to interface objects
132+ const repo1 = Repository.from(&in_memory_repo);
133+ const repo2 = Repository.from(&sql_repo);
134+
135+ // Store in heterogeneous collection
136+ var repositories = [_]Repository{ repo1, repo2 };
137+
138+ // Use through the interface - runtime polymorphism!
139+ for (repositories) |repo| {
140+ const user = User{ .id = 0, .name = "Alice", .email = "[email protected] " }; 141+ const id = try repo.vtable.create(repo.ptr, user);
142+ const found = try repo.vtable.findById(repo.ptr, id);
143+ }
144+
145+ // Return interface types from functions
146+ fn getRepository(use_memory: bool, allocator: Allocator) Repository {
147+ if (use_memory) {
148+ var repo = InMemoryRepository.init(allocator);
149+ return Repository.from(&repo);
150+ } else {
151+ var repo = SqlRepository.init(allocator);
152+ return Repository.from(&repo);
153+ }
154+ }
155+ ```
156+
157+ ### Compile-Time Validation (Alternative Approach)
158+
159+ For generic functions where you know the concrete type at compile time, you can
160+ use the interface for validation without the VTable overhead:
161+
162+ ``` zig
163+ // Generic function that accepts any Repository implementation
95164fn createUser(repo: anytype, name: []const u8, email: []const u8) !User {
96- comptime Repository.satisfiedBy(@TypeOf(repo));
97- // ... rest of implementation
165+ // Validate at compile time that repo implements IRepository
166+ comptime Repository.validation.satisfiedBy(@TypeOf(repo.*));
167+
168+ const user = User{ .id = 0, .name = name, .email = email };
169+ const id = try repo.create(user);
170+ return User{ .id = id, .name = name, .email = email };
98171}
99172
100- // Or verify directly:
101- comptime Repository.satisfiedBy(InMemoryRepository);
173+ // Works with any concrete implementation - no VTable needed
174+ var in_memory = InMemoryRepository.init(allocator);
175+ const user = try createUser(&in_memory, "Alice", "[email protected] "); 102176```
103177
104178## Interface Embedding
105179
106- Interfaces can embed other interfaces to combine their requirements:
180+ Interfaces can embed other interfaces to combine their requirements. The
181+ generated VTable will include all methods from embedded interfaces:
107182
108183``` zig
109184const Logger = Interface(.{
110- .log = fn(anytype, []const u8) void,
111- .getLogLevel = fn(anytype ) u8,
185+ .log = fn([]const u8) void,
186+ .getLogLevel = fn() u8,
112187}, null);
113188
114189const Metrics = Interface(.{
115- .increment = fn(anytype, []const u8) void,
116- .getValue = fn(anytype, []const u8) u64,
190+ .increment = fn([]const u8) void,
191+ .getValue = fn([]const u8) u64,
117192}, .{ Logger }); // Embeds Logger interface
118193
119- // Now implements both Metrics and Logger methods
120- const MonitoredRepository = Interface(.{
121- .create = fn(anytype, User) anyerror!u32,
122- .findById = fn(anytype, u32) anyerror!?User,
123- }, .{ Metrics });
194+ // Implementation must provide all methods
195+ const MyMetrics = struct {
196+ log_level: u8,
197+ counters: std.StringHashMap(u64),
198+
199+ // Logger methods
200+ pub fn log(self: MyMetrics, msg: []const u8) void { ... }
201+ pub fn getLogLevel(self: MyMetrics) u8 { return self.log_level; }
202+
203+ // Metrics methods
204+ pub fn increment(self: *MyMetrics, name: []const u8) void { ... }
205+ pub fn getValue(self: MyMetrics, name: []const u8) u64 { ... }
206+ };
207+
208+ // Use it with auto-generated wrappers:
209+ var my_metrics = MyMetrics{ ... };
210+ const metrics = Metrics.from(&my_metrics);
124211```
125212
126213> Note: you can embed arbitrarily many interfaces!
@@ -148,12 +235,12 @@ const BadImpl = struct {
148235
149236## Complex Types
150237
151- The interface checker supports complex types including:
238+ The interface checker supports complex types including structs, enums, arrays,
239+ and optionals:
152240
153241``` zig
154- const ComplexTypes = Interface(.{
242+ const Processor = Interface(.{
155243 .process = fn(
156- anytype,
157244 struct { config: Config, points: []const DataPoint },
158245 enum { ready, processing, error },
159246 []const struct {
@@ -163,4 +250,52 @@ const ComplexTypes = Interface(.{
163250 }
164251 ) anyerror!?ProcessingResult,
165252}, null);
253+ ...
254+ ```
255+
256+ ## Choosing Between VTable and Compile-Time Approaches
257+
258+ Both approaches work from the same interface definition and can be used
259+ together:
260+
261+ | Feature | VTable Runtime Polymorphism | Compile-Time Validation |
262+ | ------------------- | --------------------------------------------------------------- | ---------------------------------- |
263+ | ** Use Case** | Heterogeneous collections, plugin systems, returning interfaces | Generic functions, static dispatch |
264+ | ** Performance** | Function pointer indirection | Zero overhead (monomorphization) |
265+ | ** Binary Size** | Smaller (shared dispatch code) | Larger (per-type instantiation) |
266+ | ** Flexibility** | Store in arrays, return from functions | Known types at compile time |
267+ | ** Type Visibility** | Type-erased (` *anyopaque ` ) | Concrete type always known |
268+ | ** Method Calls** | ` interface.vtable.method(interface.ptr, args) ` | Direct: ` instance.method(args) ` |
269+ | ** When to Use** | Need runtime flexibility | Need maximum performance |
270+
271+ ** Example using both:**
272+
273+ ``` zig
274+ // Define once
275+ const Repository = Interface(.{
276+ .save = fn(Data) anyerror!void,
277+ }, null);
278+
279+ // Use compile-time validation for hot paths
280+ fn processBatch(repo: anytype, items: []const Data) !void {
281+ comptime Repository.validation.satisfiedBy(@TypeOf(repo.*));
282+ for (items) |item| {
283+ try repo.save(item); // Direct call, can be inlined
284+ }
285+ }
286+
287+ // Use VTable for plugin registry
288+ const PluginRegistry = struct {
289+ repositories: []Repository,
290+
291+ fn addPlugin(self: *PluginRegistry, repo: Repository) void {
292+ self.repositories = self.repositories ++ &[_]Repository{repo};
293+ }
294+
295+ fn saveToAll(self: PluginRegistry, data: Data) !void {
296+ for (self.repositories) |repo| {
297+ try repo.vtable.save(repo.ptr, data);
298+ }
299+ }
300+ };
166301```
0 commit comments