Skip to content

Instantly share code, notes, and snippets.

@clifinger
Created August 16, 2025 01:53
Show Gist options
  • Save clifinger/b681f5f29ad9b4761dbeb4f76bb76c00 to your computer and use it in GitHub Desktop.
Save clifinger/b681f5f29ad9b4761dbeb4f76bb76c00 to your computer and use it in GitHub Desktop.
Zig: Handling CLI Arguments with a Debug/Release Allocator
///
/// A robust pattern for initializing a Command-Line (CLI) application in Zig.
///
/// This Gist demonstrates the idiomatic way to handle program arguments by
/// intelligently selecting a memory allocator at compile-time based on the
/// build mode.
///
/// Key Concepts Shown:
///
/// 1. Compile-Time Allocator Selection:
/// - In Debug/ReleaseSafe builds, it uses `std.heap.DebugAllocator` to
/// automatically detect memory leaks.
/// - In ReleaseFast/ReleaseSmall builds, it uses `std.heap.smp_allocator`
/// for maximum performance.
///
/// 2. Labeled Blocks:
/// The `gpa: { ... }` syntax is used to cleanly initialize multiple
/// constants (`gpa` and `is_debug`) from a single expression.
///
/// 3. Guaranteed Cleanup with `defer`:
/// Ensures that memory allocated for arguments is always freed and that the
/// DebugAllocator is properly de-initialized, even if errors occur.
///
const std = @import("std");
const builtin = @import("builtin");
// We create a DebugAllocator instance that will be used in debug builds.
// Its job is to wrap another allocator and check for leaks.
var debug_allocator: std.heap.DebugAllocator(.{}) = .init{};
pub fn main() !void {
// This is the core of the pattern: a compile-time allocator selection.
// We use a labeled block to initialize two constants at once:
// - gpa: The General Purpose Allocator for our application.
// - is_debug: A boolean flag we can use for conditional logic.
const gpa, const is_debug = gpa: {
break :gpa switch (builtin.mode) {
// In debug/safe modes, wrap our allocator in the leak checker.
.Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
// In release modes, use a high-performance allocator directly.
// `smp_allocator` is a good general-purpose, thread-safe choice.
.ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
};
};
// This deferred statement ensures our resources are cleaned up before main exits.
// The `if` is crucial: we only deinit the DebugAllocator if we actually used it.
// At the end of its life, it will check for any memory leaks.
defer if (is_debug) {
_ = debug_allocator.deinit();
};
// Allocate a slice for the command-line arguments using our chosen allocator (gpa).
const args = try std.process.argsAlloc(gpa);
// No matter what happens next, we schedule the arguments' memory to be freed.
// It's crucial to use the *same allocator* for freeing as for allocating.
defer std.process.argsFree(gpa, args);
// Print some info to demonstrate which mode is active.
// Try running with: `zig build run` and `zig build run -Drelease-fast`
std.debug.print("Debug allocator in use: {}\n", .{is_debug});
std.debug.print("Executing in mode: {s}\n", .{@tagName(builtin.mode)});
// A small piece of application logic to demonstrate using the parsed arguments.
if (args.len > 1) {
std.debug.print("Changing directory to: {s}\n", .{args[1]});
// Note: Using `chdir` might fail. A real app would handle the error.
_ = std.os.linux.chdir(args[1]);
} else {
std.debug.print("No directory argument provided.\n", .{});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment