zig-pkl

Examples

Each program lives in examples/. zig build examples runs them all and zig build example-<name> runs one. They start the server with PKL_EXEC, or pkl from PATH. The codegen example is a separate package; see Code generation.

typed

Decodes a Pkl module into Zig structs with `zig_pkl.convert`.

main.zig
//! Decodes a Pkl module into Zig structs with `zig_pkl.convert`.

const std = @import("std");
const zig_pkl = @import("zig_pkl");
const convert = zig_pkl.convert;

const App = struct {
    name: []const u8,
    port: u16,
    debug: bool,
    timeout: convert.Duration,
    max_upload: convert.DataSize,
    tags: []const []const u8,
    limits: convert.Map([]const u8, i64),
    database: Database,

    pub const pkl_names = .{ .max_upload = "maxUpload" };
};

const Database = struct {
    host: []const u8,
    port: u16,
    replicas: []const []const u8,
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const evaluator = try manager.createEvaluator(zig_pkl.Options.preconfigured(), &rejected);
    defer evaluator.close();

    const module_uri = try zig_pkl.uri.fromFile(allocator, io, "prod.pkl");
    defer allocator.free(module_uri);
    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);
    const app = evaluator.evaluateInto(App, allocator, .uri(module_uri), null, &failure) catch |err| {
        if (failure) |text| std.debug.print("{s}\n", .{text});
        return err;
    };
    defer convert.freeInto(App, allocator, app);

    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;
    try out.print("{s} on port {d}, debug {}\n", .{ app.name, app.port, app.debug });
    try out.print("timeout {d}{s}, uploads up to {d}{s}\n", .{ app.timeout.value, app.timeout.unit, app.max_upload.value, app.max_upload.unit });
    for (app.tags) |tag| try out.print("tag {s}\n", .{tag});
    for (app.limits.entries) |entry| try out.print("limit {s} = {d}\n", .{ entry.key, entry.value });
    try out.print("database {s}:{d} with {d} replicas\n", .{ app.database.host, app.database.port, app.database.replicas.len });
    try out.flush();
}
App.pkl
/// Settings for an inventory service.
module example.App

name: String = "inventory"

port: UInt16 = 8080

debug: Boolean = false

timeout: Duration = 30.s

maxUpload: DataSize = 25.mb

tags: Listing<String>

limits: Mapping<String, Int>

database: Database

class Database {
  host: String
  port: UInt16 = 5432
  replicas: Listing<String>
}
prod.pkl
amends "App.pkl"

port = 443
timeout = 2.min

tags {
  "api"
  "production"
}

limits {
  ["requests_per_second"] = 500
  ["burst"] = 50
}

database {
  host = "db.internal"
  replicas {
    "db-replica-1.internal"
    "db-replica-2.internal"
  }
}

dynamic

Walks an evaluated value without declaring Zig types for it.

main.zig
//! Walks an evaluated value without declaring Zig types for it.

const std = @import("std");
const zig_pkl = @import("zig_pkl");
const Value = zig_pkl.Value;

const module_text =
    \\name = "edge"
    \\ports = List(80, 443)
    \\weights = Map("a", 0.25, "b", 0.75)
    \\owner {
    \\  team = "platform"
    \\  oncall = null
    \\}
    \\servers {
    \\  "one"
    \\  "two"
    \\}
;

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const evaluator = try manager.createEvaluator(zig_pkl.Options.restricted(), &rejected);
    defer evaluator.close();

    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);
    var tree = try evaluator.evaluateSource(.text(module_text), null, &failure);
    defer tree.deinit();

    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;

    // Look up one property directly.
    const name = zig_pkl.value.findProperty(tree.root, "name").?;
    try out.print("name is {s}\n", .{name.string});

    try describe(out, tree.root, 0);
    try out.flush();
}

