History

  • How:
    • Zig was created as a modern replacement for C, focusing on safety, readability, and performance.
    • Developed by Andrew Kelley in 2015, Zig aims to be a “robust, optimal, and clear” programming language.
    • It has evolved through a community-driven open-source model under the Zig Software Foundation (ZSF).
    • Key design philosophies:
      • No hidden control flow (no operator overloading, no exceptions, no properties).
      • No hidden memory allocation (allocators are passed explicitly to functions).
      • No preprocessor macros; compile-time code execution is done via comptime in the language itself.
      • Native interoperability with C code and build tool integration.
  • Who:
    • Created by Andrew Kelley and maintained by the Zig Software Foundation along with global open-source contributors.
  • Why:
    • To address the flaws of C (undefined behavior, unsafe defaults, complex macros) without adding the hidden overhead, implicit runtime allocations, or learning curves of C++ or Rust.

Introduction

Advantages

  • No Hidden Control Flow — What you see is what executes. No properties, no operator overloading, no exceptions, no RAII destructor magic.
  • Explicit Memory Allocation — Functions that require memory must accept an allocator argument. You can easily track, limit, and swap allocators (e.g., GeneralPurposeAllocator vs ArenaAllocator).
  • Comptime Metaprogramming — Execute any valid Zig code at compile-time. Replaces macros, templates, and generics with standard Zig code.
  • Seamless C Interoperability — Directly @cImport headers, link C libraries, and translate C code to Zig. Zig is also a drop-in C/C++ compiler (zig cc).
  • Comprehensive Build Tool — The build system is written in Zig itself, replacing make, cmake, and shell scripts.

Disadvantages

  • Young Language — Still in pre-1.0 development, meaning syntax and standard library APIs are subject to breaking changes between versions.
  • Manual Memory Management — While much safer than C (with bounds checking and slice concepts), the developer must manually pair allocations with defer allocator.free().
  • Smaller Library Ecosystem — Fewer third-party libraries compared to mature ecosystems like C++, Go, or Rust.

Remember Points

  • Unused Variables — Zig compilers treat unused variables and imports as compile-time errors.
  • Errors are Values — Error handling does not unwind the stack. Errors are returned as normal values in error unions.
  • Undefined State — You can explicitly initialize variables to undefined if they will be written later, but this must be done intentionally.

Basics

Variables and Constants

const std = @import("std");
 
pub fn main() void {
    // Constants use 'const' and cannot be modified after initialization
    const max_size: u32 = 1024;
    
    // Variables use 'var' and must specify or infer their type
    var current_count: i32 = 0;
    current_count += 1;
    
    // Type inference is supported when allocating constants/variables
    const ratio = 3.14159; // inferred as f64
    
    // Variables can be initialized as undefined (uninitialized memory)
    var uninit_buffer: [100]u8 = undefined;
    uninit_buffer[0] = 42;
    
    std.debug.print("Size: {}, Count: {}, Ratio: {}\n", .{ max_size, current_count, ratio });
}

Basic Primitive Types

  • Integers: Signed (i8, i16, i32, i64, i128, isize) and Unsigned (u8, u16, u32, u64, u128, usize).
  • Floats: f16, f32, f64, f80, f128.
  • Booleans: bool (true or false).
  • Arbitrary Bit-Width Integers: u3 (3-bit unsigned), i29 (29-bit signed integer). Perfect for hardware register mapping.

Control Flow

Conditionals: if and switch

const std = @import("std");
 
pub fn main() void {
    const score: u32 = 85;
    
    // 1. If-Else statement
    if (score >= 90) {
        std.debug.print("Grade: A\n", .{});
    } else if (score >= 80) {
        std.debug.print("Grade: B\n", .{});
    } else {
        std.debug.print("Grade: F\n", .{});
    }
    
    // 2. If expression (evaluates to a value, similar to ternary operator)
    const pass = if (score >= 50) true else false;
    
    // 3. Switch statement (Must cover all cases exhaustively!)
    switch (score) {
        0...49 => std.debug.print("Failed\n", .{}),
        50...79 => std.debug.print("Pass\n", .{}),
        80...100 => std.debug.print("Excellent!\n", .{}),
        else => std.debug.print("Invalid Score\n", .{}),
    }
}

Loops: while and for

const std = @import("std");
 
pub fn main() void {
    // 1. While loop
    var i: usize = 0;
    while (i < 5) : (i += 1) { // : (i += 1) is the update expression run at loop end
        std.debug.print("While: {}\n", .{i});
    }
    
    // 2. For loop over arrays/slices (captures element pointer or value)
    const array = [_]i32{ 10, 20, 30, 40 };
    for (array, 0..) |value, index| {
        std.debug.print("Index {}: Value {}\n", .{ index, value });
    }
    
    // 3. Multi-sequence for loop (iterates over multiple arrays of the same length)
    const chars = [_]u8{ 'a', 'b', 'c', 'd' };
    for (array, chars) |num, char| {
        std.debug.print("{} -> {c}\n", .{ num, char });
    }
}

Defer and Errdefer

Resource Cleanup Mechanics

const std = @import("std");
 
fn processFile() !void {
    std.debug.print("Opening resource...\n", .{});
    
    // 'defer' schedules an expression to run when exiting the current block scope
    defer std.debug.print("Closing resource (always runs)...\n", .{});
    
    var has_error = true;
    if (has_error) {
        // 'errdefer' schedules cleanup to run ONLY if the block exits returning an error
        errdefer std.debug.print("Rollback changes (runs on error only)!\n", .{});
        
        return error.FailedExecution;
    }
}
 
pub fn main() void {
    processFile() catch |err| {
        std.debug.print("Caught: {}\n", .{err});
    };
}

Errors as Values

Error Sets and Unions

const std = @import("std");
 
// Define an error set
const FileError = error{
    FileNotFound,
    PermissionDenied,
    OutOfMemory,
};
 
// A function returning an Error Union (FileError or u32)
fn checkFile(path_exists: bool) FileError!u32 {
    if (!path_exists) {
        return FileError.FileNotFound;
    }
    return 42;
}
 
pub fn main() void {
    // 1. try keyword: Unwraps value or bubbles up the error to caller
    const val = checkFile(true) catch 0;
    
    // 2. catch keyword: Handles the error or provides a fallback value
    const val2 = checkFile(false) catch |err| {
        std.debug.print("Error encountered: {}\n", .{err});
        return;
    };
    
    // 3. if-let error checking
    if (checkFile(true)) |unwrapped| {
        std.debug.print("File ID: {}\n", .{unwrapped});
    } else |err| {
        std.debug.print("Failed: {}\n", .{err});
    }
}

Pointers and Slices

Pointer Types and Dereferencing

const std = @import("std");
 
pub fn main() void {
    var value: i32 = 42;
    
    // Single-item pointer type is '*T'
    const ptr: *i32 = &value;
    
    // Dereference using '.*'
    ptr.* = 100;
    std.debug.print("Modified value: {}\n", .{value});
    
    // Constant pointer (*const T)
    const const_val: i32 = 50;
    const const_ptr: *const i32 = &const_val;
    // const_ptr.* = 10; -- Compile error! Cannot modify const pointer target.
    
    // Slices ([]T) store a pointer and a length
    var array = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
    const slice: []u8 = array[1..4]; // captures "ell"
    
    std.debug.print("Slice length: {}, items: {s}\n", .{ slice.len, slice });
}

Memory Allocators

Allocator Interfaces

  • Zig has no global garbage collector or implicit heap allocation. All heap memory goes through an explicit allocator.
const std = @import("std");
 
pub fn main() !void {
    // 1. GeneralPurposeAllocator (GPA): Detects leaks, double-frees, out-of-bounds access
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit(); // Check for memory leaks at program exit
    
    const allocator = gpa.allocator();
    
    // Allocate dynamic array
    const buffer = try allocator.alloc(u32, 10);
    defer allocator.free(buffer); // Clean up allocated memory
    
    for (buffer, 0..) |*item, idx| {
        item.* = @intCast(idx * 10);
    }
    
    // 2. ArenaAllocator: Allocates rapidly, frees everything at once
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit(); // Frees ALL allocations made through this arena
    const arena_allocator = arena.allocator();
    
    const node1 = try arena_allocator.create(i32);
    const node2 = try arena_allocator.create(i32);
    node1.* = 1;
    node2.* = 2;
}

Comptime (Compile-time Meta)

Type Generation and Execution

const std = @import("std");
 
// 1. Comptime evaluation of functions
fn factorial(n: u32) u32 {
    return if (n == 0) 1 else n * factorial(n - 1);
}
 
// 2. Generics: A function returning a type at compile-time
fn GenericStack(comptime T: type) type {
    return struct {
        items: [10]T,
        count: usize = 0,
        
        const Self = @This();
        
        pub fn push(self: *Self, item: T) void {
            self.items[self.count] = item;
            self.count += 1;
        }
    };
}
 
pub fn main() void {
    // Evaluated entirely at compile-time!
    const fact_10 = comptime factorial(10);
    
    // Instantiate a Generic struct
    var int_stack = GenericStack(i32){ .items = undefined };
    int_stack.push(100);
    
    std.debug.print("Fact: {}, Stack Top: {}\n", .{ fact_10, int_stack.items[0] });
}

C Interoperability

Importing C Directly

// Import standard C headers directly
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
});
 
pub fn main() void {
    const text = "Hello from C stdio!\n";
    _ = c.printf(text);
    
    // Allocate memory using C malloc
    const ptr = c.malloc(10);
    defer c.free(ptr);
}
  • Use zig translate-c code.c to automatically generate the equivalent Zig interface files.

Build Tools

Standard Build System

  • Instead of Makefile or CMake, Zig uses a build.zig script written in Zig itself.
// build.zig example
const std = @import("std");
 
pub fn build(b: *std.Build) void {
    // Determine compilation target and optimization mode
    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,
    });
    
    b.installArtifact(exe);
}
  • Key Commands:
    • zig build — Builds the application according to the rules in build.zig.
    • zig run main.zig — Directly compiles and runs the file without setting up a build directory.
    • zig test main.zig — Discovers and executes all unit tests (test "name" { ... } blocks) declared in the file.

More Learn

Explore valuable resources for Zig: