# Core Architecture Standards for Zig
This document outlines the core architecture standards for Zig projects, focusing on fundamental patterns, project structure, and organization principles. These standards aim to promote maintainability, performance, and security. All examples are written for the currently supported Zig versions (0.11.0 and later).
## 1. Project Structure
A well-defined project structure is crucial for finding code, understanding relationships, and scaling projects effectively.
### 1.1. Standard Layout
**Do This:** Adopt a standardized project layout from the beginning.
**Don't Do This:** Use a flat or haphazard directory structure.
**Why:** Consistent structure improves navigation and makes it easier for new developers to understand the codebase.
"""
my_project/
├── build.zig # Build script
├── src/ # Source code
│ ├── main.zig # Entry point
│ ├── core/ # Core logic and data structures
│ │ ├── allocator.zig # Custom allocator
│ │ └── utils.zig # General utilities
│ ├── api/ # API-related code (if applicable)
│ │ ├── routes.zig # HTTP route definitions
│ │ └── handlers.zig # Route handlers
│ └── models/ #Data Models
│ └── user.zig
├── lib/ # External dependencies (if not using package manager)
├── test/ # Unit and integration tests
│ ├── main.zig # Test entry point
│ └── core_test.zig # Tests for core functionality
├── README.md # Project documentation
├── .gitignore # Git ignore file
"""
**Explanation:**
* "build.zig": The central build script for compiling and managing the project.
* "src/": Contains all the project's source code.
* "main.zig": The application's entry point.
* "core/": Houses fundamental data structures, algorithms, or low-level functionalities that are central to the application's logic. Examples: memory management, custom allocators, utility functions.
* "api/": Contains code related to external interfaces (e.g., HTTP APIs). This can include route definitions, request handlers, and data serialization/deserialization logic.
* "models/": Defines data structures representing domain entities (e.g., users, products, orders) and can include serialization/deserialization logic.
* "lib/": Though "build.zig" and Zig's package management are preferred, this can hold externally-managed libraries. Avoid this where possible. Prefer using "build.zig" to manage dependencies.
* "test/": Contains unit and integration tests.
* "main.zig": Entry point for running tests.
* "core_test.zig": Example tests for the 'core' module.
### 1.2. Modules and Packages
**Do This:** Break code into logical modules and packages. Use "pub" keyword for exported symbols. Package related modules together in directories.
**Don't Do This:** Put all code into a single file or create a dependency cycle between modules.
**Why:** Modularity promotes encapsulation, reusability, and reduces compilation times.
**Example:**
"src/core/allocator.zig":
"""zig
// src/core/allocator.zig
const std = @import("std");
pub const FixedBufferAllocatorError = error {
OutOfMemory,
};
pub fn FixedBufferAllocator(comptime T: type, comptime capacity: usize) type {
return struct {
buffer: [capacity]T = undefined,
index: usize = 0,
pub fn init() !@This() {
return .{};
}
pub fn alloc(self: *@This()) !*T {
if (self.index >= capacity) {
return FixedBufferAllocatorError.OutOfMemory;
}
defer self.index += 1;
return &self.buffer[self.index];
}
pub fn reset(self: *@This()) void {
self.index = 0;
}
};
}
"""
"src/main.zig":
"""zig
// src/main.zig
const std = @import("std");
const Allocator = @import("core/allocator.zig").FixedBufferAllocator(u8, 1024);
pub fn main() !void {
var allocator = Allocator.init() catch unreachable;
const value = try allocator.alloc();
value.* = 42;
std.debug.print("Value: {}\n", .{value.*});
}
"""
**Explanation:**
* The "FixedBufferAllocator" is defined as a module in "core/allocator.zig". Its functions and the "FixedBufferAllocatorError" type are explicitly exported using the "pub" keyword.
* "src/main.zig" imports the "FixedBufferAllocator" using "@import".
* The "catch unreachable" handles the case where the allocator initialization fails. In a release build, this will optimize away, whereas in a debug build it will cause a crash with a helpful message.
### 1.3. Dependency Management
**Do This:** Use "build.zig" for dependency management. Leverage semantic versioning with explicit version constraints.
**Don't Do This:** Manually download and manage dependencies or rely on system-wide installations.
**Why:** Reproducible builds are essential for reliability.
**Example:**
"""zig
// build.zig
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my_project",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Add dependencies
const http = b.dependency("http", .{
.target = target,
.optimize = optimize,
});
exe.addModule("http", http.module("http")); // Ensure types are available for import
exe.linkLibrary(std.net.lib); // Example of linking a standard library
b.installArtifact(exe);
}
"""
**Explanation:**
* "b.dependency" declares an external dependency named "http". The exact mechanism for fetching the dependency (e.g., from a registry, a local path, or git) would be specified in the "build.zig.zon" file.
* "exe.addModule" creates a module named "http" that makes types and other "pub" symbols from the dependency available for import. This eliminates stringly-typed dependencies.
* "b.installArtifact" installs the executable.
### 1.4. Managing Configuration
**Do This:** Utilize structured configuration files (e.g., YAML, JSON, TOML) and load them at runtime or compile time.
**Don't Do This:** Hardcode configuration values directly in the source code.
**Why:** Configuration should be externalized for deployment flexibility
**Caveat:** While helpful in other languages, loading configuration files at runtime increases the binary size. For most simple needs, settings can simply be passed into the executable as command-line arguments. For larger needs, consider using a library that pre-processes a TOML or INI file into zig data structures at compilation time using comptime, for instance using the "embed_file" builtin. This avoids adding a costly dependency to your final executable, such as a JSON implementation or a YAML parser.
"""zig
// src/main.zig
const std = @import("std");
const Config = struct {
port: u16,
host: []const u8,
};
// For example, configuration can be received through command line parameters.
pub fn main() !void {
var args = std.process.argsAlloc(std.heap.page_allocator) catch unreachable;
defer std.process.argsFree(std.heap.page_allocator, args);
// Default values
var config = Config {
.port = 8080,
.host = "127.0.0.1",
};
// Parse arguments for --port and --host
var i: usize = 1;
while (i < args.len) {
if (std.mem.eql(u8, args[i], "--port")) {
i += 1;
if (i < args.len) {
config.port =(try std.fmt.parseInt(u16, args[i], 10));
i += 1;
} else {
std.debug.print("Error: --port requires a value\n", .{});
return error.InvalidCommandLineArgument;
}
} else if (std.mem.eql(u8, args[i], "--host")) {
i += 1;
if (i < args.len) {
config.host = args[i];
i += 1;
} else {
std.debug.print("Error: --host requires a value\n", .{});
return error.InvalidCommandLineArgument;
}
} else {
std.debug.print("Error: Unknown argument: {}\n", .{args[i]});
return error.InvalidCommandLineArgument;
}
}
std.debug.print("Listening on {}:{}\n", .{ config.host, config.port });
}
"""
## 2. Architectural Patterns
Zig, while low-level, benefits from applying well-established architectural patterns to improve structure and testability.
### 2.1. Layered Architecture
**Do This:** Organize the application into distinct layers (presentation, business logic, data access) with clear separation of concerns.
**Don't Do This:** Create a monolithic application where layers are tightly coupled.
**Why:** Layered architecture improves maintainability by isolating changes to specific layers.
**Example:** A web application architecture.
* **Presentation Layer:** Handles user interface, HTTP requests, and responses (e.g., using a web framework).
* **Business Logic Layer:** Implements core application logic and validation rules.
* **Data Access Layer:** Manages interactions with data storage (e.g., databases, files).
### 2.2. Dependency Injection
**Do This:** Pass dependencies explicitly to functions or structs.
**Don't Do This:** Directly create dependencies within functions or structs.
**Why:** Promotes testability by allowing mocking of dependencies and reducing global state.
**Example:**
"""zig
const std = @import("std");
// Interface for a logger
pub const Logger = interface {
pub fn log(self: *@This(), message: []const u8) void;
};
// Concrete logger implementation
pub const ConsoleLogger = struct {
pub fn log(self: *@This(), message: []const u8) void {
std.debug.print("{s}\n", .{message});
}
};
// A service that depends on a logger
pub const MyService = struct {
logger: *const Logger,
pub fn new(logger: *const Logger) @This() {
return .{ .logger = logger };
}
pub fn doSomething(self: *@This()) void {
self.logger.log("MyService is doing something");
}
};
pub fn main() !void {
var console_logger = ConsoleLogger{}; // Create Logger
var service = MyService.new(&console_logger);
// We can call console_logger as though it were an Interface
service.doSomething();
return;
}
"""
**Explanation:**
* "MyService" doesn't directly instantiate a specific logger implementation. Instead, it receives a logger instance through its constructor.
* This allows you to inject different logger implementations (e.g., a file logger, a test logger) without modifying "MyService".
### 2.3. Error Handling Strategies
**Do This:** Use Zig's error union types for explicit error handling. Centralize error handling logic where appropriate.
**Don't Do This:** Ignore errors or rely on panics without proper handling.
**Why:** Robust error handling is critical for application stability.
**Example:**
"""zig
const std = @import("std");
fn divide(a: i32, b: i32) !f32 {
if (b == 0) {
return error.DivisionByZero;
}
return @floatFromInt(a) / @floatFromInt(b);
}
pub fn main() !void {
const result = try divide(10, 2);
std.debug.print("Result: {}\n", .{result});
const err = divide(5,0);
if (err) |division_error| {
std.debug.print("ERROR: {}", .{division_error});
}
}
"""
**Explanation**
* The "divide" function returns "!f32", which an error union with a possible error of type "error.DivisionByZero".
* The "try" keyword can be used to automatically return the thrown error
* Alternatively, errors can be parsed using the "if (err)" keyword.
### 2.4. Custom Allocators
**Do This:** Use custom allocators for fine-grained memory management.
**Don't Do This:** Rely solely on the default global allocator in performance-critical sections.
**Why:** Improves memory efficiency and provides deterministic cleanup. This is PARTICULARLY important in Zig because Zig does NOT offer any garbage collection!
"""zig
const std = @import("std");
const PoolAllocatorError = error {
OutOfMemory,
};
pub fn PoolAllocator(comptime T: type, comptime capacity: usize) type {
return struct {
arena: [capacity]T = undefined, // Preallocate
used: [capacity] bool = .{false} , // Track Usage
pub fn init() @This() {
return .{};
}
pub fn alloc(self: *@This()) !*T {
for (self.used, 0..) |used, i| { // Find a free slot
if (!used) {
self.used[i] = true;
return &self.arena[i];
}
}
return PoolAllocatorError.OutOfMemory;
}
pub fn free(self: *@This(), ptr: *T) void {
const index = @intFromPtr(ptr) - @intFromPtr(&self.arena); // Calculate Index
if (index >= 0 and index < capacity * @sizeOf(T)) {
const real_index = index / @sizeOf(T);
self.used[real_index] = false;
}
}
};
}
pub fn main() !void {
var pool = PoolAllocator(u8, 1024).init();
var x = pool.alloc() catch PoolAllocatorError.OutOfMemory => unreachable;
x.* = 1;
defer pool.free(x);
const y = pool.alloc() catch PoolAllocatorError.OutOfMemory => unreachable;
y.* = 2;
defer pool.free(y);
std.debug.print("{}, {}\n", .{x.*, y.*});
}
"""
## 3. Implementation Details and Best Practices
### 3.1. Compile-Time Metaprogramming
**Do This:** Leverage "comptime" to perform calculations and generate code at compile time.
**Don't Do This:** Overuse "comptime" excessively, making the compilation process too slow.
**Why:** Generate efficient and specialized code based on compile-time constants.
**Example:**
"""zig
const std = @import("std");
// Generate an array of a given size at compile time
fn createArray(comptime size: usize, comptime value: i32) [size]i32 {
var result: [size]i32 = undefined;
inline for (0..size) |i| {
result[i] = value;
}
return result;
}
pub fn main() !void {
const array = createArray(5, 10); // Array size is known at compile time.
std.debug.print("{any}\n", .{array});
}
"""
**Explanation:**
* The "createArray" function takes a "comptime" size argument, meaning the array size is determined at compile time.
* The array is constructed with a value that's also known at compile time.
### 3.2. Memory Management
**Do This:** Be explicit about memory allocation and deallocation. Use "defer" for cleanup to avoid leaks. Consider arena allocators for short-lived objects.
**Don't Do This:** Leak memory or double-free memory, leading to crashes.
**Why:** Zig relies heavily on manual memory management; careful practices minimizes errors.
**Example:**
"""zig
const std = @import("std");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
std.mem.set(buffer, 0, 1024);
std.debug.print("Buffer allocated and initialized.\n", .{});
}
"""
**Explanation:**
* This example uses an "ArenaAllocator". All allocations made with this allocator will be freed when "arena.deinit()" is called.
* "defer allocator.free(buffer)" ensures "buffer" is deallocated correctly, even if errors occur before the end of the function.
### 3.3. Concurrency and Asynchronous Programming
**Do This:** Use "async" and "await" keywords appropriately. Protect shared resources with mutexes or atomic operations.
**Don't Do This:** Create data races or deadlocks.
**Why:** Efficient concurrency is vital for resource utilization, especially during I/O.
**Example:**
"""zig
const std = @import("std");
// Simulate an Async Task
fn someAsyncOperation(value: i32) usize {
std.time.sleep(std.time.milliseconds(100)); // Simulate Work
return @as(usize, value * 2);
}
// Async Function Wrapper. Can be used with await
fn asyncTask(value: i32) anyerror!usize {@async {
return someAsyncOperation(value);
}};
pub fn main() anyerror!void {
var promise = asyncTask(10);
std.debug.print("Result is {}\n", .{@await(promise)});
}
"""
### 3.4. Testing Strategies
**Do This:** Write unit tests and integration tests. Use Zig's built-in testing framework.
**Don't Do This:** Skip testing or write insufficient tests.
**Why:** Automated tests safeguard the correctness and maintainability of the code.
**Example:**
"test/core_test.zig":
"""zig
const std = @import("std");
const allocator = @import("core/allocator.zig");
test "FixedBufferAllocator: Basic Alloc" {
var buf = allocator.FixedBufferAllocator(u8, 10).init() catch unreachable;
var x = buf.alloc() catch unreachable;
x.* = 1;
try std.testing.expectEqual(@as(u8, 1), x.*);
}
"""
"test/main.zig":
"""zig
const std = @import("std");
pub fn main() !void {
std.testing.runTests() catch |err| {
std.debug.print("Tests failed: {}\n", .{err});
return err;
};
}
"""
**Explanation:**
* "test/core_test.zig" defines unit tests for the "FixedBufferAllocator" module.
* "std.testing.expectEqual" asserts that a value is what you expect it to be.
* "test/main.zig" serves as the entry point for running all tests in the project. It calls "std.testing.runTests()" to discover and execute all test functions.
### 3.5. Documentation and Code Comments
**Do This:** Write clear, concise, and up-to-date documentation and comments.
**Don't Do This:** Write redundant, out-of-date, or misleading documentation.
**Why:** Documentation bridges the information gap between the code and the developers.
**Example:**
"""zig
/// A struct that represents a point in 2D space.
pub const Point = struct {
/// The x-coordinate of the point.
x: f32,
/// The y-coordinate of the point.
y: f32,
/// Calculates the distance from this point to another point.
pub fn distanceTo(self: @This(), other: Point) f32 {
const dx = other.x - self.x;
const dy = other.y - self.y;
return std.math.sqrt(dx * dx + dy * dy);
}
};
"""
### 3.6. Code Formatting
**Do This:** Consistently format code using a standard style (e.g., "zig fmt").
**Don't Do This:** Use inconsistent formatting or violate style guidelines.
**Why:** Formatting enhances readability and reduces cognitive load.
**Example:**
Run "zig fmt" on your code. The most important thing is consistency.
### 3.7. Security Best Practices
**Do This:** Always validate untrusted inputs. Use Zig's "std.crypto" package for cryptographic operations. Prevent buffer overflows.
**Don't Do This:** Trust user-provided data without validation. Implement custom cryptographic algorithms. Ignore potential overflow errors.
**Why:** Secure coding practices are essential to prevent vulnerabilities and protect against attacks.
### 3.8. Asynchronous Programming Best Practices
**Do This:** Use "async" and "await" for concurrency. Use "std.Thread" only for parallelism. Avoid blocking operations in "async" functions. Handle errors properly in "async" functions.
**Don't Do This:** Use complex threading models without proper synchronization. Perform blocking operations in asynchronous contexts. Ignore potential errors from asynchronous operations.
**Why:** Asynchronous programming improves performance for concurrent applications and avoids blocking the main thread. Parallel programming improves CPU Utilization for computationally intensive tasks.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Zig This document outlines the component design standards for Zig, focusing on creating reusable, maintainable, and performant components. These guidelines are tailored for modern Zig development and emphasize utilizing the language's unique features effectively. ## 1. Component Architecture ### 1.1. Principles of Component Design * **Standard:** Adhere to the principles of high cohesion and low coupling. * **Do This:** Design components that perform a single, well-defined task. Ensure that components depend on minimal external dependencies. * **Don't Do This:** Create "god" components that handle multiple unrelated responsibilities or have tight dependencies on numerous other components. * **Why:** High cohesion makes components easier to understand and modify. Low coupling reduces the impact of changes in one component on other parts of the system. * **Standard:** Prefer composition over inheritance. * **Do This:** Use interfaces or function pointers to define component contracts and compose functionalities together. * **Don't Do This:** Rely heavily on inheritance, as it can lead to fragile base class problems and decreased flexibility. Zig does not have class inheritance, so focus on alternatives like composition. * **Why:** Composition promotes flexibility and reduces the risk of creating rigid hierarchies. * **Standard:** Design for testability. * **Do This:** Structure components to be easily testable in isolation. Use dependency injection or similar techniques to provide mock implementations of dependencies during testing. * **Don't Do This:** Create components with hard-coded dependencies that make unit testing difficult or impossible. * **Why:** Testable components lead to more robust and reliable software. ### 1.2. Component Modularity * **Standard:** Organize components into well-defined modules. * **Do This:** Group related components into modules based on functionality. Use Zig's "pub" keyword to control component visibility and enforce encapsulation. * **Don't Do This:** Place all components in a single monolithic module or expose internal implementation details unnecessarily. * **Why:** Modularity enhances code organization and maintainability while clearly defining public APIs. **Example:** """zig // my_module.zig pub const MyComponent = struct { pub fn doSomething(self: *MyComponent, input: i32) i32 { return self.internalFunction(input) * 2; } fn internalFunction(self: *MyComponent, value: i32) i32 { // Implementation detail, not exposed publicly return value + 1; } }; """ """zig // main.zig const std = @import("std"); const my_module = @import("my_module"); pub fn main() !void { var component = my_module.MyComponent {}; const result = component.doSomething(&component, 5); std.debug.print("Result: {}\n", .{result}); // Output: Result: 12 } """ * **Standard:** Use abstract interfaces or data types to define component interactions. * **Do This:** Define interfaces or data types that express the contracts between components. Implement these interfaces within each component. This is especially important when the concrete types are determined at runtime. * **Don't Do This:** Hardcode specific component implementations as dependencies, making it difficult to swap implementations or test independently. * **Why:** Abstract interfaces promote loose coupling and enable polymorphism. **Example:** """zig // interfaces.zig pub const DataReader = interface { pub fn readData() ![]const u8; }; pub const DataWriter = interface { pub fn writeData(data: []const u8) !void; }; """ """zig // component_a.zig const interfaces = @import("interfaces.zig"); const std = @import("std"); pub const ComponentA = struct { impl: *ComAImpl, }; const ComAImpl = struct { reader: anytype, writer: anytype, pub fn readData(self: *ComAImpl) ![]const u8 { return self.reader.readData(); } pub fn writeData(self: *ComAImpl, data: []const u8) !void { try self.writer.writeData(data); } }; pub fn init(reader: anytype, writer: anytype) ComponentA { return ComponentA{ .impl = &ComAImpl{ .reader = reader, .writer = writer, }, }; } """ * **Standard:** Manage dependencies explicitly. * **Do This:** Use dependency injection or a similar mechanism to provide components with their dependencies. Prefer passing dependencies as arguments to component constructors or functions. * **Don't Do This:** Rely on global state or hard-coded dependencies, as these make testing and maintenance difficult. * **Why:** Explicit dependency management makes components more modular and testable. ## 2. Component Implementation ### 2.1. Data Structures * **Standard:** Choose appropriate data structures for performance and memory efficiency. * **Do This:** Consider the specific requirements of your component when selecting data structures. Use "std.ArrayList" for dynamically sized arrays, "std.AutoHashMap" for key-value lookups, and structs for fixed-size data. Utilize comptime introspection to automatically optimize data structures. * **Don't Do This:** Use inefficient data structures without considering performance implications or introduce unnecessary copies or allocations. * **Why:** Efficient data structures are essential for achieving optimal performance. * **Standard:** Consider alignment and packing. * **Do This:** Understand Zig's data layout and use "packed struct" when appropriate to minimize memory usage. Be aware of alignment requirements on different architectures. * **Don't Do This:** Ignore alignment and packing, leading to potential memory waste or performance degradation. * **Why:** Efficient memory usage is crucial in resource-constrained environments. **Example:** """zig const std = @import("std"); pub fn main() !void { const UnpackedStruct = struct { a: u8, b: u32, c: u8, }; const PackedStruct = packed struct { a: u8, b: u32, c: u8, }; std.debug.print("Size of UnpackedStruct: {}\n", .{@sizeOf(UnpackedStruct)}); // Output: Size of UnpackedStruct: 12 (due to alignment) std.debug.print("Size of PackedStruct: {}\n", .{@sizeOf(PackedStruct)}); // Output: Size of PackedStruct: 6 } """ ### 2.2. Error Handling * **Standard:** Use Zig's error union and "try" syntax for robust error handling. * **Do This:** Define error sets that are specific to your component. Use "try" to propagate errors to the caller. Consider using "catch" to handle specific errors or provide fallback behavior. * **Don't Do This:** Ignore errors or use generic error types that don't provide sufficient context, or rely on panics for non-exceptional situations. * **Why:** Proper error handling is essential for building reliable software. **Example:** """zig const std = @import("std"); const ComponentError = error { InvalidInput, FileNotFound, }; fn processData(input: i32) !i32 { if (input < 0) { return ComponentError.InvalidInput; } // Simulate file not found error if (input == 13) { return ComponentError.FileNotFound; } return input * 2; } pub fn main() !void { const input_value: i32 = 13; // Try different values (e.g., -1 for InvalidInput) const result = processData(input_value) catch |err| { std.debug.print("Error processing data: {}\n", .{err}); return 0; // Fallback value }; std.debug.print("Result: {}\n", .{result}); // If error, prints 0 } """ ### 2.3. Memory Management * **Standard:** Be explicit about memory allocation and deallocation. * **Do This:** Use Zig's allocator interface for memory management. Pass allocators explicitly to components that need to allocate memory. Ensure that all allocated memory is properly deallocated. Leverage "defer" for automatic cleanup and use arenas for managing temporary allocations. * **Don't Do This:** Rely on implicit memory management or leak memory. Avoid using global allocators without careful consideration. * **Why:** Zig offers precise control over memory, which is crucial for performance-sensitive applications. **Example:** """zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const data = try allocator.alloc(u8, 10); defer allocator.free(data); for (data, 0..) |*byte, i| { byte.* = @intCast(u8, i); } std.debug.print("Data: {}\n", .{data}); } """ ### 2.4. Concurrency * **Standard:** Use Zig's concurrency features to improve performance, but avoid data races. * **Do This:** Employ "std.Thread" and "std.Mutex" for concurrent execution and synchronization. Use atomics for lock-free data sharing when appropriate. * **Don't Do This:** Introduce data races by accessing shared mutable state without proper synchronization. Overuse locks, which can lead to performance bottlenecks. * **Why:** Concurrency can significantly improve performance, but requires careful consideration of synchronization and memory safety. ## 3. Component API Design ### 3.1. Naming Conventions * **Standard:** Follow consistent naming conventions for components, functions, and variables. * **Do This:** Use PascalCase for component names, camelCase for function and variable names, SNAKE_CASE for constants. Always name things to easily understand their function. * **Don't Do This:** Use inconsistent or ambiguous names that make code difficult to understand. * **Why:** Consistent naming improves code readability and maintainability. ### 3.2. Function Design * **Standard:** Design functions to be small, focused, and easy to understand. * **Do This:** Limit the size of functions to a manageable number of lines of code (e.g., less than 50 lines per function). Extract complex logic into smaller, reusable helper functions. Return errors instead of panicking for recoverable situations. * **Don't Do This:** Create large, monolithic functions that are difficult to understand and test, or mix too much unrelated logic together. * **Why:** Small, focused functions are easier to test, debug, and maintain. ### 3.3. Interoperability * **Standard:** Consider interoperability with C and other languages, where needed. * **Do This:** Use "extern" keyword to create C-compatible interfaces. Ensure that data structures are compatible with C data types. Carefully manage memory allocation and deallocation when crossing language boundaries. Pass allocators through the component API so allocation rules are obvious. * **Don't Do This:** Expose Zig-specific data structures directly to C code or create memory leaks when passing data between languages. * **Why:** Zig excels at providing low-level access and interfaces with other languages. **Example:** """zig // zig_component.zig export fn add(a: i32, b: i32) i32 { return a + b; } """ """c // main.c #include <stdio.h> extern int add(int a, int b); int main() { int result = add(5, 3); printf("Result from Zig: %d\n", result); return 0; } """ Compile with "zig cc main.c zig_component.zig -o main" ## 4. Advanced Component Design Patterns ### 4.1. Compile-Time Reflection and Code Generation * **Standard:** Utilize compile-time reflection to generate code and optimize data structures. * **Do This:** Use "@typeInfo" and other comptime functions to inspect data types and generate code dynamically at compile time. * **Don't Do This:** Overuse compile-time reflection, which can increase compilation time. * **Why:** Compile-time reflection allows for powerful metaprogramming and optimization. **Example:** """zig const std = @import("std"); pub fn main() !void { const MyStruct = struct { a: i32, b: f32, }; const type_info = @typeInfo(MyStruct); if (type_info == .Struct) { std.debug.print("MyStruct is a struct with {} fields.\n", .{type_info.Struct.fields.len}); for (type_info.Struct.fields) |field| { std.debug.print("Field name: {}, type id: {}\n", .{field.name, field.type}); } } } """ ### 4.2. Custom Allocators * **Standard:** Create custom allocators for specialized memory management needs. * **Do This:** Implement the "std.mem.Allocator" interface to create custom allocators, such as stack allocators or pool allocators. * **Don't Do This:** Use standard allocators for all memory allocation needs, which may not be the most efficient solution for specific use cases. * **Why:** Custom allocators can provide significant performance improvements in certain scenarios. ### 4.3. Tagged Unions for Polymorphism * **Standard:** Employ tagged unions for implementing polymorphic behavior. * **Do This:** Define a tagged union type with different variants, each representing a specific implementation of an interface. Use a tag field to indicate the active variant. * **Don't Do This:** Rely on dynamic dispatch mechanisms, which can introduce runtime overhead and are outside of Zig's core design. * **Why:** Tagged unions provide a type-safe and efficient way to implement polymorphism without runtime dispatch overhead. **Example:** """zig const std = @import("std"); const Shape = union(enum) { circle: Circle, rectangle: Rectangle, }; const Circle = struct { radius: f32, }; const Rectangle = struct { width: f32, height: f32, }; fn area(shape: Shape) f32 { return switch (shape) { .circle => |c| std.math.pi * c.radius * c.radius, .rectangle => |r| r.width * r.height, }; } pub fn main() !void { const circle = Shape{ .circle = Circle{ .radius = 5.0 } }; const rectangle = Shape{ .rectangle = Rectangle{ .width = 4.0, .height = 6.0 } }; std.debug.print("Circle area: {}\n", .{area(circle)}); std.debug.print("Rectangle area: {}\n", .{area(rectangle)}); } """ ## 5. Tooling * **Standard:** Leverage the Zig build system and package manager. * **Do This:** Structure your project according to the Zig build system conventions. Use "zig build" to compile your code and manage dependencies. * **Don't Do This:** Use custom build scripts or ignore the Zig package manager, as this can lead to inconsistencies and maintenance challenges. * **Why:** The Zig build system provides a consistent and reliable way to build and manage Zig projects. ## Summary By adhering to these component design standards, Zig developers can create reusable, maintainable, performant, and secure software. The specific features of Zig, such as its explicit memory management, error handling, and compile-time reflection, should be leveraged to design components that are both efficient and easy to understand. This document provides a solid foundation for building high-quality Zig applications.
# State Management Standards for Zig This document outlines the recommended standards for managing state in Zig applications. It covers diverse approaches to application state, data flow, and reactivity, all tailored to Zig's unique features and philosophy. The focus is on maintainability, performance, and security, drawing upon the latest Zig versions and ecosystem tools. ## 1. Core Principles ### 1.1 Immutability and Data Flow * **Do This:** Favor immutable data structures and explicit data flow to minimize side effects and improve predictability. This greatly helps reasoning about the program's behavior. * **Don't Do This:** Avoid pervasive mutable global state. It makes debugging and reasoning about the program's behavior extremely difficult. * **Why?:** Immutability reduces the likelihood of unexpected state changes, leading to fewer bugs and easier debugging. Explicit data flow makes the flow of information through the application easier to understand, aiding maintainability. **Example:** """zig const std = @import("std"); const User = struct { id: u32, name: []const u8, }; fn createUser(id: u32, name: []const u8) User { return User{ .id = id, .name = name, // Note: this should typically be a copy, see data ownership section }; } fn updateUser(user: User, newName: []const u8) User { return User{ .id = user.id, .name = newName, // Again, consider a copy for proper ownership }; } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const initialUser = createUser(123, "Alice"); const updatedUser = updateUser(initialUser, "Alicia"); std.debug.print("Initial User: id={}, name={s}\n", .{initialUser.id, initialUser.name}); std.debug.print("Updated User: id={}, name={s}\n", .{updatedUser.id, updatedUser.name}); } """ ### 1.2 Explicit Memory Management and Ownership * **Do This:** Understand and manage memory ownership explicitly. Use "allocator" parameters judiciously and carefully consider the lifetime of allocated data. Use "defer" statements to ensure resources are released. * **Don't Do This:** Rely on implicit memory management or ignore potential memory leaks. * **Why?:** Zig's lack of garbage collection requires meticulous memory management. Explicit control reduces the risk of memory leaks and dangling pointers. Zig's powerful comptime features can aid with this (e.g. detecting unused resources). **Example:** """zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const message: []const u8 = "Hello, Zig!"; // Allocate memory for a copy of the message. Important for ownership! const buffer = try allocator.alloc(u8, message.len); defer allocator.free(buffer); @memcpy(buffer, message); // Copy the message into the allocated buffer std.debug.print("Message: {s}\n", .{buffer}); } """ ### 1.3 Error Handling * **Do This:** Use "try" and "catch" for explicit error handling. Leverage Zig's error union types to propagate errors gracefully. * **Don't Do This:** Ignore errors or use panics for non-exceptional situations. * **Why?:** Explicit error handling allows for graceful recovery and prevents unexpected program termination. Error unions enhance code clarity and guarantee that errors are handled appropriately. **Example:** """zig const std = @import("std"); fn readFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 { const file = try std.fs.openFile(filename, .{}); defer file.close(); const file_size = try file.getEndPos(); const buffer = try allocator.alloc(u8, file_size); try file.readAll(buffer); return buffer; } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const filename: []const u8 = "my_file.txt"; const result = readFile(allocator, filename) catch |err| { std.debug.print("Error reading file: {any}\n", .{err}); return; // Exit the function }; if (result != null) { defer allocator.free(result); std.debug.print("File content: {s}\n", .{result}); } } """ ## 2. Architectural Patterns ### 2.1 Data-Oriented Design (DOD) * **Do This:** Organize data in structures of arrays (SoA) format when performance is critical. Separate data from logic to improve cache utilization and SIMD optimization opportunities. * **Don't Do This:** Blindly use object-oriented principles where performance is paramount. Consider the memory layout implications of your data structures. * **Why?:** DOD maximizes data locality, leading to improved cache hit rates and enhanced SIMD parallelization capabilities. Zig's low-level control facilitates effective DOD implementation. **Example:** """zig const std = @import("std"); const MAX_ENTITIES = 1024; const Position = struct { x: [MAX_ENTITIES]f32, y: [MAX_ENTITIES]f32, }; const Velocity = struct { dx: [MAX_ENTITIES]f32, dy: [MAX_ENTITIES]f32, }; var position: Position = undefined; var velocity: Velocity = undefined; fn update(dt: f32) void { for (0..MAX_ENTITIES) |i| { position.x[i] += velocity.dx[i] * dt; position.y[i] += velocity.dy[i] * dt; } } pub fn main() !void { // Initialize data (simplified for demonstration) for (0..MAX_ENTITIES) |i| { position.x[i] = @floatFromInt(i); position.y[i] = @floatFromInt(i); velocity.dx[i] = 1.0; velocity.dy[i] = 0.5; } update(0.1); std.debug.print("Position (first entity): x={}, y={}\n", .{position.x[0], position.y[0]}); } """ ### 2.2 Finite State Machines (FSMs) * **Do This:** Utilize tagged unions or enums to represent distinct states in an FSM. Use switch statements for state transitions. Consider comptime evaluation to validate state transitions at compile time. * **Don't Do This:** Encode state transitions with complex if-else chains or rely on string comparisons. * **Why?:** FSMs provide a structured way to manage complex state transitions, enhancing code readability and maintainability. Tagged unions and switch statements provide type safety and facilitate compile-time checking. **Example:** """zig const std = @import("std"); const State = enum { Idle, Loading, Running, Error, }; var currentState: State = .Idle; fn transitionState(newState: State) void { std.debug.print("Transitioning from {s} to {s}\n", .{ @tagName(currentState), @tagName(newState) }); currentState = newState; } fn processState() void { switch (currentState) { .Idle => { std.debug.print("State: Idle\n", .{}); transitionState(.Loading); }, .Loading => { std.debug.print("State: Loading\n", .{}); // Simulate loading... transitionState(.Running); }, .Running => { std.debug.print("State: Running\n", .{}); // Simulate running... transitionState(.Idle); // loop back to idle }, .Error => { std.debug.print("State: Error\n", .{}); // Handle error... }, } } pub fn main() !void { for (0..3) |_| { processState(); } } """ ### 2.3 Actor Model * **Do This:** Consider the actor model for concurrent and distributed systems. Leverage channels (provided by the standard library or external libraries) for message passing between actors. Each actor should encapsulate its own state. * **Don't Do This:** Use shared mutable state and locks directly for concurrency when the actor model may be more appropriate. * **Why?:** The actor model simplifies concurrent programming by isolating state within actors and facilitating communication through asynchronous message passing. **Example (Conceptual - Zig's concurrency features are still evolving):** """zig // This is a simplified conceptual illustration of the actor model // Zig's concurrency features are under development so this may change in the future. // Placeholder for a channel implementation (using std.async when available, or a custom solution) const Channel = struct { // ... Implementation details hidden ... }; fn createChannel(comptime T: type) Channel { // Placeholder for channel creation logic return Channel{}; } fn send(channel: Channel, message: anytype) void { // Placeholder for sending a message on the channel } fn receive(channel: Channel) anytype { // Placeholder for receiving a message on the channel return undefined; } const ActorA = struct { state: i32, channel: Channel, fn run(self: *@This()) void { while (true) { const message = receive(self.channel); // Process the message and update the state accordingly self.state += 1; // Example update std.debug.print("ActorA - Received message, state = {}\n", .{self.state}); } } }; const ActorB = struct { channel: Channel, fn run(self: *@This()) void { // ... send(self.channel, "Hello from ActorB"); } }; pub fn main() !void { // In a real implementation, would need async/concurrency framework to actually run these concurrently. const channelA = createChannel(anyopaque); // create channel for ActorA const channelB = createChannel(anyopaque); // create channel for ActorB var actorA = ActorA{ .state = 0, .channel = channelA }; var actorB = ActorB{ .channel = channelB }; // Placeholder: Simulate running the actors concurrently (needs suitable Zig concurrency primitives) actorA.run(); actorB.run(); } """ ## 3. State Management Techniques ### 3.1 Function Parameters and Return Values * **Do This:** Pass state as function parameters and return updated state as return values. This promotes pure functions and explicit data flow. * **Don't Do This:** Rely on global variables or mutable function arguments for managing state unless absolutely necessary. * **Why?:** Function parameters and return values establish clear input-output relationships, making functions easier to test and reason about. **Example:** """zig const std = @import("std"); const CounterState = struct { count: u32, }; fn incrementCounter(state: CounterState) CounterState { return CounterState{ .count = state.count + 1 }; } pub fn main() !void { var state = CounterState{ .count = 0 }; state = incrementCounter(state); state = incrementCounter(state); std.debug.print("Counter: {}\n", .{state.count}); // Outputs 2 } """ ### 3.2 Context Objects * **Do This:** Group related state variables into context objects, especially when multiple functions need access to the same set of data. Use "*const" for read-only access and "*" for mutable access. Pass context objects as parameters. * **Don't Do This:** Pass a large number of individual parameters to functions. This reduces readability and increases the risk of errors. * **Why?:** Context objects improve code organization and reduce the number of function parameters. **Example:** """zig const std = @import("std"); const RenderingContext = struct { width: u32, height: u32, clearColor: u32, }; fn render(context: *const RenderingContext) void { std.debug.print("Rendering with width={}, height={}, clearColor={x}\n", .{context.width, context.height, context.clearColor}); } pub fn main() !void { const context = RenderingContext{ .width = 800, .height = 600, .clearColor = 0x0000FF, // Blue }; render(&context); } """ ### 3.3 Dependency Injection * **Do This:** Pass dependencies explicitly as function parameters or through context objects. This promotes testability and modularity. * **Don't Do This:** Hardcode dependencies within functions. * **Why?:** Dependency injection makes it easier to replace dependencies with mock objects for testing or to switch implementations. **Example:** """zig const std = @import("std"); const Logger = struct { log: fn ([]const u8) void, }; fn processData(logger: Logger, data: []const u8) void { logger.log("Processing data..."); logger.log(data); } fn defaultLog(message: []const u8) void { std.debug.print("LOG: {s}\n", .{message}); } pub fn main() !void { const logger = Logger{ .log = defaultLog }; const data: []const u8 = "Some important data"; processData(logger, data); } """ ## 4. Reactivity ### 4.1 Observer Pattern * **Do This:** Implement the observer pattern for decoupling components. Use a central subject that maintains a list of observers and notifies them of state changes. * **Don't Do This:** Tightly couple components that depend on each other's state. * **Why?:** The observer pattern allows components to react to state changes without being directly dependent on the component that owns the state. **Example (Simplified):** """zig const std = @import("std"); const Observer = struct { callback: fn (anyopaque) void, context: anyopaque, }; const Subject = struct { observers: std.ArrayList(Observer), allocator: std.mem.Allocator, fn init(allocator: std.mem.Allocator) Subject { return Subject{ .observers = std.ArrayList(Observer).init(allocator), .allocator = allocator, }; } fn deinit(self: *@This()) void { self.observers.deinit(); } fn attach(self: *@This(), observer: Observer) !void { try self.observers.append(observer); } fn detach(self: *@This(), observer: Observer) void { // ... (Implementation to remove an observer) ... } fn notify(self: *@This(), data: anyopaque) void { for (self.observers.items) |obs| { obs.callback(data); } } }; // Usage: Define a state, a subject, and observers that react to changes. """ ### 4.2 Signals and Slots (Conceptual) * **Do This:** Consider a signals and slots mechanism conceptually for event-driven architectures. This is similar to the observer pattern, but with a more explicit connection between signals (events) and slots (handlers). Create separate signal and slot types for each event. Zig itself doesn't have built-in support, so this would require custom implementation. * **Don't Do This:** Use global event handlers without a clear structure. * **Why?:** Signals and slots offer a structured, type-safe way to manage events and their corresponding handlers. **Example (Conceptual - needs custom implementation):** """zig // Conceptual example of signals and slots // Requires custom implementation in Zig // Define a Signal and Slot type for each specific event type. const ButtonClickSignal = struct { // ... }; const ButtonClickHandler = struct { callback: fn (ButtonClickSignal) void, }; // ... Implementation to connect signals to slots and emit signals ... """ ## 5. Persistent State ### 5.1 Serialization and Deserialization * **Do This:** Use libraries like "zig-json" or "c-ray" for serializing and deserializing state to/from persistent storage. Choose the appropriate format (JSON, MessagePack, etc.) based on requirements. * **Don't Do This:** Manually construct serialization/deserialization logic, especially for complex data structures. * **Why?:** Serialization allows for persisting state to files or databases. Using existing libraries reduces the risk of errors and improves efficiency. **Example (using "zig-json" - example needs confirmation of current library API):** """zig // Note: This example is illustrative and may need adjustments based on the current zig-json API // It's important to consult the library's documentation for accurate usage. const std = @import("std"); const json = @import("json"); // Assuming zig-json is available const User = struct { id: u32, name: []const u8, }; pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const user = User{ .id = 123, .name = "Alice" }; // Serialize to JSON const serialized = try json.stringify(user, .{ .allocator = allocator }); defer allocator.free(serialized); // Important to free! std.debug.print("Serialized JSON: {s}\n", .{serialized}); // Deserialize from JSON (example, adjust according to zig-json API) const deserialized = try json.parse(User,serialized, .{ .allocator = allocator }); defer allocator.free(deserialized.name); // free any allocations made in deserialization std.debug.print("Deserialized User: id={}, name={s}\n", .{deserialized.id, deserialized.name}); } """ ### 5.2 Databases * **Do This:** Use a database library (e.g., SQLite, PostgreSQL) for persisting complex state that requires querying and indexing. Design your database schema carefully. * **Don't Do This:** Store large amounts of structured data in simple files without using a database. * **Why?:** Databases provide efficient storage, querying, and indexing capabilities for structured data. **Example (Conceptual - requires a Zig database library):** """zig // Conceptual example using a database - Requires a Zig database library // Placeholder - Needs integration with a Zig database library like zig-sqlite // const db = @import("zig-sqlite"); // Hypothetical import // ... (Example to connect to database, create tables, insert and query data) ... """ ## 6. Testing State Management ### 6.1 Unit Tests * **Do This:** Write unit tests to verify the correctness of state transitions and data transformations. Mock dependencies to isolate units of code. * **Don't Do This:** Neglect testing state management logic. * **Why?:** Unit tests ensure that state is managed correctly and prevent regressions. **Example:** """zig const std = @import("std"); const testing = std.testing; const CounterState = struct { count: u32, }; fn incrementCounter(state: CounterState) CounterState { return CounterState{ .count = state.count + 1 }; } test "incrementCounter increments the count" { var state = CounterState{ .count = 0 }; state = incrementCounter(state); try testing.expectEqual(state.count, 1); } """ ### 6.2 Integration Tests * **Do This:** Write integration tests to verify the interaction of different components that manage state. * **Don't Do This:** Only write unit tests and neglect integration testing. * **Why?:** Integration tests ensure that different parts of the system work together correctly. ## 7. Security Considerations * **Do This:** Validate and sanitize user input to prevent state corruption. Use appropriate access control mechanisms to protect sensitive state. * **Don't Do This:** Trust user input implicitly or expose sensitive state without proper protection. * **Why?:** Security measures are essential to protect against malicious attacks that could compromise the application's state. Input validation can prevent issues like buffer overflows or malicious code injection.
# Performance Optimization Standards for Zig This document outlines coding standards specifically focused on performance optimization in Zig. These standards are designed to improve application speed, responsiveness, and resource usage while leveraging Zig's unique features. Adherence to these standards will result in code that is not only fast but also maintainable and understandable. ## 1. Data Structures and Memory Layout ### 1.1. Struct Packing and Alignment **Standard:** Minimize struct size by ordering members by size (largest to smallest) to reduce padding. Utilize "packed" structs when memory layout control is paramount and performance outweighs individual member access speed. Consider "@TypeInfo" to analyze data layout. **Why:** Reduces memory footprint and improves cache utilization. "packed" removes padding added by the compiler for alignment, which can be crucial in memory-constrained environments or when interfacing with external data formats. **Do This:** """zig const Point = struct { x: f32, y: f32, }; // Good: Fields are ordered appropriately for minimal padding. const MyStruct = struct { big_array: [1024]u8, // Largest value: u32, // Then u32 flag: bool, // Smallest }; // Packed struct for precise memory layout. Note: Accessing members can be slower. const PackedStruct = packed struct { a: u8, b: u16, c: u32, }; """ **Don't Do This:** """zig // Bad: Potentially unnecessary padding due to field order. const BadStruct = struct { flag: bool, value: u32, big_array: [1024]u8, }; """ **Anti-pattern:** Blindly using "packed" without understanding its implications on access performance. Measure the performance difference. **Zig-specific:** Zig's comptime capabilities are beneficial in calculating optimal struct layouts based on target architecture requirements. """zig const std = @import("std"); pub fn main() !void { const type_info = @typeInfo(MyStruct); if (type_info == .Struct) { std.debug.print("Size: {}\n", .{type_info.Struct.layout.size}); std.debug.print("Alignment: {}\n", .{type_info.Struct.layout.alignment}); } } const MyStruct = struct { a: u8, b: u32, c: u8, }; """ ### 1.2. Choosing the Right Data Structure **Standard:** Use appropriate data structures based on access patterns and data characteristics. Consider "std.ArrayList", "std.AutoHashMap", "std.SinglyLinkedList", and fixed-size arrays. Profile the performance impact of different structures. **Why:** Using the correct data structure significantly affects memory usage and access time. Choosing between an array, linked list, or hash map depends on operations like searching, insertion, deletion, and iteration. **Do This:** """zig const std = @import("std"); // When you know the size: var array: [10]i32 = undefined; // When you need dynamic resizing: var list = std.ArrayList(i32).init(std.heap.page_allocator); defer list.deinit(); try list.append(10); // For key-value pairs with frequent lookups: var map = std.AutoHashMap(i32, f32).init(std.heap.page_allocator); defer map.deinit(); try map.put(5, 3.14); // For efficient string building: var builder = std.StringArrayList.init(std.heap.page_allocator); defer builder.deinit(); try builder.append("Hello"); try builder.append("World"); const result = builder.join(std.heap.page_allocator, " ") catch unreachable; // Properly handle errors. defer std.heap.page_allocator.free(result); std.debug.print("{s}\n", .{result}); """ **Don't Do This:** """zig // Inefficient: Using a linked list when frequent random access is required. var list = std.SinglyLinkedList(i32).init(std.heap.page_allocator); defer list.deinit(); list.append(1); list.append(2); //Problematic: Manually resizing arrays which is error prone and less efficient. """ **Anti-pattern:** Relying on a single data structure for all scenarios. Failing to profile performance under realistic workloads. **Zig-specific:** Zig's allocator-aware data structures ("ArrayList", "HashMap", etc.) are powerful but require careful management of allocation and deallocation via "defer". ### 1.3. Copy-on-Write (Cow) Structures **Standard:** Implement copy-on-write semantics for immutable data structures that are frequently copied to avoid unnecessary memory duplication. **Why:** Reduces memory consumption and allocation overhead, especially when dealing with large, immutable datasets. **Example (Conceptual):** """zig const std = @import("std"); const CowString = struct { ptr: [*]const u8, len: usize, ref_count: *usize, pub fn init(allocator: std.mem.Allocator, str: []const u8) !CowString { const len = str.len; const data = try allocator.alloc(u8, len); @memcpy(data, str); const ref_count = try allocator.create(usize); ref_count.* = 1; return .{ .ptr = data, .len = len, .ref_count = ref_count, }; } pub fn clone(self: CowString, allocator: std.mem.Allocator) !CowString { self.ref_count.* += 1; return .{ .ptr = self.ptr, .len = self.len, .ref_count = self.ref_count, }; } pub fn deinit(self: CowString, allocator: std.mem.Allocator) void { self.ref_count.* -= 1; if (self.ref_count.* == 0) { allocator.free(self.ptr); allocator.destroy(self.ref_count); } } }; pub fn main() !void { const allocator = std.heap.page_allocator; var str1 = try CowString.init(allocator, "Hello, world!"); defer str1.deinit(allocator); var str2 = try str1.clone(allocator); // Clone, increasing ref_count defer str2.deinit(allocator); // str1 and str2 now point to the same memory. std.debug.print("String 1: {s}\n", .{str1.ptr[0..str1.len]}); std.debug.print("String 2: {s}\n", .{str2.ptr[0..str2.len]}); // Modification would trigger a copy (not implemented here for brevity). } """ **Anti-pattern:** Implementing COW without proper atomic reference counting, leading to data races in concurrent environments. Forgetting to deallocate the underlying data when the reference count reaches zero. **Zig-specific:** Zig's memory safety features force you to be explicit with lifetimes and ownership, which is crucial for correct COW implementation. ## 2. Algorithmic Optimization ### 2.1. Choosing Efficient Algorithms **Standard:** Select algorithms with optimal time and space complexity for the task. Understand the Big O notation of different algorithms and choose the most suitable one based on input data size and access patterns. **Why:** Algorithmic choices can have a drastic impact on performance, especially as data scales. **Example:** * **Sorting:** For small datasets, insertion sort or selection sort might be faster due to lower overhead. For larger datasets, use merge sort, quicksort, or heapsort. * **Searching:** Use binary search on sorted data, hash maps for fast lookups, or tree-based structures for ordered data with range queries. **Do This:** """zig const std = @import("std"); // Binary search for sorted arrays. fn binarySearch(array: []const i32, target: i32) ?usize { var low: usize = 0; var high: usize = array.len; while (low < high) { const mid = low + (high - low) / 2; //Prevent overflow if (array[mid] == target) { return mid; } else if (array[mid] < target) { low = mid + 1; } else { high = mid; } } return null; } pub fn main() !void{ const sorted_array = [_]i32{2,4,6,8,10,12}; const target = 8; if(binarySearch(sorted_array[0..], target)){ std.debug.print("Found at index: {}\n",.{binarySearch(sorted_array[0..], target).?}); } else { std.debug.print("Target not found\n", .{}); } } """ **Don't Do This:** """zig // Inefficient: Linear search on a large, sorted array. O(n) fn linearSearch(array: []const i32, target: i32) ?usize { for (array, 0..) |value, i|{ if(value == target){ return i; } } return null; } """ **Anti-pattern:** Implementing naive algorithms without considering their performance limitations. Failing to profile and benchmark different algorithmic approaches. **Zig-specific:** Zig's generic programming features allow you to write algorithms that work with different data types without runtime overhead. ### 2.2. Loop Optimization **Standard:** Minimize computations within loops. Avoid unnecessary memory allocations and deallocations inside loops. Use loop unrolling techniques where appropriate (especially for small loops). Consider vectorization when applicable. **Why:** Loops are often performance bottlenecks. Moving computations outside loops, reducing memory operations, and unrolling loops can significantly improve performance. **Do This:** """zig const std = @import("std"); // Good: Pre-calculate the array length outside the loop. fn processArray(array: []i32) void { const len = array.len; for (0..len) |i| { array[i] *= 2; } } //Unrolling a loop might be beneficial for certain types of workloads fn unrolledLoop(array: []i32) void{ const len = array.len; var i:usize = 0; while(i < len){ array[i] *= 2; if(i + 1 < len){ array[i + 1] *= 2; } i += 2; } } pub fn main() !void { var my_array: [4]i32 = [_]i32{1,2,3,4}; processArray(my_array[0..]); std.debug.print("{any}\n",{my_array}); var my_array2: [4]i32 = [_]i32{1,2,3,4}; unrolledLoop(my_array2[0..]); std.debug.print("{any}\n",{my_array2}); } """ **Don't Do This:** """zig // Bad: Calculating array.len inside the loop on every iteration. fn badProcessArray(array: []i32) void { for (0..array.len) |i| { array[i] *= 2; } } """ **Anti-pattern:** Performing expensive operations (e.g., function calls, memory allocation) within inner loops. Ignoring the potential for loop unrolling or vectorization. **Zig-specific:** Zig's "inline" keyword can be used to force the compiler to inline a function call, potentially eliminating function call overhead within loops. Careful use of "comptime" can precompute loop bounds or other loop invariants. ### 2.3. Avoiding Unnecessary Allocations **Standard:** Minimize memory allocation, especially in performance-critical sections. Use pre-allocated buffers, arena allocators ("std.heap.ArenaAllocator"), and manual memory management where appropriate. Favor stack allocation over heap allocation when the size is known at compile time. **Why:** Memory allocation is an expensive operation. Reducing the frequency of allocations and deallocations significantly improves performance. **Do This:** """zig const std = @import("std"); // Using an arena allocator for temporary allocations. fn processData(allocator: *std.mem.Allocator, data: []const u8) ![]u8 { var arena = std.heap.ArenaAllocator.init(allocator.*); defer arena.deinit(); const arena_allocator = arena.allocator(); const processed = try arena_allocator.alloc(u8, data.len + 1); @memcpy(processed, data); processed[data.len] = 0; // Null terminate return processed; // Ownership is returned to the caller. } pub fn main() !void { const allocator = std.heap.page_allocator; const data = "Hello, world!"; const processed_data = try processData(&allocator, data); defer allocator.free(processed_data); //Deallocate after use std.debug.print("{s}\n", .{processed_data}); } """ **Don't Do This:** """zig const std = @import("std"); //Avoid this: Allocating memory within a frequently called function without reuse. fn badProcessData(data: []const u8) ![]u8 { const allocator = std.heap.page_allocator; const processed = try allocator.alloc(u8, data.len); @memcpy(processed, data); return processed; } """ **Anti-pattern:** Frequent allocations within loops or frequently called functions. Ignoring the potential for memory reuse. **Zig-specific:** Zig's manual memory management capabilities provide fine-grained control over memory allocation and deallocation. Exploit "defer" for resource management. Zig's stage2 compiler is much more efficient at optimizing code and performing escape analysis, allowing for more efficient memory usage. ## 3. Concurrency and Parallelism ### 3.1. Utilizing Multi-threading **Standard:** Utilize multi-threading ("std.Thread") to perform tasks concurrently and leverage multiple CPU cores. Avoid data races by using proper synchronization mechanisms (mutexes, atomics). **Why:** Parallel execution can dramatically improve performance for computationally intensive tasks. **Do This:** """zig const std = @import("std"); const os = std.os; fn workerThread(arg: anyopaque) !void { const id = @ptrToInt(arg); std.debug.print("Worker thread {} started\n", .{id}); // Perform some work var sum: usize = 0; for (0..1000000){|_| sum += id; } std.debug.print("Worker thread {} finished. Sum: {}\n", .{id, sum}); } pub fn main() !void { var threads: [4]?std.Thread = [_]?std.Thread{null} ** 4; // Create and start threads for (threads, 0..) |*thread, i| { thread.* = try std.Thread.spawn(.{}, workerThread, @intToPtr(anyopaque, i)); std.debug.print("Started thread {}\n", .{i}); } // Join threads that have finished executing for (threads) |thread| { if (thread) |t| { t.join(); } } std.debug.print("All threads finished\n", .{}); } """ **Don't Do This:** """zig //Potentially problematic because it lacks proper synchronization, leading to data races var shared_data: i32 = 0; fn badWorkerThread(arg: anyopaque) !void { shared_data += 1; // Race condition! } """ **Anti-pattern:** Using threads without proper synchronization, resulting in data races and unpredictable behavior. Creating too many threads, leading to excessive context-switching overhead. **Zig-specific:** Zig makes it easy to create very small, efficient threads. Zig's memory model requires explicit memory access synchronization when using threads. Consider using "std.atomic" for synchronized primitive access. ### 3.2. Asynchronous Programming **Standard:** Use asynchronous programming techniques (e.g., "async"/"await") for I/O-bound operations to avoid blocking the main thread. **Why:** Asynchronous programming allows you to perform other tasks while waiting for I/O operations to complete, improving responsiveness. **Example:** """zig const std = @import("std"); // Placeholder implementation. Actual implementations may vary // based upon libraries used. const NetworkError = error{ConnectFailed, Timeout}; // Async function to simulate network request fn makeNetworkRequest() !NetworkError { // Simulate a network delay std.time.sleep(100 * std.time.ms); return {}; } // Async function to fetch data async fn fetchData() !void { std.debug.print("Starting network request...\n", .{}); try makeNetworkRequest(); std.debug.print("Network request completed!\n", .{}); } pub fn main() !void { // Run the async task directly in the main function. // More complex applications might use event loops. std.debug.print("Starting fetch data...\n", .{}); if (fetchData()) |result| { // No need for an event loop in this simple example. // In a more advanced use case, an event loop will poll // for completion and schedule the next operation. } catch |err| { std.debug.print("Error: {}\n", .{err}); } std.debug.print("Fetch data started asynchronously, continuing main function...\n", .{}); // Perform other tasks while waiting for the data. std.time.sleep(50 * std.time.ms); // Simulate doing something else std.debug.print("Main function continued execution\n", .{}); } """ **Anti-pattern:** Overusing "async"/"await" for CPU-bound tasks, which does not provide performance benefits and can add overhead. Not handling errors properly in asynchronous code. **Zig-specific:** Zig provides direct support for async functions via the "async" and "await" keywords, but lacks a standard library event loop. User-space event loops or integrations with external libraries are common. Note that async functions in Zig are *not* green threads. ### 3.3. Data Parallelism **Standard:** Use SIMD (Single Instruction, Multiple Data) instructions and GPU programming to perform computations on multiple data elements simultaneously. **Why:** Data parallelism significantly accelerates operations that can be applied independently to multiple data elements. **Note:** Zig provides lower-level support for SIMD than languages like Rust, C++, or specialized GPU languages. Direct SIMD intrinsics and external GPU libraries might be required. **Example (Conceptual):** """zig // Illustration only. Requires platform-specific SIMD intrinsics. fn addArraysSIMD(a: []const f32, b: []const f32, result: []f32) void { // Requires detailed knowledge of the target architecture and SIMD instruction sets. // Use platform-specific intrinsics (e.g., Intel intrinsics, ARM NEON intrinsics). // Typically involves loading data into SIMD registers, performing the addition, and storing the result. // Zig does not have high-level SIMD abstractions in the standard library. } """ **Anti-pattern:** Applying SIMD to inappropriate tasks (e.g., tasks with complex data dependencies). Not aligning data properly for optimal SIMD performance. **Zig-specific:** Zig's low-level nature gives you direct access to hardware features, allowing for fine-grained control over SIMD operations. ## 4. Code Generation and Optimization ### 4.1. Compile-Time Computation **Standard:** Take advantage of Zig's "comptime" keyword to perform computations at compile time. Pre-calculate constants, generate data structures, and optimize code based on compile-time information. **Why:** Eliminates runtime overhead by performing calculations during compilation. **Do This:** """zig const std = @import("std"); // Calculate factorial at compile time. fn factorial(comptime n: u64) u64 { return if (n == 0) 1 else n * factorial(n - 1); } // Generate an array of squares at compile time fn generateSquares(comptime n: usize) [n]u64 { var result: [n]u64 = undefined; for (0..n) |i| { result[i] = @as(u64, @intCast(i * i)); } return result; } pub fn main() !void { // Computed at compile time. const fact_5 = factorial(5); std.debug.print("Factorial of 5: {}\n", .{fact_5}); // Array generated at compile time const squares = generateSquares(5); std.debug.print("Squares: {any}\n", .{squares}); } """ **Don't Do This:** """zig // Performing calculations at runtime that could be done at compile time. fn runtimeFactorial(n: u64) u64 { return if (n == 0) 1 else n * runtimeFactorial(n - 1); } """ **Anti-pattern:** Overusing "comptime", leading to increased compile times. Performing computations at compile time that depend on runtime input. **Zig-specific:** Zig's powerful "comptime" features allow for extensive code generation and optimization based on compile-time knowledge. ### 4.2. Inlining and Link-Time Optimization (LTO) **Standard:** Use the "inline" keyword to encourage the compiler to inline function calls. Enable Link-Time Optimization (LTO) during compilation to allow the compiler to perform cross-module optimizations. **Why:** Inlining eliminates function call overhead and allows for more aggressive optimizations. LTO enables optimizations across translation units, potentially improving performance. **Do This:** """zig // Force function to be inlined (if possible) inline fn add(a: i32, b: i32) i32 { return a + b; } pub fn main() !void { const result = add(5, 3); // Likely inlined. std.debug.print("Result: {}\n", .{result}); } //Compile with: zig build -Doptimize=ReleaseSmall """ **Don't Do This:** """zig // Avoid inlining very large or complex functions, as it can increase code size. """ **Anti-pattern:** Inlining functions excessively, leading to code bloat. Forgetting to enable LTO when building release versions. **Zig-specific:** Zig's LTO implementation can significantly improve performance, especially for larger projects. ### 4.3. Profiling and Benchmarking **Standard:** Use profiling tools (e.g., "perf", "valgrind", custom profiling code) to identify performance bottlenecks. Use benchmarking tools ("std.testing.benchmark") to measure the performance of critical code sections. **Why:** Profiling and benchmarking provide concrete data for optimization efforts. **Do This:** """zig const std = @import("std"); pub fn main() !void { const result = expensiveCalculation(100000); std.debug.print("Result: {}\n", .{result}); } // Function whose performance needs to be measured fn expensiveCalculation(n: i32) i32 { var result: i32 = 0; for (0..n) |i| { result += i; } return result; } test "benchmark expensiveCalculation" { const start = std.time.nanoTimestamp(); const result = expensiveCalculation(100000); const end = std.time.nanoTimestamp(); std.debug.print("Result: {}, Time taken: {} ns\n", .{result, end - start}); } """ **Anti-pattern:** Optimizing code without profiling. Making assumptions about performance without empirical data. **Zig-specific:** Zig's standard library includes basic benchmarking tools, but external profilers are often necessary for detailed analysis. Can use sampling based performance analysis tools. ### 4.4 Specialized Functions **Standard:** If you know something about the arguments being passed to a function, you can specialize a function for those cases. Use Zig's features to switch to optimized versions of code using "if" statments or comptime branching. **Why:** Allows you to cut instructions based on specific data or situations. **Do This:** """zig const std = @import("std"); pub fn main() !void { const result = optimizedCalculation(10); std.debug.print("Result: {}\n", .{result}); const result_comptime = comptime optimizedCalculation(10); std.debug.print("Comptime Result: {}\n", .{result_comptime}); } fn optimizedCalculation(a: i32) i32 { if (@hasDecl(@TypeOf(a), "Float")) { // Float specific return @floatToInt(i32, @intToFloat(f32, a) * 10.0); } else { return a * 10; } } test "benchmark optimizedCalculation" { const start = std.time.nanoTimestamp(); const result = optimizedCalculation(100000); const end = std.time.nanoTimestamp(); std.debug.print("Result: {}, Time taken: {} ns\n", .{result, end - start}); } """ **Anti-pattern:** Writing overly specific specialized function that are hard to maintain. **Zig-specific:** Zig's powerful "comptime" features allow you to generate specialized code which reduces complexity while improving performance. By adhering to these performance optimization standards, you can write Zig code that is not only efficient but also maintainable and understandable. Remember to profile and benchmark your code to identify performance bottlenecks and measure the impact of your optimizations.
# Testing Methodologies Standards for Zig This document outlines the recommended testing methodologies and best practices for Zig projects. Adhering to these guidelines will lead to more robust, maintainable, and reliable software. ## 1. Testing Pyramid and Levels ### 1.1. General Testing Strategy * **Do This:** Follow the testing pyramid concept. Prioritize unit tests, have fewer integration tests, and a minimal number of end-to-end tests. * **Don't Do This:** Rely heavily on end-to-end tests while neglecting unit and integration tests. This approach leads to slow feedback cycles and makes debugging difficult. **Why:** The testing pyramid ensures that the majority of your test cases are fast, isolated, and cheap to run (unit tests), while still providing confidence in the system's overall functionality (integration and end-to-end tests). ### 1.2. Unit Testing * **Do This:** Write focused unit tests that verify the behavior of individual functions, methods, or modules in isolation. * **Don't Do This:** Create unit tests that depend on external systems, databases, or network connections. Use mocks or stubs to isolate the unit under test. **Why:** Unit tests should provide rapid feedback on the correctness of your code. Dependencies on external systems can make tests slow, flaky, and difficult to maintain. **Example:** Testing a simple function. """zig const std = @import("std"); const assert = std.debug.assert; const adder = struct { pub fn add(a: i32, b: i32) i32 { return a + b; } }; test "test adder.add" { assert(adder.add(2, 3) == 5); assert(adder.add(-1, 1) == 0); assert(adder.add(-5, -5) == -10); } """ ### 1.3. Integration Testing * **Do This:** Use integration tests to verify the interaction between different modules or components within your system. * **Don't Do This:** Attempt to test the entire system at once. Focus on specific, important interactions. **Why:** Integration tests ensure that individual modules work correctly together. These tests catch issues that might not be apparent from unit tests alone. **Example:** Testing interaction between two modules. For illustrative purpose, assume both "adder" and "multiplier" are modules defined in separate files. """zig const std = @import("std"); const assert = std.debug.assert; const adder = @import("adder"); const multiplier = @import("multiplier"); test "test adder and multiplier integration" { const sum = adder.add(2, 3); const product = multiplier.multiply(sum, 4); assert(product == 20); } """ ### 1.4. End-to-End (E2E) Testing * **Do This:** Limit E2E tests to critical user flows and scenarios that validate the overall system functionality. * **Don't Do This:** Overuse E2E tests. They are slow, brittle, and expensive to maintain. **Why:** E2E tests simulate real user interaction with the system. Use them sparingly to ensure that the most critical functionality is working as expected. ## 2. Test-Driven Development (TDD) ### 2.1. Red-Green-Refactor * **Do This:** Follow the Red-Green-Refactor cycle: 1. Write a failing test (Red). 2. Write the minimum amount of code to make the test pass (Green). 3. Refactor the code to improve its structure, readability, and maintainability. * **Don't Do This:** Write code without tests or write tests after the code is already complete. **Why:** TDD leads to better code design, reduces defects, and provides comprehensive test coverage. **Example:** TDD implementation 1. **Red (Failing Test):** """zig const std = @import("std"); const assert = std.debug.assert; const StringReverser = struct { // Placeholder - To be implemented }; test "test string reversal" { var reverser = StringReverser{}; const reversed = reverser.reverse("hello"); assert(std.mem.eql(u8, reversed, "olleh")); } """ 2. **Green (Passing Test):** """zig const std = @import("std"); const assert = std.debug.assert; const StringReverser = struct { pub fn reverse(self: *StringReverser, input: []const u8) []const u8 { var reversed = std.ArrayList(u8).init(std.heap.page_allocator); // Use ArrayList for dynamic allocation // Reverse the string for (input) |c| { errdefer reversed.deinit(); try reversed.insert(0, c); } return reversed.items(); } }; test "test string reversal" { var reverser = StringReverser{}; const reversed = reverser.reverse("hello"); defer std.heap.page_allocator.free(reversed); // Important: Free the memory assert(std.mem.eql(u8, reversed , "olleh")); } """ 3. **Refactor:** (Further refactoring, such as error handling and optimizations, can be applied here.) ### 2.2. Boundary Conditions * **Do This:** Test boundary conditions (e.g., empty strings, maximum values, minimum values) to ensure that your code handles edge cases correctly. * **Don't Do This:** Only test typical scenarios. Neglecting boundary conditions can lead to unexpected errors. **Why:** Boundary conditions are often where bugs lurk. Testing these cases explicitly improves the robustness of your code. **Example:** Boundary conditions for a function calculating the average of an array. """zig const std = @import("std"); const assert = std.debug.assert; const calculator = struct { pub fn average(items: []const f64) ?f64 { if (items.len == 0) { return null; // Handle empty array } var sum: f64 = 0; for (items) |item| { sum += item; } return sum / @floatFromInt(items.len); } }; test "average of empty array" { assert(calculator.average([]const f64{}) == null); } test "average of single element array" { const arr = [_]f64{5.0}; const avgOpt = calculator.average(&arr); assert(avgOpt != null); assert(avgOpt.? == 5.0); } """ ## 3. Mocking and Stubbing ### 3.1. Isolating Units * **Do This:** Use mocking and stubbing to isolate the unit under test from its dependencies. * **Don't Do This:** Directly use real dependencies in your unit tests. **Why:** Mocking and stubbing ensure that your unit tests are fast, predictable, and focused on the behavior of the unit itself. ### 3.2. Test Doubles * **Do This:** Use test doubles (mocks, stubs, spies) to control the behavior of dependencies and verify interactions. Zig does not have built-in mocking frameworks, so custom implementations or community libraries (if available) are used. * **Don't Do This:** Create overly complex mocks that replicate the entire behavior of the dependency. Focus on the specific interactions that are relevant to the test. **Why:** Test doubles allow you to simulate different scenarios and verify that the unit under test interacts with its dependencies as expected. **Example:** Custom stub implementation in Zig. """zig const std = @import("std"); const assert = std.debug.assert; // Assume this is an external service we don't want to call directly in tests. const ExternalService = struct { pub fn getData(self: *ExternalService, id: i32) ![]const u8 { _ = id; // Suppress unused variable warning // In a real implementation, this would make a network request or use a database. return error.NotImplemented; } }; // Stub for the ExternalService const StubExternalService = struct { data: []const u8, pub fn getData(self: *StubExternalService, id: i32) ![]const u8 { _ = id; return self.data; } }; const DataProcessor = struct { service: *ExternalService, pub fn processData(self: *DataProcessor, id: i32) ![]const u8 { const data = try self.service.getData(id); // Some kind of data processing... return data; } }; test "test DataProcessor with StubExternalService" { const allocator = std.heap.page_allocator; var stub_service = StubExternalService{ .data = "stubbed data" }; var processor = DataProcessor{ .service = &stub_service }; const processed_data = try processor.processData(123); defer allocator.free(processed_data); assert(std.mem.eql(u8, processed_data, "stubbed data")); } """ ## 4. Asynchronous and Concurrent Testing ### 4.1. Dealing with Async Code * **Do This:** Manage asynchronous operations and concurrency explicitly in tests. Use mechanisms to synchronize test execution with the completion of async tasks using a testing frameworks (if created) or manual synchronization with atomics and mutexes. * **Don't Do This:** Rely on timing or "std.time.sleep" to "wait" for asynchronous operations. Such approach leads to flaky and unreliable tests. **Why:** Asynchronous code presents unique challenges for testing. Proper synchronization is crucial to ensure that tests are deterministic and reliable. ### 4.2. Synchronization Primitives * **Do This:** Use synchronization primitives such as "std.Thread.Mutex" and "std.Thread.Semaphore" when testing concurrent code. * **Don't Do This:** Forget proper teardown of threads and disposal of synchronisation objects causing memory leaks or hangs. **Why:** Synchronisation primitive are critical to correctly test concurrent code. **Example:** Testing basic async operation with manual synchronization (without a testing framework). """zig const std = @import("std"); const assert = std.debug.assert; // A function that simulates an asynchronous operation with a channel fn asyncOperationWithChannel(tx: chan i32, result: i32) void { std.time.sleep(10 * std.time.millisecond); // Simulate some work tx <- result; } // A simple channel type const chan = @Vector(1, i32); test "test async operation with channel" { var channel: chan i32 = undefined; var thread: std.Thread = undefined; var result: i32 = undefined; // Launch the async operation in a separate thread thread = std.Thread.spawn(.{}, asyncOperationWithChannel, .{ &channel, 42 }) catch unreachable; // Read the result from the channel result = <-channel; // Wait for the thread to finish thread.join(); assert(result == 42); } """ ## 5. Error Handling and Failure Injection ### 5.1. Testing Error Paths * **Do This:** Explicitly test error paths and ensure that your code handles errors gracefully. * **Don't Do This:** Only test the happy path. Neglecting error handling can lead to unexpected crashes and data corruption. **Why:** Robust error handling is essential for reliable software. Testing error paths ensures that your code can recover from failures gracefully. ### 5.2. Failure Injection * **Do This:** Use failure injection techniques (e.g., simulating network outages, disk errors, or memory allocation failures) to test the resilience of your code. * **Don't Do This:**. Assume that external resources and dependencies will always be available. **Why:** Failure injection helps you identify and fix potential weaknesses in your code's error handling logic. **Example:** Injecting failure by intercepting allocation requests and returning "error.OutOfMemory". Note: this requires advanced Zig knowledge and should be used with caution. """zig const std = @import("std"); const assert = std.debug.assert; // Custom allocator that can simulate out-of-memory errors pub const FailingAllocator = struct { pub fn alloc(self: *@This(), len: usize, alignment: u29) ?[*]u8 { _ = alignment; // Suppress unused variable warning. if (self.should_fail) { self.should_fail = false; // Only fail once return null; } return self.backing_allocator.alloc(len); } pub fn free(self: *@This(), ptr: [*]u8, len: usize, alignment: u29) void { _ = alignment; // Suppress unused variable warning. self.backing_allocator.free(ptr, len); } backing_allocator: std.mem.Allocator, should_fail: bool, }; test "test with failing allocator" { var backing_allocator = std.heap.page_allocator; var failing_allocator = FailingAllocator{ .backing_allocator = backing_allocator, .should_fail = true, }; // Try to allocate some memory using the failing allocator const result = failing_allocator.alloc(1024, 1); assert(result == null); // Now try again, it should succeed failing_allocator.should_fail = false; const result2 = failing_allocator.alloc(1024,1) catch unreachable; defer failing_allocator.free(result2, 1024, 1); assert(result2 != null); } """ ## 6. Code Coverage ### 6.1. Measuring Coverage * **Do This:** Use code coverage tools to measure the percentage of your code that is executed by tests. Tools like "llvm-cov" can be integrated into the build system. * **Don't Do This:** Rely solely on code coverage metrics to assess the quality of your tests. High coverage does not necessarily mean that your tests are effective. **Why:** Code coverage provides insights into the areas of your code that are not being adequately tested. ### 6.2. Interpreting Results * **Do This:** Use code coverage reports to identify gaps in your test suite and write additional tests to cover those areas. * **Don't Do This:** Aim for 100% code coverage at all costs. Focus on testing the most critical and complex parts of your system. **Why:** Code coverage helps you prioritize your testing efforts and improve the overall quality of your code. ## 7. Property Based Testing ### 7.1 Using Generators * **Do This**: Use property-based testing frameworks to generate various input values to test different scenarios. Since there are no mature frameworks for zig, one might need to be built. * **Don't Do This**: Assume that providing a few fixed parameters covers the entire range of possibilities for the code being tested. Property-based testing helps discover unexpected edge cases. **Why:** Property-based testing automatically generates a large number of test cases based on predefined properties, which can uncover edge cases and unexpected behavior. **Example**: A rough example of property-based testing implementation(concept). """zig const std = @import("std"); const testing = @import("testing"); // Hypothetical testing framework const assert = std.debug.assert; /// Function to be tested fn add(a: i32, b: i32) i32 { return a + b; } test "add function always yields correct sum" { testing.property("Addition is commutative", struct { fn generate() testing.TestCase { const a = testing.random.int(i32); const b = testing.random.int(i32); return .{ .input = .{ .a = a, .b = b } }; } fn check(case: testing.TestCase) bool { const a = case.input.a; const b = case.input.b; return add(a,b) == add(b,a); } }); testing.property("Addition result is always larger", struct { fn generate() testing.TestCase { const a = testing.random.int(i32); const b = testing.random.int(i32); return .{ .input = .{ .a = a, .b = b } }; } fn check(case: testing.TestCase) bool { const a = case.input.a; const b = case.input.b; return add(a,b) >= a; } }); } """ This example demonstrates how using a purely hypothetical library, you'd define properties about the "add" function, such as commutativity. ## 8. Continuous Integration ### 8.1. Automated Testing * **Do This:** Integrate your tests into a CI/CD pipeline to automatically run tests on every commit or pull request. * **Don't Do This:** Rely on manual testing. Automated testing ensures that defects are detected early and often. **Why:** Continuous integration helps you maintain a high level of code quality and prevent regressions. ### 8.2. Reporting * **Do This:** Generate test reports and dashboards to track test results, code coverage, and other relevant metrics. * **Don't Do This:** Ignore failing tests. Failing tests should be addressed immediately to prevent further issues. **Why:** Test reports and dashboards provide visibility into the health of your codebase and help you identify areas that need improvement. ## 9. Performance Testing ### 9.1 Proper Tooling * **Do This:** Utilise Zig's built-in features for benchmarking code and conduct performance testing. Tools such as "zig build test -Drelease-safe" or "zig build test -Drelease-fast" with cycle counters allows you to accurately track the execution time of critical section of code. * **Don't Do This:** Trust premature optimisation results without confirming the true bottleneck in the system. **Why:** Performance testing is essential for optimising and ensuring that code is as efficient as possible. **Example**: """zig const std = @import("std"); const assert = std.debug.assert; // Function to benchmark fn calculateSum(n: usize) usize { var sum: usize = 0; for (0..n) |i| { sum += i; } return sum; } test "benchmark calculateSum" { const start = std.time.nanoTimestamp(); const result = calculateSum(1000000); // 1 million const end = std.time.nanoTimestamp(); const duration = end - start; std.debug.print("calculateSum(1000000) = {}\n", .{result}); std.debug.print("Time taken: {} ns\n", .{duration}); assert(result == 499999500000); // Verify correctness } """ ## 10. Security Testing ### 10.1 Common Vulnerabilities * **Do This:** Check for common vulnerability types, such as buffer overflows, use after free, and format string bugs. Use static analysis tools and fuzzing approaches to find security vulnerabilities. * **Don't Do This:** Assume that the code is secure without conducting security specific tests. ### 10.2 Fuzzing * **Do This:** For security sensitive code, integrating a fuzzer such as "z fuzz" will help uncover inputs that may crash or cause vulnerability within the system. * **Don't Do This:** Avoid fuzzing as part of the development lifecycle. **Example**: (Hypothetical usage with a future "z fuzz" tool) """zig // Example code to be fuzzed (potentially vulnerable to buffer overflow) fn copyString(dest: []u8, source: []const u8) void { for (source, 0..) |char, i| { dest[i] = char; // Potential buffer overflow if source is larger than dest } } // Fuzz test function export fn zig_fuzz(data: []const u8) void { var buffer: [10]u8 = undefined; // Perform test copy - will cause crash if overflow copyString(&buffer, data); } """ Running "z fuzz" on this example, the tool would generate various random inputs for the data parameter and attempt to find inputs that cause the "copyString" function to write past the end of dest (buffer), flagging a buffer overflow.
# API Integration Standards for Zig This document outlines the best practices for integrating with backend services and external APIs in Zig. Following these standards will result in more maintainable, performant, and secure code. ## 1. Architecture and General Principles Designing a good API integration involves careful consideration of the overall architecture. ### 1.1. Separation of Concerns (SoC) **Standard:** Separate API interactions from business logic and data models. Employ dedicated modules or structs to encapsulate the API client. **Why:** Enhances code reusability, testability, and reduces the impact of API changes on the core application. **Do This:** Create dedicated API client modules. **Don't Do This:** Hardcode API calls directly into application logic. **Example:** """zig // api_client.zig const std = @import("std"); pub const ApiClient = struct { client: std.http.Client, base_url: []const u8, pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !ApiClient { var client = std.http.Client.init(allocator); return ApiClient{ .client = client, .base_url = base_url, }; } pub fn deinit(self: *ApiClient) void { self.client.deinit(); } pub fn get(self: *ApiClient, endpoint: []const u8, response_buffer: []u8) ![]const u8 { const url = try std.mem.concat(self.client.allocator, u8, self.base_url, endpoint); defer self.client.allocator.free(url); var request = std.http.Client.Request{ .method = .GET, .url = url, .headers = .{}, // Define headers here if needed .body = null, // No body for GET requests }; var response = try self.client.request(request); defer response.deinit(); if (response.status != .ok) { return error.HttpRequestFailed; } // Read the response body into the provided buffer. Handle partial reads! var total_read: usize = 0; while (total_read < response_buffer.len) { const read_result = response.body.read(response_buffer[total_read..]); if (read_result) |bytes_read| { total_read += bytes_read; if (bytes_read == 0) { break; // End of stream. } } else |err| { if (err == error.EndOfStream) { break; // Expected end of stream } else { return err; // Real error } } } return response_buffer[0..total_read]; } }; """ """zig // main.zig const std = @import("std"); const ApiClient = @import("api_client.zig").ApiClient; pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const base_url = "https://api.example.com"; var api_client = try ApiClient.init(allocator, base_url); defer api_client.deinit(); const response_buffer_size: usize = 4096; // Choose an appropriate size. var response_buffer = try allocator.alloc(u8, response_buffer_size); defer allocator.free(response_buffer); const response_body = try api_client.get("/users", response_buffer); std.debug.print("Response: {}\n", .{response_body}); } """ ### 1.2. Abstraction Layers **Standard:** Introduce interfaces or abstract types to define the API client's contract. Implement these interfaces with concrete API client implementations. Use dependency injection (DI) to provide the API client to consumer code. **Why:** Provides flexibility to switch between different API providers or mock API calls for testing. **Do This:** Define interfaces using "type" and implement them with concrete structs. **Don't Do This:** Directly expose the underlying API client implementation. **Example:** """zig // api_interface.zig const std = @import("std"); pub const ApiInterface = type { pub fn get(self: *@This(), endpoint: []const u8, response_buffer: []u8) ![]const u8; pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !@This(); pub fn deinit(self: *@This()) void; }; """ """zig // api_client.zig const std = @import("std"); const ApiInterface = @import("api_interface.zig").ApiInterface; pub const ApiClient = struct { client: std.http.Client, base_url: []const u8, pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !ApiClient { var client = std.http.Client.init(allocator); return ApiClient{ .client = client, .base_url = base_url, }; } pub fn deinit(self: *ApiClient) void { self.client.deinit(); } pub fn get(self: *ApiClient, endpoint: []const u8, response_buffer: []u8) ![]const u8 { const url = try std.mem.concat(self.client.allocator, u8, self.base_url, endpoint); defer self.client.allocator.free(url); var request = std.http.Client.Request{ .method = .GET, .url = url, .headers = .{}, // Define headers here if needed .body = null, // No body for GET requests }; var response = try self.client.request(request); defer response.deinit(); if (response.status != .ok) { return error.HttpRequestFailed; } // Read the response body into the provided buffer. Handle partial reads! var total_read: usize = 0; while (total_read < response_buffer.len) { const read_result = response.body.read(response_buffer[total_read..]); if (read_result) |bytes_read| { total_read += bytes_read; if (bytes_read == 0) { break; // End of stream. } } else |err| { if (err == error.EndOfStream) { break; // Expected end of stream } else { return err; // Real error } } } return response_buffer[0..total_read]; } }; // Define the *ApiInterface functions for ApiClient. This has to be done at comptime. comptime { @compileError("This example only shows the API client, and does not actually implement the @Type. See the testing section for how to implement an interface."); } """ ### 1.3. Error Handling **Standard:** Implement robust error handling and logging for API calls. Use Zig's error union types and "try" keyword to manage errors effectively. Provide context in error messages to aid debugging. **Why:** APIs can fail for various reasons (network issues, server errors, etc.). Handling these gracefully is crucial for application stability. **Do This:** Always check for errors when making API calls. Use "try" and "catch" blocks appropriately. Log errors with sufficient context. **Don't Do This:** Ignore errors or "unwrap" results without proper handling. **Example:** """zig // Enhanced Error Handling const std = @import("std"); pub fn safeApiCall(allocator: std.mem.Allocator, url: []const u8) ![]const u8 { var client = std.http.Client.init(allocator); defer client.deinit(); var request = std.http.Client.Request{ .method = .GET, .url = url, .headers = .{}, .body = null, }; const response = try client.request(request); defer response.deinit(); if (response.status != .ok) { std.log.err("API call failed with status: {d}", .{response.status}); return error.HttpRequestFailed; // Could also return error.ApiError(response.status); } // Read the response body into a buffer. var response_buffer = std.ArrayList(u8).init(allocator); defer response_buffer.deinit(); while (true) loop: { var buf: [4096]u8 = undefined; // Small stack-allocated buffer const result = response.body.read(&buf); switch (result) { .Ok(0) => break :loop, // EOF .Ok(bytesRead) => { try response_buffer.appendSlice(buf[0..bytesRead]); }, .Err(err) => { if (err == error.EndOfStream) { break :loop; // expected end of stream } else { std.log.err("Error reading response body: {any}", .{err}); return err; } }, } } return response_buffer.toOwnedSlice(); } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const result = safeApiCall(allocator, "https://api.example.com/data"); switch (result) { .Ok(data) => { defer allocator.free(data); std.debug.print("API Data: {s}\n", .{data}); }, .Err(err) => { std.debug.print("API Error: {any}\n", .{err}); }, } } """ ### 1.4. Asynchronous Operations **Standard:** Use asynchronous operations (if necessary) to avoid blocking the main thread, especially for long-running API calls. Use Zig's "async" and "await" keywords to manage concurrency. **Why:** Non-blocking API calls improve application responsiveness, particularly in GUI applications or servers handling multiple requests. While Zig's concurrency model relies more on structured concurrency and explicit control, understanding async/await is helpful. **Do This:** Utilize "async" functions for I/O-bound operations. Utilize a thread pool (from "std.Thread") for CPU-bound operations. **Don't Do This:** Perform blocking API calls on the main thread. **Example:** Async is generally more about structured concurrency, but here's basic example """zig //Concurrency example. Zig encourages structured concurrency over Async/Await. const std = @import("std"); fn worker(id: usize, url: []const u8, allocator: std.mem.Allocator) !void { std.debug.print("Worker {d} started, fetching {s}\n", .{ id, url }); // Use the same safeApiCall function from the previous example, but simplified for brevity const data = try safeApiCall(allocator, url); defer allocator.free(data); std.debug.print("Worker {d} fetched {s} bytes from {s}\n", .{ id, data.len, url }); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer if (gpa.deinit()) { std.debug.print("leak detected\n", .{}); }; const allocator = gpa.allocator(); var threads: [2]std.Thread = undefined; var thread_options: std.Thread.Options = . {}; const urls = [_][]const u8{ "https://www.example.com", // Simulate longer API calls "https://www.google.com", }; for (threads, 0..) |*thread, i| { thread.* = try std.Thread.spawn(.{ .stack_size = 8192, // Adjust as needed .options = thread_options, }, worker, .{ i, urls[i], allocator}); } std.debug.print("All threads spawned\n", .{}); // Wait for all threads to complete for (threads) |thread| { try thread.join(); } std.debug.print("All threads completed\n", .{}); } fn safeApiCall(allocator: std.mem.Allocator, url: []const u8) ![]const u8 { var client = std.http.Client.init(allocator); defer client.deinit(); var request = std.http.Client.Request{ .method = .GET, .url = url, .headers = .{}, .body = null, }; const response = try client.request(request); defer response.deinit(); // Read the response body into a buffer. var response_buffer = std.ArrayList(u8).init(allocator); defer response_buffer.deinit(); while (true) loop: { var buf: [4096]u8 = undefined; // Small stack-allocated buffer const result = response.body.read(&buf); switch (result) { .Ok(0) => break :loop, // EOF .Ok(bytesRead) => { try response_buffer.appendSlice(buf[0..bytesRead]); }, .Err(err) => { if (err == error.EndOfStream) { break :loop; // expected end of stream } else { std.log.err("Error reading response body: {any}", .{err}); return err; } }, } } return response_buffer.toOwnedSlice(); } """ ## 2. Data Handling Efficient and safe data handling is vital when dealing with APIs. ### 2.1. Data Serialization/Deserialization **Standard:** Use libraries like "std.json" or "zig-json" for parsing and serializing JSON data. Define Zig structs that precisely represent the API's data structures. Use appropriate field types and consider optional fields carefully. **Why:** Ensures correct data interpretation and type safety. Reduces errors caused by mismatched data types. **Do This:** Create Zig structs that mirror the API's data format. Use the "std.json.parseFromSlice" to decode JSON. When creating your structs, use the correct primitive types for your data. Use optional fields when needed. **Don't Do This:** Manually parse JSON or use generic dictionaries without type checking. **Example:** """zig const std = @import("std"); const User = struct { id: u64, name: []const u8, email: []const u8, age: u8, is_active: bool, optional_field: ?[]const u8 = null, // Example of an optional json field }; pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const json_data = \\{ \\ "id": 12345, \\ "name": "John Doe", \\ "email": "john.doe@example.com", \\ "age": 30, \\ "is_active": true, \\ "optional_field": "This is an optional field!" \\} ; var user: User = undefined; var j = std.json.TokenStream.init(json_data); defer j.deinit(); // var p = std.json.PrettyPrinter.init(60, .human); You could use this to print prettier json, while (try j.next()) |token| { switch (token) { .object_begin => {}, .object_end => {}, .array_begin => {}, .array_end => {}, .string => { switch (j.getKeyPayload()) { .{"id"} => { // If we found the correct key, then read the value try j.nextValue(); user.id = try std.fmt.parseInt(u64, j.getPayload(), 10); }, .{"name"} => { try j.nextValue(); user.name = try allocator.dupe(u8, j.getPayload()); defer allocator.free(user.name); }, .{"email"} => { try j.nextValue(); user.email = try allocator.dupe(u8, j.getPayload()); defer allocator.free(user.email); }, .{"age"} => { try j.nextValue(); user.age = try std.fmt.parseInt(u8, j.getPayload(), 10); }, .{"is_active"} => { try j.nextValue(); user.is_active = std.ascii.eqlIgnoreCase(j.getPayload(), "true"); }, .{"optional_field"} => { try j.nextValue(); user.optional_field = try allocator.dupe(u8, j.getPayload()); defer if (user.optional_field) |field| { allocator.free(field); }; }, else => { std.debug.print("unknown key: {s}\n", .{j.getKeyPayload()}); }, } }, else => { std.debug.print("unknown token: {any}\n", .{token}); } } } std.debug.print("User ID: {d}\n", .{user.id}); std.debug.print("User Name: {s}\n", .{user.name}); std.debug.print("User Email: {s}\n", .{user.email}); std.debug.print("User Age: {d}\n", .{user.age}); std.debug.print("User Active: {any}\n", .{user.is_active}); std.debug.print("User Optional Field: {?s}\n", .{user.optional_field}); } """ ### 2.2. Data Validation **Standard:** Validate data received from APIs to ensure it conforms to expected formats and constraints. Use Zig's comptime features for compile-time validation where possible. **Why:** Prevents unexpected errors and security vulnerabilities caused by malformed or malicious data. **Do This:** Implement validation functions for API data. Use "std.debug.assert" for assertions that hold at compile time. **Don't Do This:** Trust API data without validation. **Example:** """zig const std = @import("std"); const Item = struct { id: u64, name: []const u8, price: f64, fn isValid(self: *const Item) bool { return self.id > 0 and self.price >= 0.0 and self.name.len > 0; } }; pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const item = Item{ .id = 123, .name = "Example Item", .price = 9.99, }; if (!item.isValid()) { std.debug.print("Invalid item data!\n", .{}); return; } std.debug.print("Item is valid.\n", .{}); } """ ### 2.3. Memory Management **Standard:** Handle memory allocation and deallocation carefully when dealing with API data, particularly strings and arrays. Use allocators and "defer" statements to ensure resources are properly managed. **Why:** Prevents memory leaks and dangling pointers, which can lead to crashes and security vulnerabilities. **Do This:** Always use an allocator for dynamic memory allocations. Use "defer" to free allocated memory. **Don't Do This:** Forget to free allocated memory or use raw pointers without proper lifetime management. ## 3. Security API integrations must adhere to strict security standards. ### 3.1. Authentication and Authorization **Standard:** Implement secure authentication and authorization mechanisms for accessing APIs. Use environment variables or configuration files to store secrets securely. Never hardcode credentials in the source code. **Why:** Protects sensitive data and prevents unauthorized access to API resources. **Do This:** Use proper authentication methods such as API keys, OAuth, or JWT. Store secrets in environment variables or secure configuration files. Retrieve secrets using "std.os.getenv". **Don't Do This:** Hardcode credentials directly in the code or expose them in version control. **Example:** """zig const std = @import("std"); pub fn main() !void { const api_key_name = "MY_API_KEY"; //Name of environment variable where API KEY is saved. const api_key = std.os.getenv(api_key_name) orelse { std.debug.print("Error: {s} environment variable not found.\n", .{api_key_name}); return error.MissingApiKey; }; defer std.heap.page_allocator.free(api_key); std.debug.print("API Key: {s}\n", .{api_key}); } """ ### 3.2. Data Encryption and Transport Security **Standard:** Use HTTPS for all API communication to encrypt data in transit. Validate SSL/TLS certificates to prevent man-in-the-middle attacks. **Why:** Protects sensitive data from eavesdropping and tampering during transmission. **Do This:** Always use HTTPS endpoints. Configure the HTTP client to validate SSL/TLS certificates. When creating "std.http.Client", an SSL context can be provided. **Don't Do This:** Use HTTP for sensitive data or disable SSL/TLS certificate validation. ### 3.3. Input Sanitization **Standard:** Sanitize all data sent to APIs to prevent injection attacks. Encode data properly for the target API's format (e.g., URL encoding). **Why:** Prevents attackers from injecting malicious code or commands into API requests. **Do This:** Sanitize data before sending it to the API. Use appropriate encoding techniques. **Don't Do This:** Pass user-supplied data directly to APIs without sanitization. ## 4. Performance Optimize API integrations for performance. ### 4.1. Connection Pooling **Standard:** Use connection pooling to reuse existing connections and reduce the overhead of establishing new connections for each API call. The "std.http.Client" uses connection pooling automatically **Why:** Improves API performance by reducing latency and resource consumption. **Do This:** Reuse the same "std.http.Client" instance for multiple API calls. Create a new "std.http.Client" to separate pools. **Don't Do This:** Create a new HTTP client for every API request. ### 4.2. Caching **Standard:** Implement caching to store API responses and reduce the number of API calls. Use appropriate cache invalidation strategies to ensure data consistency. **Why:** Reduces API load, improves response times, and reduces costs. **Do This:** Implement caching for frequently accessed API data. Use cache invalidation strategies based on data update frequency. **Don't Do This:** Cache data indefinitely without invalidation. ### 4.3. Data Compression **Standard:** Use data compression (e.g., gzip) to reduce the size of API requests and responses. **Why:** Reduces network bandwidth usage and improves transfer speeds. **Do This:** Configure the HTTP client and server to use compression. **Don't Do This:** Send uncompressed data over the network. ## 5. Testing ### 5.1. Unit Testing **Standard:** Write unit tests for API client modules to verify their functionality and error handling. Use mocking frameworks to simulate API responses. **Why:** Ensures the API client works as expected and handles different API scenarios correctly. **Do This:** Create unit tests for API client functions. Use mocking frameworks to simulate API responses and errors. **Don't Do This:** Skip unit testing for API clients. **Example:** Demonstrating usage of interfaces and mocking for API client. """zig // api_interface.zig const std = @import("std"); pub const ApiInterface = type { pub fn get(self: *@This(), endpoint: []const u8, response_buffer: []u8) ![]const u8; pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !@This(); pub fn deinit(self: *@This()) void; }; """ """zig // api_client.zig const std = @import("std"); const ApiInterface = @import("api_interface.zig").ApiInterface; pub const ApiClient = struct { client: std.http.Client, base_url: []const u8, allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !ApiClient { var client = std.http.Client.init(allocator); return ApiClient{ .client = client, .base_url = base_url, .allocator = allocator, }; } pub fn deinit(self: *ApiClient) void { self.client.deinit(); } pub fn get(self: *ApiClient, endpoint: []const u8, response_buffer: []u8) ![]const u8 { const url = try std.mem.concat(self.client.allocator, u8, self.base_url, endpoint); defer self.client.allocator.free(url); var request = std.http.Client.Request{ .method = .GET, .url = url, .headers = .{}, // Define headers here if needed .body = null, // No body for GET requests }; var response = try self.client.request(request); defer response.deinit(); if (response.status != .ok) { return error.HttpRequestFailed; } // Read the response body into the provided buffer. Handle partial reads! var total_read: usize = 0; while (total_read < response_buffer.len) { const read_result = response.body.read(response_buffer[total_read..]); if (read_result) |bytes_read| { total_read += bytes_read; if (bytes_read == 0) { break; // End of stream. } } else |err| { if (err == error.EndOfStream) { break; // Expected end of stream } else { return err; // Real error } } } return response_buffer[0..total_read]; } pub fn interface(self: *ApiClient) ApiInterface { return .{ .init = init_api_client, .get = get_api_client, .deinit = deinit_api_client, }; } fn init_api_client(allocator: std.mem.Allocator, base_url: []const u8) !@This() { return ApiClient.init(allocator, base_url); } fn get_api_client(self: *ApiClient, endpoint: []const u8, response_buffer: []u8) ![]const u8{ return self.get(endpoint, response_buffer); } fn deinit_api_client(self: *ApiClient) void { self.deinit(); } }; """ """zig // api_client_test.zig const std = @import("std"); const testing = std.testing; const ApiInterface = @import("api_interface.zig").ApiInterface; // Include the REAL ApiClient library. Ideally you'd only define and use the interface, and not couple clients const ApiClient = @import("api_client.zig").ApiClient; // Mock API Client const MockApiClient = struct { expected_response: []const u8, allocator: std.mem.Allocator, // Required since dupe requires allocator pub fn init(allocator: std.mem.Allocator, expected_response: []const u8) MockApiClient { return .{ .expected_response = expected_response, .allocator = allocator, }; } pub fn get(self: *MockApiClient, endpoint: []const u8, response_buffer: []u8) ![]const u8 { _ = endpoint; // Unused parameter if (response_buffer.len < self.expected_response.len) { return error.BufferTooSmall; } @memcpy(response_buffer[0..self.expected_response.len], self.expected_response); return response_buffer[0..self.expected_response.len]; } pub fn interface(self: *MockApiClient) ApiInterface { return .{ .init = init_mock_client, .get = get_mock_client, .deinit = deinit_mock_client, }; } fn init_mock_client(allocator: std.mem.Allocator, base_url: []const u8) !@This() { _ = base_url; //Unused parameter. return MockApiClient.init(allocator, "expected response"); } fn get_mock_client(self: *MockApiClient, endpoint: []const u8, response_buffer: []u8) ![]const u8{ return self.get(endpoint, response_buffer); } fn deinit_mock_client(self: *MockApiClient) void { _ = self; // Nothing to deinit in mock } pub fn deinit(self: *MockApiClient) void { _ = self; // Nothing to deinit in mock } }; fn testApiClient(api: anytype) !void { var buffer: [1024]u8 = undefined; const response = try api.get("/test", &buffer); try testing.expectEqualSlices(u8, "expected response", response); } test "API Client: Mock should return mocked data" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var mock_api_client: MockApiClient = MockApiClient.init(allocator, "expected response"); var api = mock_api_client.interface(); try testApiClient(api); } test "API Client: Can call methods from the interface directly"{ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var mock_api_client: MockApiClient = MockApiClient.init(allocator, "expected response"); var api = mock_api_client.interface(); var buffer: [1024]u8 = undefined; const response = try api.get(&mock_api_client, "/test", &buffer); try testing.expectEqualSlices(u8, "expected response", response); } """ ### 5.2. Integration Testing **Standard:** Perform integration tests to verify the interaction between the API client and the actual API. Use test environments or mock servers to avoid impacting production systems. **Why:** Ensures that the API integration works correctly in a real-world environment. **Do This:** Set up test environments or mock servers. Write integration tests to verify data flow and error handling. **Don't Do This:** Test directly against production APIs without proper safeguards. ## 6. Documentation ### 6.1. API Client Documentation **Standard:** Document the API client module thoroughly, including its purpose, usage, and limitations. Use Zig's documentation syntax (e.g., "///") to generate API documentation. **Why:** Enables other developers to understand and use the API client effectively. **Do This:** Write clear and concise documentation for all API client functions and data structures. Use examples to demonstrate common usage scenarios. **Don't Do This:** Neglect to document the API client module. **Example:** """zig const std = @import("std"); /// Represents a user from the API. pub const User = struct { /// The user's unique identifier. id: u64, /// The user's name. name: []const u8, /// The user's email address. email: []const u8, /// The user's age. age: u8, /// Indicates if the user is active. is_active: bool, }; /// Fetches a user from the API by ID. /// /// Parameters: /// id: The ID of the user to fetch. /// /// Returns: /// A "User" struct if the user is found, or null if not found. pub fn getUser(id: u64) ?User { _ = id; // Suppress unused variable warning. Real code would use parameter. return null; } """ ### 6.2. API Usage Examples **Standard:** Provide clear and concise examples of how to use the API client in different scenarios. Include examples for common operations like retrieving data, creating resources, and handling errors. **Why:** Helps developers quickly understand how to integrate with the API in their applications. **Do This:** Provide example code snippets for common API usage scenarios. Include examples of error handling and data validation. By adhering to these standards, Zig developers can create robust, secure, and performant API integrations. This comprehensive guide ensures code maintainability, readability, and adherence to the latest Zig best practices, significantly aiding both human developers and AI coding assistants.