zig-skills/references/std-process.md

6.2 KiB

std.process - Process API Reference (Zig 0.16.0)

Primary release-note source: https://ziglang.org/download/0.16.0/release-notes.html

Zig 0.16 moves process I/O, args, environment, current directory, and child process management behind explicit std.Io and Juicy Main.

Juicy Main

Preferred application entry:

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;
    const arena = init.arena.allocator();

    const args = try init.minimal.args.toSlice(arena);
    const env = init.environ_map;
    const preopens = init.preopens;

    _ = .{ gpa, io, args, env, preopens };
}

std.process.Init provides:

  • minimal.args
  • minimal.environ
  • arena
  • gpa
  • io
  • environ_map
  • preopens

Use std.process.Init.Minimal only when a program deliberately wants less setup.

Args

Prefer args from std.process.Init:

const args = try init.minimal.args.toSlice(init.arena.allocator());

Avoid older global argument APIs in new 0.16 code unless you are inside compatibility code.

Environment

Prefer init.environ_map at application boundaries.

Important error rename:

  • error.EnvironmentVariableNotFound -> error.EnvironmentVariableMissing

When spawning, pass an environment map through process options:

const result = try std.process.run(gpa, io, .{
    .argv = &.{ "tool" },
    .environ_map = init.environ_map,
});
defer gpa.free(result.stdout);
defer gpa.free(result.stderr);

Current Directory

var buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
const n = try std.process.currentPath(io, &buffer);
const cwd = buffer[0..n];
const cwd = try std.process.currentPathAlloc(io, gpa);
defer gpa.free(cwd);

On Windows these paths use WTF-8. On other platforms they are opaque path bytes with no guaranteed text encoding; do not assume UTF-8 when displaying or parsing them.

Do not add new std.process.getCwd* callsites.

Run and Capture Output

Use std.process.run(gpa, io, options).

const result = try std.process.run(gpa, io, .{
    .argv = &.{ "git", "status", "--short" },
    .stdout_limit = .limited(64 * 1024),
    .stderr_limit = .limited(64 * 1024),
    .cwd = .inherit,
});
defer gpa.free(result.stdout);
defer gpa.free(result.stderr);

switch (result.term) {
    .exited => |code| if (code != 0) return error.CommandFailed,
    else => return error.CommandFailed,
}

Important options:

  • argv
  • stdout_limit / stderr_limit as std.Io.Limit
  • reserve_amount
  • cwd
  • environ_map
  • expand_arg0
  • progress_node
  • create_no_window
  • disable_aslr
  • timeout

Supplying .environ_map replaces the child's environment, but its PATH does not resolve argv[0]; executable lookup still uses the parent environment. Use std.process.spawnPath for deterministic directory-relative resolution.

Spawn Child Process

Use std.process.spawn(io, options). std.process.Child.init is not the 0.16 pattern.

var child = try std.process.spawn(io, .{
    .argv = &.{ "tool", "--flag" },
    .stdin = .ignore,
    .stdout = .pipe,
    .stderr = .pipe,
    .cwd = .inherit,
});
defer child.kill(io);

const term = try child.wait(io);
_ = term;

child.wait(io) blocks until termination and cleans resources. child.kill(io) is uncancelable and idempotent after wait/kill.

Pipes

Pipe fields are std.Io.File values when requested.

var child = try std.process.spawn(io, .{
    .argv = &.{ "cat" },
    .stdin = .pipe,
    .stdout = .pipe,
    .stderr = .pipe,
});
defer child.kill(io);

var stdin_buf: [4096]u8 = undefined;
var stdin_writer = child.stdin.?.writer(io, &stdin_buf);
try stdin_writer.interface.writeAll("hello\n");
try stdin_writer.interface.flush();
child.stdin.?.close(io);
child.stdin = null;

var stdout_buf: [4096]u8 = undefined;
var stdout_reader = child.stdout.?.reader(io, &stdout_buf);
const stdout = try stdout_reader.interface.allocRemaining(gpa, .limited(64 * 1024));
defer gpa.free(stdout);

const term = try child.wait(io);
_ = term;

For simultaneous stdout/stderr capture, prefer std.process.run or std.Io.File.MultiReader to avoid pipe deadlocks.

Working Directory

Child cwd uses std.process.Child.Cwd:

.cwd = .inherit
.cwd = .{ .path = "projects" }
.cwd = .{ .dir = some_io_dir }

Use std.process.spawnPath(io, dir, options) when argv[0] should be resolved relative to a directory as a file path.

Standard I/O Options

SpawnOptions.StdIo values:

  • .inherit
  • .file
  • .ignore
  • .pipe
  • .close

Example:

var child = try std.process.spawn(io, .{
    .argv = &.{ "tool" },
    .stdin = .ignore,
    .stdout = .pipe,
    .stderr = .pipe,
});

Preopens

WASI preopens moved to std.process.Preopens and are exposed by std.process.Init.

const preopens = init.preopens;
_ = preopens;

Memory Locking

Memory locking/protection APIs moved under std.process:

  • std.process.lockMemory
  • std.process.unlockMemory
  • std.process.lockMemoryAll
  • std.process.unlockMemoryAll
  • std.process.MemoryProtection
  • std.process.protectMemory

Use them only for explicit platform/security needs.

Migration Map

Old pattern Zig 0.16 pattern
pub fn main() !void plus global args/env pub fn main(init: std.process.Init) !void
std.process.Child.run(.{ ... }) std.process.run(gpa, io, .{ ... })
std.process.Child.init(argv, allocator) std.process.spawn(io, .{ .argv = argv, ... })
child.spawn() spawn returns the child
child.wait() child.wait(io)
child.kill() child.kill(io)
std.process.getCwd(...) std.process.currentPath(io, ...)
std.process.getCwdAlloc(...) std.process.currentPathAlloc(io, allocator)
error.EnvironmentVariableNotFound error.EnvironmentVariableMissing

Review Checklist

  • Does the function already have std.process.Init or an io parameter?
  • Are args/env taken from init instead of globals?
  • Are child processes bounded with output limits or timeouts where appropriate?
  • Are pipe reads/writes using std.Io.File readers/writers with io?
  • Is child.kill(io) used in defer when early exits could leave a process alive?
  • Is stdout/stderr capture safe from deadlock?