fn describe(out: *std.Io.Writer, value: *const Value, depth: usize) !void {
    switch (value.*) {
        .nil => try out.writeAll("null\n"),
        .boolean => |b| try out.print("{}\n", .{b}),
        .int => |i| switch (i) {
            inline else => |n| try out.print("{d}\n", .{n}),
        },
        .float => |f| try out.print("{d}\n", .{f}),
        .string => |s| try out.print("\"{s}\"\n", .{s}),
        .list, .listing, .set => |items| {
            try out.print("{t} of {d}\n", .{ value.*, items.len });
            for (items) |item| {
                try indent(out, depth + 1);
                try describe(out, item, depth + 1);
            }
        },
        .map, .mapping => |entries| {
            try out.print("{t} of {d}\n", .{ value.*, entries.len });
            for (entries) |entry| {
                try indent(out, depth + 1);
                try out.print("{s} => ", .{entry.key.string});
                try describe(out, entry.value, depth + 1);
            }
        },
        .object => |object| {
            try out.print("{s} object\n", .{object.class_name});
            for (object.members) |member| {
                try indent(out, depth + 1);
                switch (member) {
                    .property => |property| {
                        try out.print("{s}: ", .{property.name});
                        try describe(out, property.value, depth + 1);
                    },
                    .entry => |entry| {
                        try out.writeAll("[entry] ");
                        try describe(out, entry.value, depth + 1);
                    },
                    .element => |element| {
                        switch (element.index) {
                            inline else => |n| try out.print("[{d}] ", .{n}),
                        }
                        try describe(out, element.value, depth + 1);
                    },
                }
            }
        },
        else => try out.print("{t}\n", .{value.*}),
    }
}

fn indent(out: *std.Io.Writer, depth: usize) !void {
    for (0..depth) |_| try out.writeAll("  ");
}

expressions

Evaluates expressions against a module instead of the whole module.

main.zig
//! Evaluates expressions against a module instead of the whole module.

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

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const evaluator = try manager.createEvaluator(zig_pkl.Options.preconfigured(), &rejected);
    defer evaluator.close();

    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;

    const module_uri = try zig_pkl.uri.fromFile(allocator, io, "service.pkl");
    defer allocator.free(module_uri);
    const service = zig_pkl.Source.uri(module_uri);

    const expressions = [_][]const u8{
        "port",
        "endpoints.length",
        "url(endpoints.first)",
        "endpoints.map((path) -> url(path)).join(\", \")",
    };
    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);
    for (expressions) |expression| {
        var result = evaluator.evaluateSource(service, expression, &failure) catch |err| {
            if (failure) |text| std.debug.print("{s}\n", .{text});
            return err;
        };
        defer result.deinit();
        try out.print("{s} = ", .{expression});
        switch (result.root.*) {
            .int => |number| try out.print("{d}\n", .{number.signed}),
            .string => |text| try out.print("{s}\n", .{text}),
            else => |other| try out.print("({t})\n", .{other}),
        }
    }

    // An inline module needs no file.
    var sum = try evaluator.evaluateSource(.text("numbers = List(1, 2, 3, 4)"), "numbers.fold(0, (total, n) -> total + n)", &failure);
    defer sum.deinit();
    try out.print("sum of numbers = {d}\n", .{sum.root.int.signed});
    try out.flush();
}
service.pkl
module example.Service

host = "api.example.com"
port = 8443
endpoints = List("/health", "/orders", "/users")

function url(path: String): String = "https://\(host):\(port)\(path)"

output

Reads the text, files, and bytes a module renders through `output`.

main.zig
//! Reads the text, files, and bytes a module renders through `output`.

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

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const evaluator = try manager.createEvaluator(zig_pkl.Options.preconfigured(), &rejected);
    defer evaluator.close();

    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;

    const module_uri = try zig_pkl.uri.fromFile(allocator, io, "site.pkl");
    defer allocator.free(module_uri);
    const site = zig_pkl.Source.uri(module_uri);
    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);

    // The module's own renderer produces `output.text`.
    var text = try evaluator.evaluateSource(site, "output.text", &failure);
    defer text.deinit();
    try out.print("output.text:\n{s}\n", .{text.root.string});

    // Each entry of `output.files` renders with its own renderer.
    var files = try evaluator.evaluateSource(site, "output.files.toMap().mapValues((_, file) -> file.text)", &failure);
    defer files.deinit();
    for (files.root.map) |entry| try out.print("file {s}:\n{s}", .{ entry.key.string, entry.value.string });

    // `output.bytes` exists from Pkl 0.29.
    const version = try zig_pkl.getVersion(allocator, io, pkl);
    if (version.atLeast(.{ .major = 0, .minor = 29, .patch = 0 })) {
        var bytes = try evaluator.evaluateSource(site, "output.bytes", &failure);
        defer bytes.deinit();
        try out.print("output.bytes: {d} bytes\n", .{bytes.root.bytes.len});
    }

    // `output_format` picks the renderer for modules that do not set one.
    var options = zig_pkl.Options.preconfigured();
    options.output_format = "yaml";
    const yaml = try manager.createEvaluator(options, &rejected);
    defer yaml.close();
    var rendered = try yaml.evaluateSource(.text("name = \"edge\"\nports = List(80, 443)"), "output.text", &failure);
    defer rendered.deinit();
    try out.print("as yaml:\n{s}", .{rendered.root.string});
    try out.flush();
}
site.pkl
module example.Site

title = "zig-pkl"
pages = List("index", "guide", "api")

output {
  renderer = new JsonRenderer {}
  files {
    for (page in pages) {
      ["pages/\(page).yaml"] {
        value = new Dynamic {
          name = page
          site = title
        }
        renderer = new YamlRenderer {}
      }
    }
  }
}

project

Loads `PklProject` settings and dependencies, then evaluates a module that imports a dependency by name.

main.zig
//! Loads `PklProject` settings and dependencies, then evaluates a module
//! that imports a dependency by name.

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

const Settings = struct {
    environment: []const u8,
    buffer_size: zig_pkl.convert.DataSize,
    timeout: zig_pkl.convert.Duration,

    pub const pkl_names = .{ .buffer_size = "bufferSize" };
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);

    // Any evaluator can evaluate the project file itself.
    const loader = try manager.createEvaluator(zig_pkl.Options.preconfigured(), &rejected);
    defer loader.close();
    var project = try zig_pkl.project.loadFile(allocator, io, loader, "PklProject");
    defer project.deinit();

    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;
    try out.print("project {s}\n", .{project.projectUri()});
    for (project.project.dependencies) |dependency| try out.print("dependency @{s}\n", .{dependency.name});

    // The project supplies its settings and dependencies; this program keeps
    // its own allow lists because the project does not set any.
    var options = project.options;
    if (options.allowed_modules == null) options.allowed_modules = zig_pkl.Options.preconfigured().allowed_modules;
    if (options.allowed_resources == null) options.allowed_resources = zig_pkl.Options.preconfigured().allowed_resources;
    const evaluator = try manager.createEvaluator(options, &rejected);
    defer evaluator.close();

    // Modules inside the project are addressed beside the canonical PklProject.
    const main_uri = try project.moduleUri(allocator, "main.pkl");
    defer allocator.free(main_uri);
    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);
    const settings = evaluator.evaluateInto(Settings, allocator, .uri(main_uri), null, &failure) catch |err| {
        if (failure) |text| std.debug.print("{s}\n", .{text});
        return err;
    };
    defer zig_pkl.convert.freeInto(Settings, allocator, settings);
    try out.print("environment {s}, buffer {d}{s}, timeout {d}{s}\n", .{
        settings.environment,
        settings.buffer_size.value,
        settings.buffer_size.unit,
        settings.timeout.value,
        settings.timeout.unit,
    });
    try out.flush();
}
main.pkl
import "@units/units.pkl"

environment = read("env:DEPLOY_ENV")
bufferSize = units.kib(64)
timeout = units.defaultTimeout
PklProject
amends "pkl:Project"

dependencies {
  ["units"] = import("units/PklProject")
}

evaluatorSettings {
  timeout = 30.s
  env {
    ["DEPLOY_ENV"] = "staging"
  }
}

readers

Serves modules and resources from the Zig program instead of the disk.

main.zig
//! Serves modules and resources from the Zig program instead of the disk.

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

