# Zig API Reference for 2025 (0.14.0 - 0.15.x)
Purpose: Quick reference for LLMs and developers writing fresh Zig code in 2025. Focus on "how to do things correctly now" rather than migration from old APIs.
Current Version: Zig 0.15.x (as of late 2025)
Sources:
- ArrayList (Unmanaged by Default)
- I/O: Writers and Readers
- Build System
- Type Reflection
- Language Features
- Standard Library Changes
- Allocator API
- Format Strings
- Containers
- Quick Reference Table
- Signal Handling (POSIX)
Key Change: std.ArrayList is now unmanaged. Pass the allocator to each method call.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Initialize with empty struct literal
var list: std.ArrayListUnmanaged(i32) = .{};
defer list.deinit(allocator);
// Pass allocator to every mutating operation
try list.append(allocator, 42);
try list.append(allocator, 100);
try list.appendSlice(allocator, &[_]i32{ 1, 2, 3 });
// Non-mutating operations don't need allocator
for (list.items) |item| {
std.debug.print("{d}\n", .{item});
}
// toOwnedSlice needs allocator
const owned = try list.toOwnedSlice(allocator);
defer allocator.free(owned);
}// Initialization
var list: std.ArrayListUnmanaged(T) = .{};
// Cleanup
list.deinit(allocator);
// Adding items
try list.append(allocator, item);
try list.appendSlice(allocator, slice);
try list.insert(allocator, index, item);
// Removing items (no allocator needed)
_ = list.pop();
_ = list.popOrNull();
_ = list.orderedRemove(index);
_ = list.swapRemove(index);
// Capacity management
try list.ensureTotalCapacity(allocator, n);
try list.ensureUnusedCapacity(allocator, n);
list.clearRetainingCapacity();
list.shrinkRetainingCapacity(new_len);
// Conversion
const slice = try list.toOwnedSlice(allocator);const MyStruct = struct {
items: std.ArrayListUnmanaged(u8),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) MyStruct {
return .{
.items = .{},
.allocator = allocator,
};
}
pub fn deinit(self: *MyStruct) void {
self.items.deinit(self.allocator);
}
pub fn add(self: *MyStruct, item: u8) !void {
try self.items.append(self.allocator, item);
}
};Key Change: Complete overhaul in 0.15. Buffers are now in the interface, not the implementation.
const std = @import("std");
pub fn main() !void {
// Create buffer for writer
var stdout_buf: [4096]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
const stdout = &stdout_writer.interface;
var stderr_buf: [4096]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
const stderr = &stderr_writer.interface;
// Use the writer
try stdout.print("Hello, {s}!\n", .{"world"});
try stderr.print("Error: {s}\n", .{"something went wrong"});
// IMPORTANT: Flush before exit!
try stdout.flush();
try stderr.flush();
}pub fn readFile(path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
// Read entire file into allocated memory
return try file.readToEndAlloc(allocator, max_size);
}pub fn writeFile(path: []const u8, data: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(data);
}const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Create module first, then executable
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
// Run step
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_cmd.step);
}const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "mylib", .module = mylib_module },
},
}),
});const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);// Use addLibrary with .linkage = .static (not addStaticLibrary!)
const static_lib = b.addLibrary(.{
.name = "mylib",
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(static_lib);
// Optional: install header for C consumers
b.installFile("src/mylib.h", "include/mylib.h");
// Create a library-only build step
const lib_step = b.step("lib", "Build only the static library");
lib_step.dependOn(&static_lib.step);Note: addStaticLibrary() no longer exists in Zig 0.15. Use addLibrary() with .linkage = .static instead.
Key Change: All std.builtin.Type tags are now lowercase. Keywords need @"" syntax.
fn inspectType(comptime T: type) void {
switch (@typeInfo(T)) {
.int => |info| {
std.debug.print("Int: {d} bits, signed={}\n", .{ info.bits, info.signedness == .signed });
},
.float => |info| {
std.debug.print("Float: {d} bits\n", .{info.bits});
},
.pointer => |info| {
std.debug.print("Pointer size: {}\n", .{info.size});
},
.@"struct" => |info| { // Note: @"struct" for keyword
std.debug.print("Struct with {d} fields\n", .{info.fields.len});
},
.@"enum" => |info| {
std.debug.print("Enum with {d} fields\n", .{info.fields.len});
},
.@"union" => |info| {
std.debug.print("Union with {d} fields\n", .{info.fields.len});
},
else => {},
}
}// Lowercase now
comptime {
std.debug.assert(@typeInfo(*u8).pointer.size == .one);
std.debug.assert(@typeInfo([*]u8).pointer.size == .many);
std.debug.assert(@typeInfo([]u8).pointer.size == .slice);
}fn unlikelyPath() void {
@branchHint(.cold);
// ... error handling code
}
fn likelyPath() void {
@branchHint(.likely);
// ... hot path
}fn myFunction() void {}
comptime {
@export(&myFunction, .{ .name = "my_exported_function" });
}fn stateMachine(input: []const u8) void {
var state: enum { start, reading, done } = .start;
state_loop: switch (state) {
.start => {
state = .reading;
continue :state_loop;
},
.reading => {
// process...
state = .done;
continue :state_loop;
},
.done => {},
}
}const Point = struct {
x: i32,
y: i32,
};
// Cleaner initialization
const p: Point = .{ .x = 10, .y = 20 };
// In function calls
fn draw(point: Point) void { ... }
draw(.{ .x = 5, .y = 10 });| Old | New |
|---|---|
std.rand |
std.Random |
std.TailQueue |
std.DoublyLinkedList |
std.zig.CrossTarget |
std.Target.Query |
std.fs.MAX_PATH_BYTES |
std.fs.max_path_bytes |
// Use specific variants
var iter = std.mem.tokenizeScalar(u8, input, ' ');
while (iter.next()) |token| {
// ...
}
// Or for sequences
var iter2 = std.mem.tokenizeSequence(u8, input, "\r\n");// UTF-16 functions now capitalize "Le"
std.unicode.utf16LeToUtf8(...); // Note: Le not le// Runtime page size (not compile-time constant anymore)
const page_size = std.heap.pageSize();
// Compile-time bounds for generic code
const min_page = std.heap.page_size_min;
const max_page = std.heap.page_size_max;// VTable functions use std.mem.Alignment, not u8
const alignment: std.mem.Alignment = .@"16";| Specifier | Purpose |
|---|---|
{t} |
Tag names and error names |
{d} |
Custom number formatting (via formatNumber method) |
{b64} |
Standard base64 output |
{f} |
Required for custom format methods |
const MyType = struct {
value: i32,
// New signature - takes *std.Io.Writer directly
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("MyType({d})", .{self.value});
}
};
// Usage requires {f} specifier
std.debug.print("{f}\n", .{my_instance});const std = @import("std");
const MyData = struct {
node: std.DoublyLinkedList.Node = .{},
value: i32,
fn fromNode(node: *std.DoublyLinkedList.Node) *MyData {
return @fieldParentPtr("node", node);
}
};
var list: std.DoublyLinkedList = .{};
var item = MyData{ .value = 42 };
list.append(&item.node);
// Iterate
var it = list.first;
while (it) |node| : (it = node.next) {
const data = MyData.fromNode(node);
std.debug.print("{d}\n", .{data.value});
}// Use AutoHashMap for simple cases
var map = std.AutoHashMap([]const u8, i32).init(allocator);
defer map.deinit();
try map.put("key", 42);
if (map.get("key")) |value| {
std.debug.print("Found: {d}\n", .{value});
}| Task | Zig 0.15+ Code |
|---|---|
| ArrayList init | var list: std.ArrayListUnmanaged(T) = .{}; |
| ArrayList append | try list.append(allocator, item); |
| ArrayList deinit | list.deinit(allocator); |
| stdout writer | var buf: [4096]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); const stdout = &w.interface; |
| Print to stdout | try stdout.print("{s}\n", .{msg}); try stdout.flush(); |
| Type switch | .int, .float, .@"struct", .@"enum" |
| Cold function | @branchHint(.cold); |
| Export symbol | @export(&func, .{ .name = "name" }); |
| Page size | std.heap.pageSize() (runtime) |
| Build executable | b.addExecutable(.{ .name = "x", .root_module = b.createModule(.{ ... }) }); |
| Build static lib | b.addLibrary(.{ .name = "x", .linkage = .static, .root_module = ... }); |
- Forgetting to flush stdout/stderr - Always call
try stdout.flush()before program exit - Using
ArrayList.init(allocator)- This doesn't exist anymore; use.{}empty literal - Forgetting allocator in append/deinit - Every mutating ArrayList operation needs the allocator
- Using uppercase type tags -
.Intis now.int,.Structis now.@"struct" - Using
std.io.getStdOut()- Usestd.fs.File.stdout().writer(&buffer)instead - Using old build.zig patterns - Use
root_modulewithb.createModule(), notroot_source_filedirectly - Using
addStaticLibrary()- This doesn't exist in 0.15; useaddLibrary(.{ .linkage = .static, ... })instead
Key Points: Use std.posix.Sigaction for cross-platform POSIX signal handling. The calling convention is .c (lowercase).
const std = @import("std");
const posix = std.posix;
// Global flag for cooperative cancellation
var g_cancel_flag = std.atomic.Value(bool).init(false);
// Signal handler - use .c calling convention (lowercase!)
fn sigintHandler(_: c_int) callconv(.c) void {
// Signal handlers should avoid allocations - use direct write
const msg = "\nCancelling...\n";
_ = posix.write(posix.STDERR_FILENO, msg) catch {};
g_cancel_flag.store(true, .release);
}
fn setupSignalHandler() void {
const act = posix.Sigaction{
.handler = .{ .handler = sigintHandler },
.mask = std.mem.zeroes(posix.sigset_t), // Empty signal mask
.flags = 0,
};
// Note: sigaction returns void, not an error
posix.sigaction(posix.SIG.INT, &act, null);
}
pub fn main() !u8 {
setupSignalHandler();
// Check cancel flag periodically in your work loop
while (!g_cancel_flag.load(.acquire)) {
// ... do work ...
std.Thread.sleep(50 * std.time.ns_per_ms);
}
return 130; // Standard exit code for Ctrl+C
}- Calling convention: Use
.c(lowercase), not.C - Empty sigset: Use
std.mem.zeroes(posix.sigset_t)-empty_sigsetwas removed - Return type:
posix.sigaction()returns void, not an error union - Signal safety: Avoid allocations in signal handlers; use
posix.write()for output - Atomic flags: Use
std.atomic.Value(bool)for thread-safe cancel flags
posix.SIG.INT // Ctrl+C (interrupt)
posix.SIG.TERM // Termination request
posix.SIG.HUP // Hangup
posix.SIG.USR1 // User-defined signal 1
posix.SIG.USR2 // User-defined signal 2Last updated: January 2026 Zig version: 0.15.2