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.joinand friends, even when working from stack buffers.heap.zig - Reflect over struct fields using
std.meta.FieldEnum,std.meta.fields, andstd.meta.stringToEnumto 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.
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();
}
$ zig run mem_token_workbench.zignormalized mode -> fast_render
log level -> warn
roots (2)
[0] /srv/www
[1] /srv/cache
extra segments -> /opt/tools, /opt/tools/binPrefer 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.
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();
}
$ zig run meta_struct_report.zigsettings 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: 0std.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.joinallocates 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.stringToEnumperforms 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.zigto detect duplicate roots by sorting or deduplicating the slice list withstd.mem.sortandstd.mem.indexOfbefore joining. - Augment
meta_struct_report.zigto emit JSON by pairingstd.meta.fieldswithstd.json.StringifyStream, keeping the compile-time schema but offering machine-readable output.32 - Add a
strictflag to the override parser that requires every key inFieldEnum(Settings)to appear at least once, usingstd.meta.tagsto 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.terminatedand sentinel slices so you can stream sections without copying entire files into memory.28 std.metaintentionally exposes only compile-time data; if you need runtime reflection, you must generate it yourself (for example, via build steps that emit lookup tables).