File Seeking in Zig
One of the most notable changes in Zig 0.16 is the overhaul of the file I/O API. The traditional pattern of calling seek() followed by read() has been largely replaced with a cleaner and more explicit approach using positional reads.
The following program demonstrates how to read data from a specific byte offset in a file without modifying the file current position:
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(
init.arena.allocator(),
);
if (args.len < 2) {
std.debug.print("Usage: {s} <file>\n", .{args[0]});
return;
}
const filePath = args[1];
const file = try std.Io.Dir.cwd().openFile(init.io, filePath, .{});
defer file.close(init.io);
// In Zig 0.16, file I/O uses positional reads instead of seek+read.
// Read 1 byte at offset 5 directly, without changing any seek position.
var byte_buf: [1]u8 = undefined;
const n = try file.readPositionalAll(init.io, &byte_buf, 5);
if (n > 0) {
const byte = byte_buf[0];
std.debug.print("Byte at offset 5: 0x{x}\n", .{byte_buf[0]});
// Print the actual character if it is printable
if (std.ascii.isPrint(byte)) {
std.debug.print("Character: '{c}'\n", .{byte});
} else {
std.debug.print("Character: (non-printable)\n", .{});
}
} else {
std.debug.print("File is shorter than 5 bytes.\n", .{});
}
// Get file size via stat
const stat = try file.stat(init.io);
std.debug.print("File size: {d} bytes\n", .{stat.size});
}
In earlier versions of Zig, reading from a specific offset typically required two steps: first seeking to the desired position with seekTo(), then performing a read(). This approach mutated the file’s internal cursor and could lead to subtle bugs, especially in more complex code.
Zig 0.16 introduces positional I/O functions like readPositionalAll(), which allow you to read data from any byte offset in a single operation — without affecting the file’s current seek position.
This change makes the code more predictable, easier to reason about, and better suited for both synchronous and asynchronous programming.
std.process.Initprovides access to command-line arguments and the I/O context in the new Zig 0.16 style.std.Io.Dir.cwd().openFile()opens the file using the updated I/O interface.file.readPositionalAll(init.io, &byte_buf, 5)reads bytes starting at offset 5. TheAllsuffix means it tries to completely fill the provided buffer.file.stat(init.io)retrieves metadata about the open file, including its total size in bytes.- After reading the byte, the program checks if it is printable using
std.ascii.isPrint()and displays the character with{c}if it is.
This new API is part of Zig’s ongoing effort to make I/O operations explicit, consistent, and robust across different contexts.
As Zig moves closer to version 1.0, expect even more emphasis on positional and vectored I/O patterns rather than relying on implicit file cursors.
Save the code as lseek.zig and run it on a sample file:
$ zig version
0.16.0
$ cat /tmp/test.txt
123456789
$ zig run /tmp/lseek.zig -- /tmp/test.txt
Byte at offset 5: 0x36
Character: '6'
File size: 10 bytes
Happy coding in Zig!