//! Generates the Zig bindings from the output of `wasm-tools component wit --json`. //! //! usage: gen //! //! Writes /wit.zig, one file per interface under /wit/ and //! /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"), ¶ms); 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 ", .{}); 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); } }