pumpkin-api-zig/tools/gen.zig
MegalithOfficial 28c3e929ca
Add Zig bindings for the Pumpkin plugin API
The bindings under src/wit are generated from the pumpkin-plugin-wit
submodule by tools/gen.zig. The canonical ABI is handled at comptime in
src/abi.zig, so plugins are plain wasm32-freestanding modules and only
need wasm-tools to become a component.

On top of that there is a small hand written layer: plugin registration,
typed event handlers, declarative commands (cmd), text components from
struct literals (msg) and std.log support.
2026-09-19 17:46:24 +02:00

819 lines
33 KiB
Zig

//! Generates the Zig bindings from the output of `wasm-tools component wit --json`.
//!
//! usage: gen <wit.json> <src-dir>
//!
//! Writes <src-dir>/wit.zig, one file per interface under <src-dir>/wit/ and
//! <src-dir>/wit/exports.zig for the world's exports.
const std = @import("std");
const Allocator = std.mem.Allocator;
const Value = std.json.Value;
const Writer = std.Io.Writer;
pub const File = struct {
path: []const u8,
data: []const u8,
};
const header = "// Generated by tools/gen.zig, do not edit.\n\n";
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
std.debug.print("gen: " ++ fmt ++ "\n", args);
std.process.exit(1);
}
fn oom() noreturn {
fatal("out of memory", .{});
}
fn maybe(value: Value, key: []const u8) ?Value {
if (value != .object) fatal("expected an object around '{s}'", .{key});
const found = value.object.get(key) orelse return null;
return if (found == .null) null else found;
}
fn member(value: Value, key: []const u8) Value {
return maybe(value, key) orelse fatal("missing '{s}'", .{key});
}
fn items(value: Value) []const Value {
if (value != .array) fatal("expected an array", .{});
return value.array.items;
}
fn text(value: Value) []const u8 {
if (value != .string) fatal("expected a string", .{});
return value.string;
}
fn index(value: Value) usize {
if (value != .integer) fatal("expected an index", .{});
return @intCast(value.integer);
}
const Kind = enum {
record,
variant,
@"enum",
flags,
tuple,
list,
option,
result,
handle,
resource,
/// `use` or `type x = y`
type,
};
const Def = struct { kind: Kind, payload: Value };
const Core = enum { i32, i64, f32, f64 };
const Signature = struct {
params: []const Core,
result: ?Core,
/// The result doesn't fit in a single value and goes through memory.
indirect: bool,
};
const Scope = std.StringArrayHashMapUnmanaged(void);
const Gen = struct {
arena: Allocator,
types: []const Value,
interfaces: []const Value,
world: Value,
/// "pumpkin:plugin"
namespace: []const u8,
/// "0.1.0"
version: []const u8,
/// Interfaces the world imports. The rest only contribute types.
imports: std.AutoArrayHashMapUnmanaged(usize, void) = .empty,
/// Interface being written, null for exports.zig.
current: ?usize = null,
/// Other interfaces the file being written refers to.
used: std.AutoArrayHashMapUnmanaged(usize, void) = .empty,
/// `used` of the previous pass over the same file. A parameter can't share
/// its name with an imported module, and the imports are only known once
/// the file has been written.
imported: std.AutoArrayHashMapUnmanaged(usize, void) = .empty,
fn init(arena: Allocator, root: Value) Gen {
const packages = items(member(root, "packages"));
const worlds = items(member(root, "worlds"));
if (packages.len != 1) fatal("expected one package, found {d}", .{packages.len});
if (worlds.len != 1) fatal("expected one world, found {d}", .{worlds.len});
const package = text(member(packages[0], "name"));
const at = std.mem.indexOfScalar(u8, package, '@') orelse fatal("package '{s}' has no version", .{package});
var g: Gen = .{
.arena = arena,
.types = items(member(root, "types")),
.interfaces = items(member(root, "interfaces")),
.world = worlds[0],
.namespace = package[0..at],
.version = package[at + 1 ..],
};
for (member(g.world, "imports").object.values()) |item| {
const iface = maybe(item, "interface") orelse continue;
g.imports.put(arena, index(member(iface, "id")), {}) catch oom();
}
g.validate();
return g;
}
fn def(g: *const Gen, id: usize) Def {
if (id >= g.types.len) fatal("type {d} out of range", .{id});
switch (member(g.types[id], "kind")) {
.string => |name| {
if (std.mem.eql(u8, name, "resource")) return .{ .kind = .resource, .payload = .null };
fatal("unsupported type kind '{s}'", .{name});
},
.object => |o| {
const name = o.keys()[0];
const kind = std.meta.stringToEnum(Kind, name) orelse fatal("unsupported type kind '{s}'", .{name});
return .{ .kind = kind, .payload = o.values()[0] };
},
else => fatal("malformed type {d}", .{id}),
}
}
fn nameOf(g: *const Gen, id: usize) ?[]const u8 {
const name = maybe(g.types[id], "name") orelse return null;
return text(name);
}
fn ownerOf(g: *const Gen, id: usize) ?usize {
const owner = maybe(g.types[id], "owner") orelse return null;
if (owner != .object) return null;
return index(owner.object.get("interface") orelse return null);
}
fn interfaceName(g: *const Gen, iface: usize) []const u8 {
return text(member(g.interfaces[iface], "name"));
}
fn isBorrow(g: *const Gen, ref: Value) bool {
if (ref != .integer) return false;
const d = g.def(index(ref));
return d.kind == .handle and d.payload.object.contains("borrow");
}
fn isOwn(g: *const Gen, ref: Value) bool {
if (ref != .integer) return false;
const d = g.def(index(ref));
return d.kind == .handle and d.payload.object.contains("own");
}
/// The type references a definition is built from.
fn children(g: *const Gen, d: Def) []const Value {
var out: std.ArrayList(Value) = .empty;
switch (d.kind) {
.record => for (items(member(d.payload, "fields"))) |f| out.append(g.arena, member(f, "type")) catch oom(),
.variant => for (items(member(d.payload, "cases"))) |c| {
if (maybe(c, "type")) |t| out.append(g.arena, t) catch oom();
},
.tuple => out.appendSlice(g.arena, items(member(d.payload, "types"))) catch oom(),
.list, .option, .type => out.append(g.arena, d.payload) catch oom(),
.result => {
if (maybe(d.payload, "ok")) |t| out.append(g.arena, t) catch oom();
if (maybe(d.payload, "err")) |t| out.append(g.arena, t) catch oom();
},
.@"enum", .flags, .handle, .resource => {},
}
return out.items;
}
/// Everything the generator or src/abi.zig can't express ends up here
/// instead of in bindings that are quietly wrong.
fn validate(g: *const Gen) void {
for (0..g.types.len) |id| {
const d = g.def(id);
const label = g.nameOf(id) orelse "(anonymous)";
if (d.kind == .flags and items(member(d.payload, "flags")).len > 32) {
fatal("flags '{s}' has more than 32 members", .{label});
}
if (d.kind == .resource) {
const owner = g.ownerOf(id) orelse fatal("resource '{s}' has no interface", .{label});
if (!g.imports.contains(owner)) fatal("resource '{s}' is exported by the guest, which is not supported", .{label});
}
for (g.children(d)) |child| {
if (g.isBorrow(child)) fatal("'{s}' contains a borrowed handle, only parameters may", .{label});
}
}
for (g.interfaces) |iface| {
var it = member(iface, "functions").object.iterator();
while (it.next()) |entry| g.validateFunc(entry.key_ptr.*, entry.value_ptr.*);
}
var it = member(g.world, "exports").object.iterator();
while (it.next()) |entry| {
const func = maybe(entry.value_ptr.*, "function") orelse continue;
g.validateFunc(entry.key_ptr.*, func);
for (items(member(func, "params"))) |p| {
if (g.isBorrow(member(p, "type"))) fatal("export '{s}' borrows a handle, which is not supported", .{entry.key_ptr.*});
}
}
}
fn validateFunc(g: *const Gen, name: []const u8, func: Value) void {
const kind = switch (member(func, "kind")) {
.string => |s| s,
.object => |o| o.keys()[0],
else => fatal("malformed function '{s}'", .{name}),
};
const known = [_][]const u8{ "freestanding", "method", "static", "constructor" };
for (known) |k| {
if (std.mem.eql(u8, kind, k)) break;
} else fatal("function '{s}' is '{s}', which is not supported", .{ name, kind });
if (maybe(func, "result")) |result| {
if (g.isBorrow(result)) fatal("function '{s}' returns a borrowed handle", .{name});
}
}
// naming
const Style = enum { snake, camel, title };
fn words(g: *const Gen, name: []const u8, style: Style) []const u8 {
var out: std.ArrayList(u8) = .empty;
var upper = style == .title;
for (name) |c| {
if (c == '-' or c == '.' or c == '_') {
if (style == .snake) out.append(g.arena, '_') catch oom() else upper = true;
continue;
}
out.append(g.arena, if (upper) std.ascii.toUpper(c) else c) catch oom();
upper = false;
}
return out.items;
}
fn moduleName(g: *const Gen, iface: usize) []const u8 {
return g.words(g.interfaceName(iface), .snake);
}
fn funcName(g: *const Gen, func: Value) []const u8 {
const full = text(member(func, "name"));
if (std.mem.startsWith(u8, full, "[constructor]")) return "init";
const base = if (std.mem.indexOfScalar(u8, full, '.')) |dot| full[dot + 1 ..] else full;
return g.words(base, .camel);
}
fn declare(g: *const Gen, scope: *Scope, name: []const u8, where: []const u8) void {
const entry = scope.getOrPut(g.arena, name) catch oom();
if (entry.found_existing) fatal("'{s}' is declared twice in {s}", .{ name, where });
}
fn print(g: *const Gen, comptime fmt: []const u8, args: anytype) []const u8 {
return std.fmt.allocPrint(g.arena, fmt, args) catch oom();
}
// type references
fn primitive(name: []const u8) []const u8 {
const map = std.StaticStringMap([]const u8).initComptime(.{
.{ "bool", "bool" }, .{ "string", "[]const u8" }, .{ "char", "u21" },
.{ "u8", "u8" }, .{ "u16", "u16" }, .{ "u32", "u32" },
.{ "u64", "u64" }, .{ "s8", "i8" }, .{ "s16", "i16" },
.{ "s32", "i32" }, .{ "s64", "i64" }, .{ "f32", "f32" },
.{ "f64", "f64" },
});
return map.get(name) orelse fatal("unknown primitive '{s}'", .{name});
}
fn writeRef(g: *Gen, w: *Writer, ref: ?Value) Writer.Error!void {
switch (ref orelse return w.writeAll("void")) {
.string => |s| try w.writeAll(primitive(s)),
.integer => try g.writeTypeId(w, index(ref.?)),
else => fatal("malformed type reference", .{}),
}
}
fn writeTypeId(g: *Gen, w: *Writer, id: usize) Writer.Error!void {
const d = g.def(id);
if (g.nameOf(id)) |name| {
if (g.ownerOf(id)) |owner| {
if (g.current != owner) {
g.used.put(g.arena, owner, {}) catch oom();
try w.print("{s}.", .{g.moduleName(owner)});
}
return w.writeAll(g.words(name, .title));
}
// Owned by the world, which only ever aliases interface types.
if (d.kind != .type) fatal("the world defines '{s}' itself, which is not supported", .{name});
return g.writeRef(w, d.payload);
}
if (d.kind == .handle) return g.writeTypeId(w, index(d.payload.object.values()[0]));
try g.writeShape(w, d);
}
/// list, option, result, tuple or an alias, the kinds that may be anonymous.
fn writeShape(g: *Gen, w: *Writer, d: Def) Writer.Error!void {
switch (d.kind) {
.type => try g.writeRef(w, d.payload),
.list => {
try w.writeAll("[]const ");
try g.writeRef(w, d.payload);
},
.option => {
try w.writeAll("?");
try g.writeRef(w, d.payload);
},
.result => {
try w.writeAll("abi.Result(");
try g.writeRef(w, maybe(d.payload, "ok"));
try w.writeAll(", ");
try g.writeRef(w, maybe(d.payload, "err"));
try w.writeAll(")");
},
.tuple => {
try w.writeAll("struct { ");
for (items(member(d.payload, "types")), 0..) |item, i| {
if (i != 0) try w.writeAll(", ");
try g.writeRef(w, item);
}
try w.writeAll(" }");
},
else => fatal("'{s}' needs a name", .{@tagName(d.kind)}),
}
}
// Flattening. src/abi.zig does the same from the Zig types at comptime and
// refuses to compile when the two disagree.
fn join(a: Core, b: Core) Core {
if (a == b) return a;
if ((a == .i32 and b == .f32) or (a == .f32 and b == .i32)) return .i32;
return .i64;
}
fn flatten(g: *const Gen, ref: ?Value, out: *std.ArrayList(Core)) void {
switch (ref orelse return) {
.string => |s| {
const eql = std.mem.eql;
if (eql(u8, s, "string")) return out.appendSlice(g.arena, &.{ .i32, .i32 }) catch oom();
const core: Core = if (eql(u8, s, "u64") or eql(u8, s, "s64"))
.i64
else if (eql(u8, s, "f32"))
.f32
else if (eql(u8, s, "f64"))
.f64
else
.i32;
out.append(g.arena, core) catch oom();
},
.integer => {
const d = g.def(index(ref.?));
switch (d.kind) {
.type => g.flatten(d.payload, out),
.record, .tuple => for (g.children(d)) |child| g.flatten(child, out),
.list => out.appendSlice(g.arena, &.{ .i32, .i32 }) catch oom(),
.variant => {
var payloads: std.ArrayList(?Value) = .empty;
for (items(member(d.payload, "cases"))) |c| payloads.append(g.arena, maybe(c, "type")) catch oom();
g.flattenCases(payloads.items, out);
},
.option => g.flattenCases(&.{ null, d.payload }, out),
.result => g.flattenCases(&.{ maybe(d.payload, "ok"), maybe(d.payload, "err") }, out),
.@"enum", .flags, .handle, .resource => out.append(g.arena, .i32) catch oom(),
}
},
else => fatal("malformed type reference", .{}),
}
}
fn flattenCases(g: *const Gen, payloads: []const ?Value, out: *std.ArrayList(Core)) void {
var joined: std.ArrayList(Core) = .empty;
for (payloads) |payload| {
var flat: std.ArrayList(Core) = .empty;
g.flatten(payload, &flat);
for (flat.items, 0..) |c, i| {
if (i < joined.items.len) {
joined.items[i] = join(joined.items[i], c);
} else {
joined.append(g.arena, c) catch oom();
}
}
}
out.append(g.arena, .i32) catch oom();
out.appendSlice(g.arena, joined.items) catch oom();
}
fn signature(g: *const Gen, func: Value, side: enum { import, @"export" }) Signature {
var params: std.ArrayList(Core) = .empty;
for (items(member(func, "params"))) |p| g.flatten(member(p, "type"), &params);
if (params.items.len > 16) {
params.clearRetainingCapacity();
params.append(g.arena, .i32) catch oom();
}
var results: std.ArrayList(Core) = .empty;
g.flatten(maybe(func, "result"), &results);
if (results.items.len <= 1) {
const result = if (results.items.len == 1) results.items[0] else null;
return .{ .params = params.items, .result = result, .indirect = false };
}
if (side == .import) {
params.append(g.arena, .i32) catch oom();
return .{ .params = params.items, .result = null, .indirect = true };
}
return .{ .params = params.items, .result = .i32, .indirect = true };
}
// declarations
fn writeDocs(w: *Writer, item: Value, indent: []const u8) Writer.Error!void {
const docs = maybe(item, "docs") orelse return;
const contents = maybe(docs, "contents") orelse return;
var lines = std.mem.splitScalar(u8, text(contents), '\n');
while (lines.next()) |line| {
const trimmed = std.mem.trimEnd(u8, line, " \r");
if (trimmed.len == 0) try w.print("{s}///\n", .{indent}) else try w.print("{s}/// {s}\n", .{ indent, trimmed });
}
}
fn fieldName(g: *const Gen, item: Value) std.zig.FormatId {
return std.zig.fmtId(g.words(text(member(item, "name")), .snake));
}
fn isPlainAlias(g: *const Gen, id: usize) bool {
const d = g.def(id);
return d.kind == .type and d.payload == .integer and maybe(g.types[id], "docs") == null;
}
fn writeType(g: *Gen, w: *Writer, id: usize, funcs: Value) Writer.Error!void {
const wit_name = g.nameOf(id).?;
const name = g.words(wit_name, .title);
const d = g.def(id);
try writeDocs(w, g.types[id], "");
try w.print("pub const {s} = ", .{name});
switch (d.kind) {
.record => {
try w.writeAll("struct {\n");
for (items(member(d.payload, "fields"))) |f| {
try writeDocs(w, f, " ");
try w.print(" {f}: ", .{g.fieldName(f)});
try g.writeRef(w, member(f, "type"));
try w.writeAll(",\n");
}
try w.writeAll("};\n\n");
},
.variant => {
try w.writeAll("union(enum) {\n");
for (items(member(d.payload, "cases"))) |c| {
try writeDocs(w, c, " ");
try w.print(" {f}", .{g.fieldName(c)});
if (maybe(c, "type")) |t| {
try w.writeAll(": ");
try g.writeRef(w, t);
}
try w.writeAll(",\n");
}
try w.writeAll("};\n\n");
},
.@"enum" => {
const cases = items(member(d.payload, "cases"));
const tag = if (cases.len <= 1 << 8) "u8" else if (cases.len <= 1 << 16) "u16" else "u32";
try w.print("enum({s}) {{\n", .{tag});
for (cases) |c| {
try writeDocs(w, c, " ");
try w.print(" {f},\n", .{g.fieldName(c)});
}
try w.writeAll("};\n\n");
},
.flags => {
const flags = items(member(d.payload, "flags"));
const bits: usize = if (flags.len <= 8) 8 else if (flags.len <= 16) 16 else 32;
try w.print("packed struct(u{d}) {{\n", .{bits});
for (flags) |f| {
try writeDocs(w, f, " ");
try w.print(" {f}: bool = false,\n", .{g.fieldName(f)});
}
if (flags.len != bits) try w.print(" _: u{d} = 0,\n", .{bits - flags.len});
try w.writeAll("};\n\n");
},
.resource => {
try w.print(
\\enum(u32) {{
\\ _,
\\
\\ pub const resource_drop = &raw.{f};
\\
\\
, .{std.zig.fmtId(g.print("[resource-drop]{s}", .{wit_name}))});
try g.writeMethods(w, id, name, funcs);
try w.print(
\\ /// Keeps the handle alive past the current callback. Release it with `deinit`.
\\ pub fn keep(self: {0s}) void {{
\\ abi.keep({0s}, self);
\\ }}
\\
\\ /// Releases the handle now instead of when the callback returns.
\\ pub fn deinit(self: {0s}) void {{
\\ abi.drop({0s}, self);
\\ }}
\\}};
\\
\\
, .{name});
},
.handle => fatal("'{s}' names a handle type", .{wit_name}),
.type, .list, .option, .result, .tuple => {
try g.writeShape(w, d);
try w.writeAll(if (g.isPlainAlias(id)) ";\n" else ";\n\n");
},
}
}
fn methodOwner(func: Value) ?usize {
const kind = member(func, "kind");
if (kind != .object) return null;
return index(kind.object.values()[0]);
}
fn writeMethods(g: *Gen, w: *Writer, resource: usize, resource_name: []const u8, funcs: Value) Writer.Error!void {
var scope: Scope = .empty;
for ([_][]const u8{ "resource_drop", "keep", "deinit" }) |reserved| g.declare(&scope, reserved, resource_name);
for (funcs.object.values()) |func| {
if (methodOwner(func) == resource) g.declare(&scope, g.funcName(func), resource_name);
}
for (funcs.object.values()) |func| {
if (methodOwner(func) == resource) try g.writeFunc(w, func, " ", resource_name, &scope);
}
}
fn paramName(g: *const Gen, param: Value, scope: *const Scope) []const u8 {
const name = g.words(text(member(param, "name")), .snake);
if (scope.contains(name)) return g.print("{s}_", .{name});
if (std.mem.eql(u8, name, "abi") or std.mem.eql(u8, name, "raw")) return g.print("{s}_", .{name});
for (g.imported.keys()) |iface| {
if (std.mem.eql(u8, g.moduleName(iface), name)) return g.print("{s}_", .{name});
}
return name;
}
fn writeFunc(
g: *Gen,
w: *Writer,
func: Value,
indent: []const u8,
self_type: ?[]const u8,
scope: *const Scope,
) Writer.Error!void {
const params = items(member(func, "params"));
const is_method = std.mem.startsWith(u8, text(member(func, "name")), "[method]");
var names: std.ArrayList([]const u8) = .empty;
for (params) |p| names.append(g.arena, g.paramName(p, scope)) catch oom();
try writeDocs(w, func, indent);
var consumes = false;
for (params, names.items) |p, name| {
if (!g.isOwn(member(p, "type"))) continue;
if (!consumes and maybe(func, "docs") != null) try w.print("{s}///\n", .{indent});
consumes = true;
try w.print("{s}/// Consumes `{s}`.\n", .{ indent, name });
}
try w.print("{s}pub fn {f}(", .{ indent, std.zig.fmtId(g.funcName(func)) });
for (params, names.items, 0..) |p, name, i| {
if (i != 0) try w.writeAll(", ");
try w.print("{f}: ", .{std.zig.fmtId(name)});
if (is_method and i == 0) try w.writeAll(self_type.?) else try g.writeRef(w, member(p, "type"));
}
try w.writeAll(") ");
try g.writeRef(w, maybe(func, "result"));
try w.print(" {{\n{s} return abi.call(raw.{f}, ", .{ indent, std.zig.fmtId(text(member(func, "name"))) });
try g.writeRef(w, maybe(func, "result"));
try w.writeAll(if (params.len > 1) ", .{ " else ", .{");
for (params, names.items, 0..) |p, name, i| {
if (i != 0) try w.writeAll(", ");
if (g.isBorrow(member(p, "type"))) {
try w.print("abi.borrow({f})", .{std.zig.fmtId(name)});
} else {
try w.print("{f}", .{std.zig.fmtId(name)});
}
}
try w.print("{s});\n{s}}}\n\n", .{ if (params.len > 1) " }" else "}", indent });
}
fn writeRaw(w: *Writer, module: []const u8, name: []const u8, sig: Signature) Writer.Error!void {
try w.print(" extern \"{s}\" fn {f}(", .{ module, std.zig.fmtId(name) });
for (sig.params, 0..) |p, i| {
if (i != 0) try w.writeAll(", ");
try w.writeAll(@tagName(p));
}
try w.print(") {s};\n", .{if (sig.result) |r| @tagName(r) else "void"});
}
fn writeImports(g: *const Gen, w: *Writer) Writer.Error!void {
for (g.used.keys()) |iface| {
const name = g.moduleName(iface);
try w.print("const {f} = @import(\"{s}.zig\");\n", .{ std.zig.fmtId(name), name });
}
}
// files
fn interfaceFile(g: *Gen, iface: usize) Writer.Error![]const u8 {
g.current = iface;
g.imported = .empty;
_ = try g.interfacePass(iface);
g.imported = g.used;
return g.interfacePass(iface);
}
fn interfacePass(g: *Gen, iface: usize) Writer.Error![]const u8 {
g.used = .empty;
const where = g.interfaceName(iface);
const types = member(g.interfaces[iface], "types").object;
const no_funcs: Value = .{ .object = .empty };
const funcs = if (g.imports.contains(iface)) member(g.interfaces[iface], "functions") else no_funcs;
const module = g.print("{s}/{s}@{s}", .{ g.namespace, where, g.version });
var scope: Scope = .empty;
for (types.keys()) |name| g.declare(&scope, g.words(name, .title), where);
for (funcs.object.values()) |func| {
if (methodOwner(func) == null) g.declare(&scope, g.funcName(func), where);
}
var body: Writer.Allocating = .init(g.arena);
const w = &body.writer;
// `use`d types come first in the JSON, keep them together like an import block.
var in_aliases = false;
for (types.values()) |id| {
const is_alias = g.isPlainAlias(index(id));
if (in_aliases and !is_alias) try w.writeAll("\n");
in_aliases = is_alias;
try g.writeType(w, index(id), funcs);
}
if (in_aliases) try w.writeAll("\n");
for (funcs.object.values()) |func| {
if (methodOwner(func) == null) try g.writeFunc(w, func, "", null, &scope);
}
var raw: Writer.Allocating = .init(g.arena);
for (funcs.object.keys(), funcs.object.values()) |name, func| {
try writeRaw(&raw.writer, module, name, g.signature(func, .import));
}
for (types.keys(), types.values()) |name, id| {
if (g.def(index(id)).kind != .resource) continue;
const drop: Signature = .{ .params = &.{.i32}, .result = null, .indirect = false };
try writeRaw(&raw.writer, module, g.print("[resource-drop]{s}", .{name}), drop);
}
var out: Writer.Allocating = .init(g.arena);
try out.writer.writeAll(header);
const uses_abi = raw.written().len != 0 or std.mem.indexOf(u8, body.written(), "abi.") != null;
if (uses_abi) try out.writer.writeAll("const abi = @import(\"../abi.zig\");\n");
try g.writeImports(&out.writer);
if (uses_abi or g.used.count() != 0) try out.writer.writeAll("\n");
try out.writer.writeAll(body.written());
if (raw.written().len != 0) try out.writer.print("const raw = struct {{\n{s}}};\n", .{raw.written()});
return g.print("{s}\n", .{std.mem.trimEnd(u8, out.written(), "\n")});
}
fn writeExport(g: *Gen, w: *Writer, export_name: []const u8, func: Value) Writer.Error!void {
const sig = g.signature(func, .@"export");
const params = items(member(func, "params"));
const base = if (std.mem.indexOfScalar(u8, export_name, '#')) |hash| export_name[hash + 1 ..] else export_name;
try writeDocs(w, func, "");
try w.print("pub fn {f}(comptime impl: fn (", .{std.zig.fmtId(g.words(base, .camel))});
for (params, 0..) |p, i| {
if (i != 0) try w.writeAll(", ");
try g.writeRef(w, member(p, "type"));
}
try w.writeAll(") ");
try g.writeRef(w, maybe(func, "result"));
try w.writeAll(") void {\n const glue = struct {\n fn call(");
for (sig.params, 0..) |p, i| {
if (i != 0) try w.writeAll(", ");
try w.print("p{d}: {s}", .{ i, @tagName(p) });
}
try w.print(") callconv(.c) {s} {{\n", .{if (sig.result) |r| @tagName(r) else "void"});
try w.writeAll(" const args = abi.liftArgs(std.meta.ArgsTuple(@TypeOf(impl)), ");
try w.writeAll(if (sig.params.len > 1) ".{ " else ".{");
for (sig.params, 0..) |_, i| {
if (i != 0) try w.writeAll(", ");
try w.print("p{d}", .{i});
}
try w.writeAll(if (sig.params.len > 1) " });\n" else "});\n");
if (!sig.indirect) try w.writeAll(" defer abi.endCall();\n");
try w.writeAll(" return abi.lowerReturn(");
try g.writeRef(w, maybe(func, "result"));
try w.writeAll(", @call(.auto, impl, args));\n }\n");
// The host reads an indirect result after we return, cleanup has to wait for post-return.
if (sig.indirect) try w.writeAll("\n fn post(_: i32) callconv(.c) void {\n abi.endCall();\n }\n");
try w.print(" }};\n @export(&glue.call, .{{ .name = \"{s}\" }});\n", .{export_name});
if (sig.indirect) try w.print(" @export(&glue.post, .{{ .name = \"cabi_post_{s}\" }});\n", .{export_name});
try w.writeAll("}\n\n");
}
fn exportsFile(g: *Gen) Writer.Error![]const u8 {
g.current = null;
g.used = .empty;
g.imported = .empty;
var scope: Scope = .empty;
var body: Writer.Allocating = .init(g.arena);
const exports = member(g.world, "exports").object;
for (exports.keys(), exports.values()) |name, item| {
if (maybe(item, "function")) |func| {
g.declare(&scope, g.words(name, .camel), "the world's exports");
try g.writeExport(&body.writer, name, func);
continue;
}
const iface = index(member(member(item, "interface"), "id"));
const funcs = member(g.interfaces[iface], "functions").object;
for (funcs.keys(), funcs.values()) |func_name, func| {
g.declare(&scope, g.words(func_name, .camel), "the world's exports");
const full = g.print("{s}/{s}@{s}#{s}", .{ g.namespace, g.interfaceName(iface), g.version, func_name });
try g.writeExport(&body.writer, full, func);
}
}
var out: Writer.Allocating = .init(g.arena);
try out.writer.writeAll(header ++ "const std = @import(\"std\");\nconst abi = @import(\"../abi.zig\");\n");
try g.writeImports(&out.writer);
try out.writer.print("\n{s}\n", .{std.mem.trimEnd(u8, body.written(), "\n")});
return out.written();
}
};
/// Paths are relative to the source directory.
pub fn generate(arena: Allocator, source: []const u8) ![]const File {
const root = try std.json.parseFromSliceLeaky(Value, arena, source, .{});
var g: Gen = .init(arena, root);
var files: std.ArrayList(File) = .empty;
var root_file: Writer.Allocating = .init(arena);
try root_file.writer.writeAll(header);
for (0..g.interfaces.len) |iface| {
const name = g.moduleName(iface);
if (std.mem.eql(u8, name, "exports")) fatal("an interface is called 'exports'", .{});
try files.append(arena, .{ .path = g.print("wit/{s}.zig", .{name}), .data = try g.interfaceFile(iface) });
try root_file.writer.print("pub const {f} = @import(\"wit/{s}.zig\");\n", .{ std.zig.fmtId(name), name });
}
try files.append(arena, .{ .path = "wit/exports.zig", .data = try g.exportsFile() });
try root_file.writer.writeAll("pub const exports = @import(\"wit/exports.zig\");\n");
try files.append(arena, .{ .path = "wit.zig", .data = root_file.written() });
return files.items;
}
pub fn main(init: std.process.Init) !void {
const arena = init.arena.allocator();
const io = init.io;
const args = try init.minimal.args.toSlice(arena);
if (args.len != 3) fatal("usage: gen <wit.json> <src-dir>", .{});
const cwd = std.Io.Dir.cwd();
const source = cwd.readFileAlloc(io, args[1], arena, .unlimited) catch |err| {
fatal("can't read {s}: {t}", .{ args[1], err });
};
const out = try cwd.createDirPathOpen(io, args[2], .{});
try out.createDirPath(io, "wit");
for (try generate(arena, source)) |file| {
try out.writeFile(io, .{ .sub_path = file.path, .data = file.data });
}
}
test "demo world" {
var arena_state: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena_state.deinit();
const expected = [_]File{
.{ .path = "wit/types.zig", .data = @embedFile("testdata/expected/wit/types.zig") },
.{ .path = "wit/host.zig", .data = @embedFile("testdata/expected/wit/host.zig") },
.{ .path = "wit/guest.zig", .data = @embedFile("testdata/expected/wit/guest.zig") },
.{ .path = "wit/exports.zig", .data = @embedFile("testdata/expected/wit/exports.zig") },
.{ .path = "wit.zig", .data = @embedFile("testdata/expected/wit.zig") },
};
const files = try generate(arena_state.allocator(), @embedFile("testdata/demo.json"));
try std.testing.expectEqual(expected.len, files.len);
for (expected, files) |want, got| {
try std.testing.expectEqualStrings(want.path, got.path);
try std.testing.expectEqualStrings(want.data, got.data);
}
}