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 newstd.Io.Writer
interface.- The
&stdout_writer.interface
gives us a pointer to the concreteWriter
struct, ready for methods likeprint
andflush
.
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
), soinput.len
andinput[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!