Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@

name: CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

jobs:
test-example:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
zig-version: ["0.14.1"]
zig-version: ["0.15.1"]

steps:
- uses: actions/checkout@v3

- name: Install Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: ${{ matrix.zig-version }}

- name: Check Zig Version
run: zig version

- name: Run tests
run: zig build test
209 changes: 172 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
# Zig Interfaces & Validation

A compile-time interface checker for Zig that enables interface-based design
with comprehensive type checking and detailed error reporting.
A comprehensive interface system for Zig supporting both **compile-time
validation** and **runtime polymorphism** through VTable generation.

## Features

This library provides a way to define and verify interfaces in Zig at compile
time. It supports:
This library provides two complementary approaches to interface-based design in
Zig:

- Type-safe interface definitions with detailed error reporting
**VTable-Based Runtime Polymorphism:**

- **Automatic VTable wrapper generation**
- Automatic VTable type generation from interface definitions
- Runtime polymorphism with function pointer dispatch
- Return interface types from functions and store in fields

**Compile-Time Interface Validation:**

- Zero-overhead generic functions with compile-time type checking
- Detailed error reporting for interface mismatches
- Interface embedding (composition)
- Complex type validation including structs, enums, arrays, and slices
- Comprehensive compile-time error messages with helpful hints
- Flexible error union compatibility with `anyerror`

## Install

Add or update this library as a dependency in your zig project run the following command:
Add or update this library as a dependency in your zig project run the following
command:

```sh
zig fetch --save git+https://github.com/nilslice/zig-interface
Expand Down Expand Up @@ -48,27 +58,39 @@ In the end you can import the `interface` module. For example:
const Interface = @import("interface").Interface;

const Repository = Interface(.{
.create = fn(anytype, User) anyerror!u32,
.findById = fn(anytype, u32) anyerror!?User,
.update = fn(anytype, User) anyerror!void,
.delete = fn(anytype, u32) anyerror!void,
.create = fn(User) anyerror!u32,
.findById = fn(u32) anyerror!?User,
.update = fn(User) anyerror!void,
.delete = fn(u32) anyerror!void,
}, null);
```

## Usage

1. Define an interface with required method signatures:
### VTable-Based Runtime Polymorphism

The primary use case for this library is creating type-erased interface objects
that enable runtime polymorphism. This is ideal for storing different
implementations in collections, returning interface types from functions, or
building plugin systems.

**1. Define an interface with required method signatures:**

```zig
const Repository = Interface(.{
.create = fn(anytype, User) anyerror!u32,
.findById = fn(anytype, u32) anyerror!?User,
.update = fn(anytype, User) anyerror!void,
.delete = fn(anytype, u32) anyerror!void,
.create = fn(User) anyerror!u32,
.findById = fn(u32) anyerror!?User,
.update = fn(User) anyerror!void,
.delete = fn(u32) anyerror!void,
}, null);
```

2. Implement the interface methods in your type:
> Note: `Interface()` generates a type whose function set declared implicitly
> take an `*anyopaque` self-reference. This saves you from needing to include it
> in the declaration. However, `anyerror` must be included for any fallible
> function, but can be omitted if your function cannot return an error.

**2. Implement the interface methods in your type:**

```zig
const InMemoryRepository = struct {
Expand All @@ -84,43 +106,108 @@ const InMemoryRepository = struct {
return new_user.id;
}

// ... other Repository methods
pub fn findById(self: InMemoryRepository, id: u32) !?User {
return self.users.get(id);
}

pub fn update(self: *InMemoryRepository, user: User) !void {
if (!self.users.contains(user.id)) return error.UserNotFound;
try self.users.put(user.id, user);
}

pub fn delete(self: *InMemoryRepository, id: u32) !void {
if (!self.users.remove(id)) return error.UserNotFound;
}
};
```

3. Verify the implementation at compile time:
**3. Use the interface for runtime polymorphism:**

```zig
// In functions that accept interface implementations:
// Create different repository implementations
var in_memory_repo = InMemoryRepository.init(allocator);
var sql_repo = SqlRepository.init(allocator, db_connection);

// Convert to interface objects
const repo1 = Repository.from(&in_memory_repo);
const repo2 = Repository.from(&sql_repo);

// Store in heterogeneous collection
var repositories = [_]Repository{ repo1, repo2 };

// Use through the interface - runtime polymorphism!
for (repositories) |repo| {
const user = User{ .id = 0, .name = "Alice", .email = "[email protected]" };
const id = try repo.vtable.create(repo.ptr, user);
const found = try repo.vtable.findById(repo.ptr, id);
}

// Return interface types from functions
fn getRepository(use_memory: bool, allocator: Allocator) Repository {
if (use_memory) {
var repo = InMemoryRepository.init(allocator);
return Repository.from(&repo);
} else {
var repo = SqlRepository.init(allocator);
return Repository.from(&repo);
}
}
```

