Chapter 51Mem And Meta Utilities

Mem and Meta Utilities

Overview

Having wrangled randomness and numeric helpers in the previous chapter, we now turn to the slice plumbing and reflection primitives that glue many Zig subsystems together.50 Zig’s std.mem establishes predictable rules for tokenizing, trimming, searching, and copying arbitrarily shaped data, while std.meta exposes enough type information to build lightweight generic helpers without giving up static guarantees.mem.zigmeta.zig Together they let you parse configuration files, introspect user-defined structs, and stitch together data pipelines with the same zero-cost abstractions used throughout the standard library.

Learning Goals

  • Iterate across slices with std.mem.tokenize*, std.mem.split*, and search routines without allocating.
  • Normalize or rewrite slice contents in-place and aggregate results with std.mem.join and friends, even when working from stack buffers.heap.zig
  • Reflect over struct fields using std.meta.FieldEnum, std.meta.fields, and std.meta.stringToEnum to build tiny schema-aware utilities.

Slice Plumbing with

Tokenization, splitting, and rewriting all revolve around the same idea: work with borrowed slices instead of allocating new strings. Most std.mem helpers therefore accept a borrowed buffer and return slices into the original data, leaving you in control of lifetimes and copying.

Tokenization Versus Splitting

The next example processes a faux configuration blob. It tokenizes lines, trims whitespace, hunts for key=value pairs, and normalizes mode names in-place before joining the remaining path list via a fixed buffer allocator.

Zig
const std = @import("std");

const whitespace = " \t\r";

pub fn main() !void {
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const out = &stdout_writer.interface;

    const config =
        \\# site roots and toggles
        \\root = /srv/www
        \\root=/srv/cache
        \\mode = fast-render
        \\log-level = warn
        \\extra-paths = :/opt/tools:/opt/tools/bin:
        \\
        \\# trailing noise we should ignore
        \\:
    ;

    var root_storage: [6][]const u8 = undefined;
    var root_count: usize = 0;
    var extra_storage: [8][]const u8 = undefined;
    var extra_count: usize = 0;
    var mode_buffer: [32]u8 = undefined;
    var normalized_mode: []const u8 = "slow";
    var log_level: []const u8 = "info";

    var lines = std.mem.tokenizeScalar(u8, config, '\n');
    while (lines.next()) |line| {
        const trimmed = std.mem.trim(u8, line, whitespace);
        if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) continue;

        const eq_index = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue;

        const key = std.mem.trim(u8, trimmed[0..eq_index], whitespace);
        const value = std.mem.trim(u8, trimmed[eq_index + 1 ..], whitespace);

        if (std.mem.eql(u8, key, "root")) {
            if (root_count < root_storage.len) {
                root_storage[root_count] = value;
                root_count += 1;
            }
        } else if (std.mem.eql(u8, key, "mode")) {
            if (value.len <= mode_buffer.len) {
                std.mem.copyForwards(u8, mode_buffer[0..value.len], value);
                const mode_view = mode_buffer[0..value.len];
                std.mem.replaceScalar(u8, mode_view, '-', '_');
                normalized_mode = mode_view;
            }
        } else if (std.mem.eql(u8, key, "log-level")) {
            log_level = value;
        } else if (std.mem.eql(u8, key, "extra-paths")) {
            var paths = std.mem.splitScalar(u8, value, ':');
            while (paths.next()) |segment| {
                const cleaned = std.mem.trim(u8, segment, whitespace);
                if (cleaned.len == 0) continue;
                if (extra_count < extra_storage.len) {
                    extra_storage[extra_count] = cleaned;
                    extra_count += 1;
                }
            }
        }
    }

    var extras_join_buffer: [256]u8 = undefined;
    var extras_allocator = std.heap.FixedBufferAllocator.init(&extras_join_buffer);
    var extras_joined_slice: []u8 = &.{};
    if (extra_count != 0) {
        extras_joined_slice = try std.mem.join(extras_allocator.allocator(), ", ", extra_storage[0..extra_count]);
    }
    const extras_joined: []const u8 = if (extra_count == 0) "(none)" else extras_joined_slice;

    try out.print("normalized mode -> {s}\n", .{normalized_mode});
    try out.print("log level -> {s}\n", .{log_level});
    try out.print("roots ({d})\n", .{root_count});
    for (root_storage[0..root_count], 0..) |root, idx| {
        try out.print("  [{d}] {s}\n", .{ idx, root });
    }
    try out.print("extra segments -> {s}\n", .{extras_joined});

    try out.flush();
}
Run
Shell
$ zig run mem_token_workbench.zig
Output
Shell
normalized mode -> fast_render
log level -> warn
roots (2)
  [0] /srv/www
  [1] /srv/cache
