Reversing Strings in Zig

In this post, we will build a simple command-line tool: a string reverser. It takes a string as input via the command line and prints it backward. This humble example will showcase how to wield the new Writer for stdout output, handle command-line arguments, and manage memory with Zig’s arenas and allocators. Along the way, we will touch on why this new API feels so idiomatic and why it is a game-changer for Zig developers.

If you are new to Zig, fear not—this code is beginner-friendly but packs in real-world patterns. Let’s dive in!

The Setup: Allocators and Command-Line Args Link to heading

Every Zig program starts with imports and a main() function. We kick off with the standard library:

const std = @import("std");

Our main() uses an error union (!void) to handle potential failures gracefully—Zig’s way of making errors explicit and composable.

First, we set up memory management. Zig does nt have a global heap like some languages; you explicitly choose an allocator. Here, we use a GeneralPurposeAllocator (GPA) for its debugging features in development:

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

The defer ensures cleanup, even on early returns. Next, we grab command-line arguments. In Zig 0.15+, std.process.argsAlloc allocates an array of null-terminated strings:

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

Note the paired argsFree—Zig’s explicit deallocation keeps things leak-free. We check if an argument is provided; if not, we print usage and bail.

Output with the New std.Io.Writer Link to heading

Here’s where the magic of the new I/O shines. In pre-0.15 Zig, stdout handling was clunky—mixing std.io.getStdOut() with wrappers like BufferedWriter. Now, it’s streamlined:

    var buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&buffer);
    const stdout = &stdout_writer.interface;
  • We define a fixed-size buffer ([4096]u8) on the stack—no heap allocation for this common case.
  • std.fs.File.stdout().writer(&buffer) creates a buffered writer, embedding the new std.Io.Writer interface.
  • The &stdout_writer.interface gives us a pointer to the concrete Writer struct, ready for methods like print and flush.

This setup integrates buffering directly into the interface: small writes accumulate in the buffer, and flush drains to the OS only when needed. It’s optimizer-friendly and reduces syscalls.

For the usage message:

    if (args.len < 2) {
        try stdout.print("Usage: {s} <string>\n", .{std.fs.path.basename(args[0])});
        try stdout.flush();
        return;
    }

print uses Zig’s comptime formatting—efficient and type-safe. We extract the basename of the executable with std.fs.path.basename for a clean “Usage: reverse ”.

The Reversal Logic: Simple and Efficient Link to heading

The core of our program is a backward loop over the input string:

    const input = args[1];

    var i = input.len;
    while (i > 0) : (i -= 1) {
        try stdout.print("{c}", .{input[i - 1]});
    }
    try stdout.print("\n", .{});
    try stdout.flush();
  • Strings in Zig are slices ([]const u8), so input.len and input[i-1] access bytes directly.
  • We loop from len down to 1, printing each character in reverse with "{c}" for single bytes.
  • No temporary allocations—just direct output. The buffer handles batching.
  • Final newline and flush ensure everything hits the terminal.

This is idiomatic Zig: no hidden costs, explicit control, and zero-copy where possible.

The Full Code Link to heading

Putting it all together:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    var buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&buffer);
    const stdout = &stdout_writer.interface;

    if (args.len < 2) {
        try stdout.print("Usage: {s} <string>\n", .{std.fs.path.basename(args[0])});
        try stdout.flush();
        return;
    }

    const input = args[1];

    var i = input.len;
    while (i > 0) : (i -= 1) {
        try stdout.print("{c}", .{input[i - 1]});
    }
    try stdout.print("\n", .{});
    try stdout.flush();
}

Save this as reverse.zig and run with zig run reverse.zig -- hello:

$ zig run reverse.zig -- hello
olleh

Happy coding in Zig!