🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Memory-Mapped I/O

Memory-mapped I/O is a way to treat a file or a block of memory as an array. The operating system maps the bytes into the program's address space.

Anonymous mapping

This mapping has no file behind it. Writing two bytes and reading them back gives AB.

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const len: usize = 4096;
    const mem = try std.posix.mmap(null, len, std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0);
    defer std.posix.munmap(mem);
    mem[0] = 65;
    mem[1] = 66;
    try out.print("{c}{c} {d}\n", .{ mem[0], mem[1], mem.len });
}

Mapping a file for reading

The file is written first, then opened for reading. The mapping shows the five file bytes as a string.

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    {
        const file = try std.fs.cwd().createFile("data.bin", .{});
        defer file.close();
        try file.writeAll("hello");
    }
    const file = try std.fs.cwd().openFile("data.bin", .{});
    defer file.close();
    const mem = try std.posix.mmap(null, 5, std.posix.PROT.READ, .{ .TYPE = .SHARED }, file.handle, 0);
    defer std.posix.munmap(mem);
    try out.print("{s} {d}\n", .{ mem, mem.len });
}

Writing through a mapping

The mapping is shared and writable. Changing the first byte changes the file itself.

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    {
        const f = try std.fs.cwd().createFile("w.bin", .{});
        defer f.close();
        try f.writeAll("abcd");
    }
    const f = try std.fs.cwd().openFile("w.bin", .{ .mode = .read_write });
    defer f.close();
    {
        const mem = try std.posix.mmap(null, 4, std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED }, f.handle, 0);
        defer std.posix.munmap(mem);
        mem[0] = 'z';
    }
    const g = try std.fs.cwd().openFile("w.bin", .{});
    defer g.close();
    var buf: [4]u8 = undefined;
    _ = try g.readAll(&buf);
    try out.print("{s}\n", .{buf});
}

Example

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const len = 5 * @sizeOf(u32);
    const mem = try std.posix.mmap(null, len, std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0);
    defer std.posix.munmap(mem);
    const nums = @as([*]u32, @ptrCast(@alignCast(mem.ptr)))[0..5];
    for (0..5) |i| nums[i] = @intCast(i + 1);
    var sum: u32 = 0;
    for (nums) |v| sum += v;
    try out.print("{d}\n", .{sum});
}

Key points

  • Mapping: a mapping is a range of memory that the operating system connects to a file or to fresh pages.
  • An anonymous mapping has no file behind it and starts filled with zeros.
  • A file mapping shows the file's bytes as an array the program can read.
  • A shared mapping writes changes back to the file.
  • The length of a mapping is given in bytes and is rounded up to whole pages by the system.
  • Every mapping must be released with munmap when it is no longer needed.

The program maps twenty bytes with no file behind them. The bytes are viewed as five 32-bit numbers. The numbers 1 through 5 are stored and added. The sum is 15.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What does a shared writable file mapping do when the program writes through it?