Skip to content

Commit 19f2c93

Browse files
authored
Merge pull request #9 from nilslice/updated-interface
feat: support vtable generation
2 parents d3d3f78 + e8c9552 commit 19f2c93

File tree

10 files changed

+1663
-260
lines changed

10 files changed

+1663
-260
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
21
name: CI
32
on:
43
push:
5-
branches: [ main ]
4+
branches: [main]
65
pull_request:
7-
branches: [ main ]
6+
branches: [main]
87

98
jobs:
109
test-example:
1110
runs-on: ${{ matrix.os }}
1211
strategy:
1312
matrix:
1413
os: [ubuntu-latest, macos-latest, windows-latest]
15-
zig-version: ["0.14.1"]
14+
zig-version: ["0.15.1"]
1615

1716
steps:
1817
- uses: actions/checkout@v3
19-
18+
2019
- name: Install Zig
2120
uses: goto-bus-stop/setup-zig@v2
2221
with:
2322
version: ${{ matrix.zig-version }}
2423

2524
- name: Check Zig Version
2625
run: zig version
27-
26+
2827
- name: Run tests
2928
run: zig build test

README.md

Lines changed: 172 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
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
2232
zig fetch --save git+https://github.com/nilslice/zig-interface
@@ -48,27 +58,39 @@ In the end you can import the `interface` module. For example:
4858
const Interface = @import("interface").Interface;
4959
5060
const 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
6380
const 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
7496
const 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
95164
fn 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
109184
const 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
114189
const 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

Comments
 (0)