/// A resource reader for `secret:<name>` URIs backed by a fixed table.
const Secrets = struct {
    table: []const [2][]const u8,

    fn resourceReader(self: *Secrets) zig_pkl.ResourceReader {
        return .{ .ptr = self, .vtable = &vtable };
    }

    const vtable = zig_pkl.ResourceReader.VTable{
        .scheme = scheme,
        .has_hierarchical_uris = no,
        .is_globbable = no,
        .read = read,
        .list = list,
    };

    fn scheme(_: *anyopaque) []const u8 {
        return "secret";
    }

    fn no(_: *anyopaque) bool {
        return false;
    }

    /// Runs on a reader thread. The result must come from `allocator`.
    fn read(ptr: *anyopaque, allocator: std.mem.Allocator, uri: []const u8) anyerror![]u8 {
        const self: *Secrets = @ptrCast(@alignCast(ptr));
        const name = uri["secret:".len..];
        for (self.table) |row| {
            if (std.mem.eql(u8, row[0], name)) return allocator.dupe(u8, row[1]);
        }
        return error.SecretNotFound;
    }

    fn list(_: *anyopaque, allocator: std.mem.Allocator, _: []const u8) anyerror![]zig_pkl.reader.PathElement {
        return allocator.alloc(zig_pkl.reader.PathElement, 0);
    }
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    // Modules compiled into the program, addressed as `embedded:/...`.
    var embedded = zig_pkl.reader.Embed{
        .scheme_name = "embedded",
        .entries = &.{
            .{ .path = "/base.pkl", .contents = "region = \"eu-west-1\"\nreplicas = 2\npassword: String\n" },
            .{ .path = "/services/api.pkl", .contents = "amends \"../base.pkl\"\nreplicas = 4\npassword = read(\"secret:api-password\").text\n" },
            .{ .path = "/services/worker.pkl", .contents = "amends \"../base.pkl\"\npassword = read(\"secret:worker-password\").text\n" },
        },
    };
    var secrets = Secrets{ .table = &.{
        .{ "api-password", "correct horse" },
        .{ "worker-password", "battery staple" },
    } };
    const module_readers = [_]zig_pkl.ModuleReader{embedded.moduleReader()};
    const resource_readers = [_]zig_pkl.ResourceReader{secrets.resourceReader()};

    // The builder adds `embedded:` and `secret:` to the allow lists.
    var builder = try zig_pkl.Builder.restricted(allocator);
    defer builder.deinit();
    _ = try builder.moduleReaders(&module_readers);
    _ = try builder.resourceReaders(&resource_readers);

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const evaluator = try manager.createEvaluator(builder.options, &rejected);
    defer evaluator.close();

    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;
    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);

    // Globbing lists modules through the same reader.
    var names = evaluator.evaluateSource(
        .text("services = import*(\"embedded:/services/*.pkl\").keys.toList()"),
        "services",
        &failure,
    ) catch |err| {
        if (failure) |text| std.debug.print("{s}\n", .{text});
        return err;
    };
    defer names.deinit();
    for (names.root.list) |name| {
        var service = evaluator.evaluateSource(.uri(name.string), null, &failure) catch |err| {
            if (failure) |text| std.debug.print("{s}\n", .{text});
            return err;
        };
        defer service.deinit();
        const replicas = zig_pkl.value.findProperty(service.root, "replicas").?.int.signed;
        const password = zig_pkl.value.findProperty(service.root, "password").?.string;
        try out.print("{s}: {d} replicas, password {d} characters\n", .{ name.string, replicas, password.len });
    }
    try out.flush();
}

restricted

Evaluates untrusted Pkl with no file, network, or environment access.

main.zig
//! Evaluates untrusted Pkl with no file, network, or environment access.

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

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);

    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;

    // Only `pkl:` and `repl:` modules, no resources, an empty environment,
    // and a deadline for runaway evaluation.
    var options = zig_pkl.Options.restricted();
    options.timeout_seconds = 5;
    const sandbox = try manager.createEvaluator(options, &rejected);
    defer sandbox.close();

    const attempts = [_][]const u8{
        "result = 6 * 7",
        "result = read(\"env:HOME\")",
        "result = import(\"file:///etc/hosts\")",
        "result = read(\"https://example.com/\")",
    };
    for (attempts) |text| {
        var failure: ?[]u8 = null;
        defer if (failure) |message| allocator.free(message);
        if (sandbox.evaluateSource(.text(text), "result", &failure)) |result| {
            var tree = result;
            defer tree.deinit();
            try out.print("allowed: {s} -> {d}\n", .{ text, tree.root.int.signed });
        } else |err| switch (err) {
            error.EvaluationFailed => try out.print("denied:  {s}\n         {s}\n", .{ text, reason(failure.?) }),
            else => return err,
        }
    }

    // Grant exactly one environment variable and nothing else.
    var narrow = zig_pkl.Options.restricted();
    narrow.allowed_resources = &.{"env:"};
    narrow.env = &.{.{ .key = "REGION", .value = "eu-west-1" }};
    const scoped = try manager.createEvaluator(narrow, &rejected);
    defer scoped.close();
    var failure: ?[]u8 = null;
    defer if (failure) |message| allocator.free(message);
    var region = try scoped.evaluateSource(.text("region = read(\"env:REGION\")\nhome = read?(\"env:HOME\")"), null, &failure);
    defer region.deinit();
    const home = zig_pkl.value.findProperty(region.root, "home").?;
    try out.print("REGION = {s}, HOME visible: {}\n", .{ zig_pkl.value.findProperty(region.root, "region").?.string, home.* != .nil });
    try out.flush();
}

