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.
819 lines
33 KiB
Zig
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"), ¶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 <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);
|
|
}
|
|
}
|