extra segments -> /opt/tools, /opt/tools/bin

Prefer std.mem.tokenize* variants when you want to skip delimiters entirely, and std.mem.split* when empty segments matter—for example, when you need to detect doubled separators.

Copying, Rewriting, and Aggregating Slices

std.mem.copyForwards guarantees safe overlap when copying forward, while std.mem.replaceScalar lets you normalize characters in-place without touching allocation. Once you have the slices you care about, use std.mem.join with a std.heap.FixedBufferAllocator to coalesce them into a single view without falling back to the general-purpose heap. Keep an eye on buffer lengths (as the example does for mode_buffer) so that the rewrite step stays bounds-safe.

Reflection Helpers with

Where std.mem keeps data moving, std.meta helps describe it. The library exposes field metadata, alignment, and enumerated tags so that you can build schema-aware tooling without macro systems or runtime type information.

Field-Driven Overrides with

This sample defines a Settings struct, prints a schema summary, and applies overrides parsed from a string by dispatching through std.meta.FieldEnum. Each assignment uses statically typed code yet supports dynamic key lookup via std.meta.stringToEnum and the struct’s own default values.

Zig
const std = @import("std");

const Settings = struct {
    render: bool = false,
    retries: u8 = 1,
    mode: []const u8 = "slow",
    log_level: []const u8 = "info",
    extra_paths: []const u8 = "",
};

const Field = std.meta.FieldEnum(Settings);
const whitespace = " \t\r";

const raw_config =
    \\# overrides loaded from a repro case
    \\render = true
    \\retries = 4
    \\mode = fast-render
    \\extra_paths = /srv/www:/srv/cache
;

const ParseError = error{
    UnknownKey,
    BadBool,
    BadInt,
};

fn printValue(out: anytype, value: anytype) !void {
    const T = @TypeOf(value);
    switch (@typeInfo(T)) {
        .pointer => |ptr_info| switch (ptr_info.child) {
            u8 => if (ptr_info.size == .slice or ptr_info.size == .many or ptr_info.size == .c) {
                try out.print("{s}", .{value});
                return;
            },
            else => {},
        },
        else => {},
    }
    try out.print("{any}", .{value});
}

fn parseBool(value: []const u8) ParseError!bool {
    if (std.ascii.eqlIgnoreCase(value, "true") or std.mem.eql(u8, value, "1")) return true;
    if (std.ascii.eqlIgnoreCase(value, "false") or std.mem.eql(u8, value, "0")) return false;
    return error.BadBool;
}

fn applySetting(settings: *Settings, key: []const u8, value: []const u8) ParseError!void {
    const tag = std.meta.stringToEnum(Field, key) orelse return error.UnknownKey;

    switch (tag) {
        .render => settings.render = try parseBool(value),
        .retries => {
            const parsed = std.fmt.parseInt(u16, value, 10) catch return error.BadInt;
            settings.retries = std.math.cast(u8, parsed) orelse return error.BadInt;
        },
        .mode => settings.mode = value,
        .log_level => settings.log_level = value,
        .extra_paths => settings.extra_paths = value,
    }
}

fn emitSchema(out: anytype) !void {
    try out.print("settings schema:\n", .{});
    inline for (std.meta.fields(Settings)) |field| {
        const defaults = Settings{};
        const default_value = @field(defaults, field.name);
        try out.print("  - {s}: {s} (align {d}) default=", .{ field.name, @typeName(field.type), std.meta.alignment(field.type) });
        try printValue(out, default_value);
        try out.print("\n", .{});
    }
}