/// The reason in a Pkl error message, after its `–– Pkl Error ––` banner.
fn reason(text: []const u8) []const u8 {
    var lines = std.mem.splitScalar(u8, text, '\n');
    while (lines.next()) |line| {
        const trimmed = std.mem.trim(u8, line, " ");
        if (trimmed.len != 0 and std.mem.indexOf(u8, trimmed, "Pkl Error") == null) return trimmed;
    }
    return text;
}

external-reader

Runs this same program as an external resource reader that Pkl starts.

Without arguments it evaluates Pkl that reads `vault:` resources. Pkl then starts `<this program> --serve`, which answers over standard input and output. External readers need Pkl 0.27 or newer.

main.zig
//! Runs this same program as an external resource reader that Pkl starts.
//!
//! Without arguments it evaluates Pkl that reads `vault:` resources. Pkl then
//! starts `<this program> --serve`, which answers over standard input and
//! output. External readers need Pkl 0.27 or newer.

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

/// Resources served to Pkl as `vault:<name>`.
const Vault = struct {
    fn resourceReader(self: *Vault) zig_pkl.ResourceReader {
        return .{ .ptr = self, .vtable = &vtable };
    }

    const vtable = zig_pkl.ResourceReader.VTable{
        .scheme = scheme,
        .has_hierarchical_uris = no,
        .is_globbable = no,
        .read = read,
        .list = list,
    };

    fn scheme(_: *anyopaque) []const u8 {
        return "vault";
    }

    fn no(_: *anyopaque) bool {
        return false;
    }

    fn read(_: *anyopaque, allocator: std.mem.Allocator, uri: []const u8) anyerror![]u8 {
        const name = uri["vault:".len..];
        return std.fmt.allocPrint(allocator, "token-for-{s}", .{name});
    }

    fn list(_: *anyopaque, allocator: std.mem.Allocator, _: []const u8) anyerror![]zig_pkl.reader.PathElement {
        return allocator.alloc(zig_pkl.reader.PathElement, 0);
    }
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const args = try init.minimal.args.toSlice(init.arena.allocator());
    if (args.len > 1 and std.mem.eql(u8, args[1], "--serve")) return serve(allocator, io);

    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";
    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};

    const version = try zig_pkl.getVersion(allocator, io, pkl);
    if (!version.atLeast(.{ .major = 0, .minor = 27, .patch = 0 })) {
        try out.print("external readers need Pkl 0.27 or newer; this is {d}.{d}.{d}\n", .{ version.major, version.minor, version.patch });
        return;
    }

    const self_path = try std.process.executablePathAlloc(io, allocator);
    defer allocator.free(self_path);
    var options = zig_pkl.Options.restricted();
    options.allowed_resources = &.{"vault:"};
    options.external_resource_readers = &.{.{
        .scheme = "vault",
        .executable = self_path,
        .arguments = &.{"--serve"},
    }};

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const evaluator = try manager.createEvaluator(options, &rejected);
    defer evaluator.close();

    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);
    var tree = evaluator.evaluateSource(
        .text("database = read(\"vault:database\").text\ncache = read(\"vault:cache\").text"),
        null,
        &failure,
    ) catch |err| {
        if (failure) |text| std.debug.print("{s}\n", .{text});
        return err;
    };
    defer tree.deinit();
    for (tree.root.object.members) |member| {
        try out.print("{s} = {s}\n", .{ member.property.name, member.property.value.string });
    }
}

/// The reader side. Standard output carries protocol frames only.
fn serve(allocator: std.mem.Allocator, io: std.Io) !void {
    var vault = Vault{};
    const client = zig_pkl.ExternalClient{ .resource_readers = &.{vault.resourceReader()} };
    var input_buffer: [64 * 1024]u8 = undefined;
    var output_buffer: [64 * 1024]u8 = undefined;
    var input = std.Io.File.stdin().readerStreaming(io, &input_buffer);
    var output = std.Io.File.stdout().writerStreaming(io, &output_buffer);
    try client.run(allocator, &input.interface, &output.interface);
}

concurrency

Shares one Pkl server between threads.

One `Manager` owns the server process. Evaluators are cheap handles on it, and any thread may evaluate through any evaluator concurrently.

main.zig
//! Shares one Pkl server between threads.
//!
//! One `Manager` owns the server process. Evaluators are cheap handles on
//! it, and any thread may evaluate through any evaluator concurrently.

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

const module_text =
    \\function fib(n: Int): Int = if (n < 2) n else fib(n - 1) + fib(n - 2)
;

