Appendix B — Appendix B: Migrating from Zig 0.14 to 0.15
Quick reference for upgrading codebases from Zig 0.14.x to 0.15.2
B.1 Overview
This appendix provides a quick-reference guide for migrating code from Zig 0.14.x to 0.15.2. For detailed explanations and production examples, see the main chapters.
Estimated migration time: 2-4 hours for typical projects (< 10,000 lines)
Breaking Changes at a Glance
| Change | Impact | Quick Fix |
|---|---|---|
Build system: root_module required |
Every build.zig | Wrap in b.createModule(.{...}) |
| I/O API: Explicit buffering | All I/O code | Add buffer parameter to writer() |
| ArrayList: Unmanaged default | All container code | Pass allocator to methods |
B.2 Build System Migration
The Change
addExecutable(), addTest(), and addLibrary() now require .root_module field.
Before (0.14.x)
After (0.15.2)
Migration Steps
- Add
.root_module = b.createModule(.{wrapper - Move
root_source_file,target,optimizeinsidecreateModule() - Close with
}),
Common error:
error: missing struct field: root_module
→ Add the .root_module field as shown above.
B.3 I/O and Writer Migration
The Changes
- Location change:
std.io.getStdOut()→std.fs.File.stdout() - Buffer required:
writer()now requires a buffer parameter - Interface accessor: Methods accessed via
.interfacefield - Flush required: Must call
flush()before close
Before (0.14.x)
After (0.15.2)
Buffer Sizing Guide
| Use Case | Buffer Size | Example |
|---|---|---|
| stdout/stderr | 256-1024 bytes | var buf: [256]u8 = undefined; |
| File I/O | 4096 bytes | var buf: [4096]u8 = undefined; |
| Error messages | Unbuffered | writer(&.{}) |
Migration Steps
- Replace
std.io.getStdOut()→std.fs.File.stdout() - Add buffer:
var buf: [4096]u8 = undefined; - Pass to writer:
writer(&buf) - Add
.interfaceto method calls:writer.interface.print() - Add
flush()before close/exit
Common error:
error: no field named 'print' in struct 'fs.File.Writer'
→ Use writer.interface.print() instead of writer.print()
Critical mistake: Forgetting flush() causes silent data loss!
B.4 ArrayList Migration
The Change
ArrayList(T) is now unmanaged by default (no stored allocator). All mutation methods require explicit allocator parameter.
Before (0.14.x)
After (0.15.2)
Migration Steps
.init(allocator)→.empty(or.{}).deinit()→.deinit(allocator)- Add
allocatoras first parameter to:append,appendSlice,insert,resize, etc.
Common errors:
error: expected 2 arguments, found 1
defer list.deinit();
→ Pass allocator: defer list.deinit(allocator);
error: expected 3 arguments, found 2
try list.append(42);
→ Pass allocator: try list.append(allocator, 42);
Memory Savings
Unmanaged containers save 8 bytes per instance (no allocator pointer):
B.5 HashMap Migration
Same pattern as ArrayList - pass allocator to mutation methods:
Before (0.14.x)
After (0.15.2)
B.6 Common Pitfalls
1. Forgetting flush() - Silent Data Loss
Symptom: File is incomplete or empty, no error message
Fix:
2. Wrong Import Path for stdout/stderr
Symptom:
error: no field named 'getStdOut' in struct 'std.io'
Fix:
3. Missing Allocator in deinit()
Symptom:
error: expected 2 arguments, found 1
Fix:
4. Missing .interface Accessor
Symptom:
error: no field named 'print' in struct 'fs.File.Writer'
Fix:
5. Buffer Lifetime Issues
Problem: Buffer destroyed before use
// ❌ WRONG - Stack allocation
fn getWriter(file: std.fs.File) Writer {
var buf: [256]u8 = undefined; // Destroyed on return!
return file.writer(&buf);
}
// ✅ CORRECT - Buffer outlives writer
const FileWriter = struct {
buffer: [4096]u8 = undefined,
writer: std.fs.File.Writer,
fn init(file: std.fs.File) FileWriter {
var self: FileWriter = undefined;
self.writer = file.writer(&self.buffer);
return self;
}
};B.7 Migration Checklist
Pre-Migration (15 min)
Phase 1: Build System (15-30 min)
Phase 2: I/O (30-60 min)
Phase 3: Containers (30-60 min)
Final Validation (30-60 min)
Total time: 2-4 hours for typical projects
B.8 Quick Find-Replace Patterns
Build system:
Find: .target = target,\n .optimize = optimize,
Replace: .root_module = b.createModule(.{\n .target = target,\n .optimize = optimize,
(Then manually add closing }),)
I/O:
Find: std.io.getStdOut()
Replace: std.fs.File.stdout()
Find: std.io.getStdErr()
Replace: std.fs.File.stderr()
Containers:
Find: ArrayList(.*).init\(allocator\)
Replace: ArrayList$1.empty
(Then manually add allocator parameters)
B.9 Example Migration
Complete file migration example:
Before (0.14.x):
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const stdout = std.io.getStdOut().writer();
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.appendSlice("Hello, World!");
try stdout.print("{s}\n", .{list.items});
}After (0.15.2):
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok);
const allocator = gpa.allocator();
const stdout = std.fs.File.stdout();
var buf: [256]u8 = undefined;
var writer = stdout.writer(&buf);
var list = std.ArrayList(u8).empty;
defer list.deinit(allocator);
try list.appendSlice(allocator, "Hello, World!");
try writer.interface.print("{s}\n", .{list.items});
try writer.interface.flush();
}B.10 Resources
For detailed explanations: - Chapter 5: Collections & Containers (managed vs unmanaged) - Chapter 6: I/O, Streams & Formatting (buffering patterns) - Chapter 9: Build System (module system)
For working code examples: - examples/ directory contains 100 validated 0.15.2 examples - All examples compile successfully on Zig 0.15.2
If migration issues persist: - Zig Discord: Real-time help - Ziggit forum: Migration questions - GitHub issues: Report unclear error messages
Appendix B Complete
For quick-reference patterns working with 0.14.1 code, see Appendix A.