↓ Skip to main content
  1. Posts/

Writing to files using buffers in Zig 0.16.0

definitepotato
Author
definitepotato
Code slinger, golang, ziglang, grokking life, tabletop games enthusiast, obsession with keebs and numbers.
Table of Contents

Concepts to understand before we start writing to a file
#

1. Why writes are buffered
#

Every write operation is a system call (syscalls), and syscalls cost cpu cycles no matter how many bytes you send. The trade is always memory for speed. A bigger buffer means fewer syscalls but more RAM. A smaller buffer uses less memory but more syscalls by comparison.

2. You own the buffer, the writer borrows
#

Zig’s writer doesn’t allocate or pick the size of your buffer. You create the buffer, choose the size and lend it to the writer:

var buf: [2048]u8 = undefined;
var file_writer = file.writer(io. &buf);

This means the buffer’s lifetime is the caller’s responsibility. The buffer must outlive the writer that uses it. In our code both will live in main so it’ll be automatic.

3. The two-layer writer
#

Same pattern we used in the reader the file_writer knows how to talk to a file and it’s .interface field is a generic std.Io.Writer where all the methods you actually call live: writeAll, print, flush, etc.

4. Don’t forget to flush()
#

The single most common bug when using buffers, in any language, is forgetting to flush. In our case, when writeAll returns, your bytes are in the buffer, not yet in the file. They reach the file in exactly two scenarios:

  1. The buffer is full so the writers writes and empties the buffer
  2. You call flush()

Typically towards the end of a write your bytes will almost never align perfectly to trigger scenario 1 so if your code exits early before it writes the final bytes, those bytes stay in memory and never make it to the file. It’s also good to know that file.close(io) doesn’t flush (this is deliberate because flushing can fail), and a failure during a close would have nowhere sensible to go. The pattern to remember is last write, then flush, then close.

5. createFile truncates by default
#

When calling createFile(io, "file.txt", .{}) if the file already exists the old contents are wiped when this call succeeds, before you’ve written anything. This is preferred but it’s worth noting the destructive part happens at create time, not at write time. There is a .truncate = false option but we’re won’t get into that here.

7. Sizing the write buffer
#

2KB - 8KB is a sensible default. Writers larger than the buffer are not a problem, the write can handle it. Also a zero length buffer is legal and gives you a fully unbuffered writer meaning that every write goes straight to the operating system. Useful when each write must hit the file immediately but as discussed above the cost is cpu cycles.

Let’s write code
#

Create a file
#

const std = @import("std");

pub fn main() !void {
  const allocator = std.heap.page_allocator;
  var threaded: std.Io.Threaded = .init(allocator, .{});
  defer threaded.deinit();
  const io = threaded.io();

  const file = try std.Io.Dir.cwd().createFile(io, "file.txt", .{});
  defer file.close(io);
}

Approach 1: Write to the file using a stack buffer
#

const std = @import("std");

pub fn main() !void {
  const allocator = std.heap.page_allocator;
  var threaded: std.Io.Threaded = .init(allocator, .{});
  defer threaded.deinit();
  const io = threaded.io();

  const file = try std.Io.Dir.cwd().createFile(io, "file.txt", .{});
  defer file.close(io);

  var buf: [2048]u8 = undefined;

  var file_writer = file.writer(io, &buf);
  const writer_iface: *std.Io.Writer = &file_writer.interface;

  try writer_iface.writeAll("some slice of bytes we want to write to the file\n");
  try writer_iface.flush();
}

Approach 2: Write to a file using an allocator
#

  const allocator = std.heap.page_allocator;
  var threaded: std.Io.Threaded = .init(allocator, .{});
  defer threaded.deinit();
  const io = threaded.io();

  var gpa: std.heap.DebugAllocator(.{}) = .init;
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  const buf = try allocator.alloc(u8, 2048);
  defer allocator.free(buf);

  const file = try std.Io.Dir.cwd().createFile(io, "file.txt", .{});
  defer file.close(io);

  var file_writer = file.writer(io, &buf);
  const writer_iface: *std.Io.Writer = &file_writer.interface;

  try writer_iface.writeAll("some slice of bytes we want to write to the file\n");
  try writer_iface.flush();

Related