const threads = 4;
const per_thread = 8;

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    const shared = try manager.createEvaluator(zig_pkl.Options.restricted(), &rejected);
    defer shared.close();

    var results: [threads][per_thread]i64 = undefined;
    var failures = std.atomic.Value(usize).init(0);
    var workers: [threads]std.Thread = undefined;
    for (&workers, 0..) |*worker, index| {
        worker.* = try std.Thread.spawn(.{}, work, .{ shared, index, &results[index], &failures });
    }
    for (workers) |worker| worker.join();

    var buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;
    for (results, 0..) |row, index| {
        try out.print("thread {d}:", .{index});
        for (row) |number| try out.print(" {d}", .{number});
        try out.writeAll("\n");
    }
    try out.print("{d} evaluations failed\n", .{failures.load(.monotonic)});
    try out.flush();
    if (failures.load(.monotonic) != 0) return error.EvaluationFailed;
}

fn work(evaluator: *zig_pkl.Evaluator, index: usize, row: *[per_thread]i64, failures: *std.atomic.Value(usize)) void {
    for (row, 0..) |*slot, step| {
        const n = index * per_thread + step;
        var expression_buffer: [32]u8 = undefined;
        const expression = std.fmt.bufPrint(&expression_buffer, "fib({d})", .{n}) catch unreachable;
        var failure: ?[]u8 = null;
        defer if (failure) |text| evaluator.manager.allocator.free(text);
        var tree = evaluator.evaluateSource(.text(module_text), expression, &failure) catch {
            _ = failures.fetchAdd(1, .monotonic);
            slot.* = -1;
            continue;
        };
        defer tree.deinit();
        slot.* = tree.root.int.signed;
    }
}

errors

Handles evaluation errors, rejected settings, log messages, timeouts, and cancellation.

main.zig
//! Handles evaluation errors, rejected settings, log messages, timeouts, and
//! cancellation.

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

/// Prints `trace()` output and warnings that Pkl sends while evaluating.
const PrintLogger = struct {
    fn logger(self: *PrintLogger) zig_pkl.Logger {
        return .{ .ptr = self, .vtable = &.{ .log = log } };
    }

    /// Runs on the manager's response thread, so keep it short. The event
    /// text is only valid during the call.
    fn log(_: *anyopaque, event: zig_pkl.logger.Event) void {
        const level = if (event.level == 0) "trace" else "warn";
        std.debug.print("[{s}] {s} ({s})\n", .{ level, event.message, event.frame_uri });
    }
};

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    var print_logger = PrintLogger{};
    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();

    var buffer: [4096]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &buffer);
    const out = &stdout.interface;
    defer out.flush() catch {};

    // Pkl rejects settings it cannot use, and `rejected` holds its reason.
    // Pkl before 0.29 exits on this one instead: the manager then fails
    // every request with `error.Closed`, and the server's stderr says why.
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| allocator.free(text);
    {
        const disposable = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
        defer disposable.deinit();
        var invalid = zig_pkl.Options.restricted();
        invalid.allowed_modules = &.{"("};
        if (disposable.createEvaluator(invalid, &rejected)) |evaluator| {
            evaluator.close();
        } else |err| switch (err) {
            error.Rejected => try out.print("rejected: {s}\n", .{reason(rejected.?)}),
            error.Closed => {
                const stderr = try disposable.serverStderr(allocator);
                defer if (stderr) |text| allocator.free(text);
                try out.print("server exited: {s}\n", .{reason(stderr orelse "")});
            },
            else => return err,
        }
    }

    var options = zig_pkl.Options.restricted();
    options.logger = print_logger.logger();
    const evaluator = try manager.createEvaluator(options, &rejected);
    defer evaluator.close();

    // `trace` reaches the logger, and the value is still returned.
    var failure: ?[]u8 = null;
    defer if (failure) |text| allocator.free(text);
    var traced = try evaluator.evaluateSource(.text("total = trace(List(1, 2, 3).fold(0, (a, b) -> a + b))"), "total", &failure);
    traced.deinit();

    // A failed evaluation returns `error.EvaluationFailed` and the message.
    if (evaluator.evaluateSource(.text("port: UInt16 = 70000"), "port", &failure)) |result| {
        var tree = result;
        tree.deinit();
    } else |err| switch (err) {
        error.EvaluationFailed => {
            try out.print("evaluation failed: {s}\n", .{reason(failure.?)});
            allocator.free(failure.?);
            failure = null;
        },
        else => return err,
    }

    // A client-side deadline stops waiting; the server finishes on its own.
    const slow = "total = IntSeq(1, 50000000).fold(0, (a, b) -> a + b)";
    if (evaluator.evaluateWithOptions("repl:text", slow, "total", &failure, .{
        .timeout = .{ .duration = .{ .raw = .fromMilliseconds(50), .clock = .awake } },
    })) |result| {
        var tree = result;
        tree.deinit();
        try out.writeAll("finished before the deadline\n");
    } else |err| switch (err) {
        error.Timeout => try out.writeAll("timed out after 50 ms\n"),
        else => return err,
    }

    // Another thread can cancel a request in flight.
    var cancellation = zig_pkl.Cancellation{};
    const canceller = try std.Thread.spawn(.{}, cancelSoon, .{ io, &cancellation });
    defer canceller.join();
    if (evaluator.evaluateWithOptions("repl:text", slow, "total", &failure, .{ .cancellation = &cancellation })) |result| {
        var tree = result;
        tree.deinit();
        try out.writeAll("finished before cancellation\n");
    } else |err| switch (err) {
        error.Canceled => try out.writeAll("canceled\n"),
        else => return err,
    }
}