### Compile-Time Validation (Alternative Approach)

For generic functions where you know the concrete type at compile time, you can
use the interface for validation without the VTable overhead:

```zig
// Generic function that accepts any Repository implementation
fn createUser(repo: anytype, name: []const u8, email: []const u8) !User {
comptime Repository.satisfiedBy(@TypeOf(repo));
// ... rest of implementation
// Validate at compile time that repo implements IRepository
comptime Repository.validation.satisfiedBy(@TypeOf(repo.*));

const user = User{ .id = 0, .name = name, .email = email };
const id = try repo.create(user);
return User{ .id = id, .name = name, .email = email };
}

// Or verify directly:
comptime Repository.satisfiedBy(InMemoryRepository);
// Works with any concrete implementation - no VTable needed
var in_memory = InMemoryRepository.init(allocator);
const user = try createUser(&in_memory, "Alice", "[email protected]");
```

## Interface Embedding

Interfaces can embed other interfaces to combine their requirements:
Interfaces can embed other interfaces to combine their requirements. The
generated VTable will include all methods from embedded interfaces:

```zig
const Logger = Interface(.{
.log = fn(anytype, []const u8) void,
.getLogLevel = fn(anytype) u8,
.log = fn([]const u8) void,
.getLogLevel = fn() u8,
}, null);

const Metrics = Interface(.{
.increment = fn(anytype, []const u8) void,
.getValue = fn(anytype, []const u8) u64,
.increment = fn([]const u8) void,
.getValue = fn([]const u8) u64,
}, .{ Logger }); // Embeds Logger interface

// Now implements both Metrics and Logger methods
const MonitoredRepository = Interface(.{
.create = fn(anytype, User) anyerror!u32,
.findById = fn(anytype, u32) anyerror!?User,
}, .{ Metrics });
// Implementation must provide all methods
const MyMetrics = struct {
log_level: u8,
counters: std.StringHashMap(u64),

// Logger methods
pub fn log(self: MyMetrics, msg: []const u8) void { ... }
pub fn getLogLevel(self: MyMetrics) u8 { return self.log_level; }

// Metrics methods
pub fn increment(self: *MyMetrics, name: []const u8) void { ... }
pub fn getValue(self: MyMetrics, name: []const u8) u64 { ... }
};

// Use it with auto-generated wrappers:
var my_metrics = MyMetrics{ ... };
const metrics = Metrics.from(&my_metrics);
```

> Note: you can embed arbitrarily many interfaces!
Expand Down Expand Up @@ -148,12 +235,12 @@ const BadImpl = struct {

## Complex Types

The interface checker supports complex types including:
The interface checker supports complex types including structs, enums, arrays,
and optionals:

```zig
const ComplexTypes = Interface(.{
const Processor = Interface(.{
.process = fn(
anytype,
struct { config: Config, points: []const DataPoint },
enum { ready, processing, error },
[]const struct {
Expand All @@ -163,4 +250,52 @@ const ComplexTypes = Interface(.{
}
) anyerror!?ProcessingResult,
}, null);
...
```

## Choosing Between VTable and Compile-Time Approaches

Both approaches work from the same interface definition and can be used
together:

| Feature | VTable Runtime Polymorphism | Compile-Time Validation |
| ------------------- | --------------------------------------------------------------- | ---------------------------------- |
| **Use Case** | Heterogeneous collections, plugin systems, returning interfaces | Generic functions, static dispatch |
| **Performance** | Function pointer indirection | Zero overhead (monomorphization) |
| **Binary Size** | Smaller (shared dispatch code) | Larger (per-type instantiation) |
| **Flexibility** | Store in arrays, return from functions | Known types at compile time |
| **Type Visibility** | Type-erased (`*anyopaque`) | Concrete type always known |
| **Method Calls** | `interface.vtable.method(interface.ptr, args)` | Direct: `instance.method(args)` |
| **When to Use** | Need runtime flexibility | Need maximum performance |

**Example using both:**

```zig
// Define once
const Repository = Interface(.{
.save = fn(Data) anyerror!void,
}, null);

// Use compile-time validation for hot paths
fn processBatch(repo: anytype, items: []const Data) !void {
comptime Repository.validation.satisfiedBy(@TypeOf(repo.*));
for (items) |item| {
try repo.save(item); // Direct call, can be inlined
}
}

// Use VTable for plugin registry
const PluginRegistry = struct {
repositories: []Repository,

fn addPlugin(self: *PluginRegistry, repo: Repository) void {
self.repositories = self.repositories ++ &[_]Repository{repo};
}

fn saveToAll(self: PluginRegistry, data: Data) !void {
for (self.repositories) |repo| {
try repo.vtable.save(repo.ptr, data);
}
}
};
```
Loading
Loading