Skip to main content
Version: Zig 0.13.0

Readers and Writers

std.io.Writer and std.io.Reader provide standard ways of making use of IO. std.ArrayList(u8) has a writer method which gives us a writer. Let's use it.

const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;
const ArrayList = std.ArrayList;
const test_allocator = std.testing.allocator;

test "io writer usage" {
var list = ArrayList(u8).init(test_allocator);
defer list.deinit();
const bytes_written = try list.writer().write(
"Hello World!",
);
try expect(bytes_written == 12);
try expect(eql(u8, list.items, "Hello World!"));
}

Here we will use a reader to copy the file's contents into an allocated buffer. The second argument of readAllAlloc is the maximum size that it may allocate; if the file is larger than this, it will return error.StreamTooLong.

const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;
const test_allocator = std.testing.allocator;
test "io reader usage" {
const message = "Hello File!";

const file = try std.fs.cwd().createFile(
"junk_file2.txt",
.{ .read = true },
);
defer file.close();

try file.writeAll(message);
try file.seekTo(0);

const contents = try file.reader().readAllAlloc(
test_allocator,
message.len,
);
defer test_allocator.free(contents);

try expect(eql(u8, contents, message));
}

A common usecase for readers is to read until the next line (e.g. for user input). Here we will do this with the std.io.getStdIn() file.

const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;

fn nextLine(reader: anytype, buffer: []u8) !?[]const u8 {
var line = (try reader.readUntilDelimiterOrEof(
buffer,
'\n',
)) orelse return null;
// trim annoying windows-only carriage return character
if (@import("builtin").os.tag == .windows) {
return std.mem.trimRight(u8, line, "\r");
} else {
return line;
}
}

test "read until next line" {
const stdout = std.io.getStdOut();
const stdin = std.io.getStdIn();

try stdout.writeAll(
\\ Enter your name:
);

var buffer: [100]u8 = undefined;
const input = (try nextLine(stdin.reader(), &buffer)).?;
try stdout.writer().print(
"Your name is: \"{s}\"\n",
.{input},
);
}

An std.io.Writer type consists of a context type, error set, and a write function. The write function must take in the context type and a byte slice. The write function must also return an error union of the Writer type's error set and the number of bytes written. Let's create a type that implements a writer.

const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;
// Don't create a type like this! Use an
// arraylist with a fixed buffer allocator
const MyByteList = struct {
data: [100]u8 = undefined,
items: []u8 = &[_]u8{},

const Writer = std.io.Writer(
*MyByteList,
error{EndOfBuffer},
appendWrite,
);

fn appendWrite(
self: *MyByteList,
data: []const u8,
) error{EndOfBuffer}!usize {
if (self.items.len + data.len > self.data.len) {
return error.EndOfBuffer;
}
@memcpy(
self.data[self.items.len..][0..data.len],
data,
);
self.items = self.data[0 .. self.items.len + data.len];
return data.len;
}

fn writer(self: *MyByteList) Writer {
return .{ .context = self };
}
};

test "custom writer" {
var bytes = MyByteList{};
_ = try bytes.writer().write("Hello");
_ = try bytes.writer().write(" Writer!");
try expect(eql(u8, bytes.items, "Hello Writer!"));
}