fn cancelSoon(io: std.Io, cancellation: *zig_pkl.Cancellation) void {
    io.sleep(.fromMilliseconds(50), .awake) catch {};
    cancellation.cancel();
}

/// The reason in a Pkl error message, after its `–– Pkl Error ––` banner.
fn reason(text: []const u8) []const u8 {
    var lines = std.mem.splitScalar(u8, text, '\n');
    while (lines.next()) |line| {
        const trimmed = std.mem.trim(u8, line, " ");
        if (trimmed.len != 0 and std.mem.indexOf(u8, trimmed, "Pkl Error") == null) return trimmed;
    }
    return text;
}

codegen

src/main.zig
const std = @import("std");
const zig_pkl = @import("zig_pkl");
const Service = @import("bindings").service;

/// Evaluates `path` inside this directory's Pkl project and decodes it.
pub fn loadService(allocator: std.mem.Allocator, io: std.Io, manager: *zig_pkl.Manager, path: []const u8, failure: *?[]u8) !Service {
    var rejected: ?[]u8 = null;
    defer if (rejected) |text| manager.allocator.free(text);
    const loader = try manager.createEvaluator(zig_pkl.Options.preconfigured(), &rejected);
    defer loader.close();
    var project = try zig_pkl.project.loadFile(allocator, io, loader, "PklProject");
    defer project.deinit();

    var options = project.options;
    options.allowed_modules = zig_pkl.Options.preconfigured().allowed_modules;
    options.allowed_resources = zig_pkl.Options.preconfigured().allowed_resources;
    const evaluator = try manager.createEvaluator(options, &rejected);
    defer evaluator.close();
    const module_uri = try project.moduleUri(allocator, path);
    defer allocator.free(module_uri);
    return Service.load(allocator, evaluator, .uri(module_uri), failure);
}

pub fn main(init: std.process.Init) !u8 {
    const allocator = init.gpa;
    const io = init.io;
    const args = try init.minimal.args.toSlice(init.arena.allocator());
    const path = if (args.len > 1) args[1] else "pkl/dev.pkl";
    const pkl = init.environ_map.get("PKL_EXEC") orelse "pkl";

    var stdout_buffer: [1024]u8 = undefined;
    var stdout = std.Io.File.stdout().writer(io, &stdout_buffer);
    const out = &stdout.interface;

    const manager = try zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop);
    defer manager.deinit();
    var failure: ?[]u8 = null;
    defer if (failure) |text| manager.allocator.free(text);
    var service = loadService(allocator, io, manager, path, &failure) catch |err| {
        if (failure) |text| std.debug.print("{s}\n", .{text});
        return err;
    };
    defer service.deinit(allocator);

    try out.print("listening on {s}:{d} (log level {t}, timeout {d}{s})\n", .{
        service.host, service.listen_port, service.log_level, service.timeout.value, service.timeout.unit,
    });
    for (service.routes) |route| {
        try out.print("route {s} -> {s} (", .{ route.path, route.backend });
        for (route.methods, 0..) |method, i| try out.print("{s}{t}", .{ if (i == 0) "" else ", ", method });
        try out.writeAll(")\n");
    }
    for (service.backends.entries) |entry| switch (entry.value) {
        .http_backend => |backend| try out.print("backend {s}: {s} weight {d}\n", .{ entry.key, backend.url, backend.weight }),
        .static_backend => |backend| try out.print("backend {s}: files in {s}\n", .{ entry.key, backend.directory }),
    };
    try out.flush();
    return 0;
}

