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.defaultTimeoutPklProject
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")
}