fn dumpSettings(out: anytype, settings: Settings) !void {
    try out.print("resolved values:\n", .{});
    inline for (std.meta.fields(Settings)) |field| {
        const value = @field(settings, field.name);
        try out.print("  {s} => ", .{field.name});
        try printValue(out, value);
        try out.print("\n", .{});
    }
}

pub fn main() !void {
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const out = &stdout_writer.interface;

    try emitSchema(out);

    var settings = Settings{};
    var failures: usize = 0;

    var lines = std.mem.tokenizeScalar(u8, raw_config, '\n');
    while (lines.next()) |line| {
        const trimmed = std.mem.trim(u8, line, whitespace);
        if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) continue;

        const eql = std.mem.indexOfScalar(u8, trimmed, '=') orelse {
            failures += 1;
            continue;
        };

        const key = std.mem.trim(u8, trimmed[0..eql], whitespace);
        const raw = std.mem.trim(u8, trimmed[eql + 1 ..], whitespace);
        if (key.len == 0) {
            failures += 1;
            continue;
        }

        if (applySetting(&settings, key, raw)) |_| {} else |err| {
            failures += 1;
            try out.print("  warning: {s} -> {any}\n", .{ key, err });
        }
    }

    try dumpSettings(out, settings);
    const tags = std.meta.tags(Field);
    try out.print("field tags visited: {any}\n", .{tags});
    try out.print("parsing failures: {d}\n", .{failures});

    try out.flush();
}
Run
Shell
$ zig run meta_struct_report.zig
Output
Shell
settings schema:
  - render: bool (align 1) default=false
  - retries: u8 (align 1) default=1
  - mode: []const u8 (align 1) default=slow
  - log_level: []const u8 (align 1) default=info
  - extra_paths: []const u8 (align 1) default=
resolved values:
  render => true
  retries => 4
  mode => fast-render
  log_level => info
  extra_paths => /srv/www:/srv/cache
field tags visited: { .render, .retries, .mode, .log_level, .extra_paths }
parsing failures: 0

std.meta.tags(FieldEnum(T)) materialises an array of field tags at comptime, making it cheap to track which fields a routine has touched without runtime reflection.

Schema Inspection Patterns

By combining std.meta.fields with @field, you can emit a documentation table or prepare a lightweight LSP schema for editor integrations. std.meta.alignment reports the natural alignment of each field type, while the field iterator exposes default values so you can display sensible fallbacks alongside user-supplied overrides. Because everything happens at compile time, the generated code compiles down to a handful of constants and direct loads.

Notes & Caveats

  • When tokenizing, remember that the returned slices alias the original buffer; mutate or copy them before the source goes out of scope.
  • std.mem.join allocates through the supplied allocator—stack-buffer allocators work well for short joins, but switch to a general-purpose allocator as soon as you expect unbounded data.
  • std.meta.stringToEnum performs a linear scan for large enums; cache the result or build a lookup table when parsing untrusted input at scale.

Exercises

  • Extend mem_token_workbench.zig to detect duplicate roots by sorting or deduplicating the slice list with std.mem.sort and std.mem.indexOf before joining.
  • Augment meta_struct_report.zig to emit JSON by pairing std.meta.fields with std.json.StringifyStream, keeping the compile-time schema but offering machine-readable output.32
  • Add a strict flag to the override parser that requires every key in FieldEnum(Settings) to appear at least once, using std.meta.tags to track coverage.36

Caveats, Alternatives, Edge Cases

  • If you need delimiter-aware iteration that preserves separators, fall back to std.mem.SplitIterator—tokenizers always drop delimiter slices.
  • For very large configuration blobs, consider std.mem.terminated and sentinel slices so you can stream sections without copying entire files into memory.28
  • std.meta intentionally exposes only compile-time data; if you need runtime reflection, you must generate it yourself (for example, via build steps that emit lookup tables).

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.