test "the development configuration decodes into the generated types" {
    const allocator = std.testing.allocator;
    const io = std.testing.io;
    const pkl = std.process.Environ.getAlloc(std.testing.environ, allocator, "PKL_EXEC") catch |err| switch (err) {
        error.EnvironmentVariableMissing => try allocator.dupe(u8, "pkl"),
        else => return err,
    };
    defer allocator.free(pkl);
    const manager = zig_pkl.Manager.init(allocator, io, &.{ pkl, "server" }, zig_pkl.Logger.noop) catch |err| switch (err) {
        error.FileNotFound => return if (pklRequired()) error.PklRequired else error.SkipZigTest,
        else => return err,
    };
    defer manager.deinit();
    var failure: ?[]u8 = null;
    defer if (failure) |text| manager.allocator.free(text);
    var service = try loadService(allocator, io, manager, "pkl/dev.pkl", &failure);
    defer service.deinit(allocator);

    try std.testing.expectEqual(@as(u16, 3000), service.listen_port);
    try std.testing.expectEqual(Service.LogLevel.debug, service.log_level);
    try std.testing.expectEqual(@as(f64, 1), service.max_body.value);
    try std.testing.expectEqualStrings("mib", service.max_body.unit);
    try std.testing.expectEqual(@as(usize, 2), service.routes.len);
    try std.testing.expectEqual(@as(usize, 2), service.routes[0].methods.len);
    try std.testing.expectEqual(Service.Method.post, service.routes[0].methods[1]);
    try std.testing.expectEqual(Service.Method.get, service.routes[1].methods[0]);
    try std.testing.expectEqual(@as(i64, 3), service.backends.get("api").?.http_backend.weight);
    try std.testing.expectEqualStrings("public", service.backends.get("assets").?.static_backend.directory);
    try std.testing.expect(service.tls == null);
}

/// Whether `ZIG_PKL_REQUIRE_PKL=1` asks for a missing Pkl to fail the test.
fn pklRequired() bool {
    const flag = std.process.Environ.getAlloc(std.testing.environ, std.testing.allocator, "ZIG_PKL_REQUIRE_PKL") catch return false;
    defer std.testing.allocator.free(flag);
    return std.mem.eql(u8, flag, "1");
}

test "the Pkl defaults are available without Pkl" {
    const service = Service{};
    try std.testing.expectEqual(@as(u16, 8080), service.listen_port);
    try std.testing.expectEqualStrings("127.0.0.1", service.host);
    try std.testing.expectEqual(@as(f64, 5), service.timeout.value);
    try std.testing.expectEqualStrings("s", service.timeout.unit);
    try std.testing.expectEqualStrings("mib", service.max_body.unit);
    try std.testing.expectEqual(@as(usize, 0), service.routes.len);
    try std.testing.expectEqual(@as(usize, 0), service.backends.entries.len);
    const route = Service.Route{ .path = "/", .backend = "api" };
    try std.testing.expectEqualSlices(Service.Method, &.{.get}, route.methods);
}
pkl/dev.pkl
amends "Service.pkl"

port = 3000
logLevel = "debug"

routes {
  new {
    path = "/api"
    backend = "api"
    methods { "POST" }
  }
  new {
    path = "/"
    backend = "assets"
  }
}

backends {
  ["api"] = new HttpBackend {
    url = "http://127.0.0.1:9000"
    weight = 3
  }
  ["assets"] = new StaticBackend {
    directory = "public"
  }
}
pkl/Service.pkl
/// Configuration for an HTTP service.
@zig.Package { name = "service" }
module example.Service

import "@zig/zig.pkl"

/// Address to listen on.
host: String = "127.0.0.1"

@zig.Name { value = "listen_port" }
port: UInt16 = 8080

/// Time allowed for a request before it is canceled.
timeout: Duration = 5.s

maxBody: DataSize = 1.mib

logLevel: "debug" | "info" | "warn" = "info"

routes: Listing<Route>

backends: Mapping<String, Backend>

tls: Tls?

class Route {
  path: String
  /// Key into [backends].
  backend: String
  methods: Listing<Method> = new { "GET" }
}

typealias Method = "GET" | "POST" | "PUT" | "DELETE"

abstract class Backend {
  weight: Int = 1
}

class HttpBackend extends Backend {
  url: String
}

class StaticBackend extends Backend {
  directory: String
}

class Tls {
  certificate: String
  key: String
}
PklProject
amends "pkl:Project"

dependencies {
  ["zig"] = import("../../codegen/PklProject")
}