@thi.ng/wasm-api-bindgen
Version:
Polyglot bindings code generators (TS/JS, Zig, C11) for hybrid WebAssembly projects
226 lines (225 loc) • 6.47 kB
JavaScript
import { isNumber } from "@thi.ng/checks";
import { isString } from "@thi.ng/checks/is-string";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { capitalize } from "@thi.ng/strings/case";
import { classifyField } from "./internal/classify.js";
import {
defaultValue,
ensureStringArray,
injectBody,
isOpaque,
isPadding,
isStringSlice,
isUnion,
isWasmString,
prefixLines,
sliceTypes,
withIndentation
} from "./internal/utils.js";
const ZIG = (opts = {}) => {
const INDENT = " ";
const SCOPES = [/\{$/, /(?<!\{.*)\}\)?[;,]?$/];
const gen = {
id: "zig",
pre: (coll) => {
const res = [
`const std = @import("std");`,
`const bindgen = @import("wasm-api-bindgen");`
];
for (let type of sliceTypes(coll)) {
if (type !== "string" && type !== "opaque") {
const name = capitalize(type);
res.push(
`
pub const ${name}Slice = bindgen.Slice([]${type}, [*]${type});`,
`pub const Const${name}Slice = bindgen.Slice([]const ${type}, [*]const ${type});`
);
}
}
if (opts.pre) res.push("", ...ensureStringArray(opts.pre));
return res.join("\n");
},
post: () => opts.post ? isString(opts.post) ? opts.post : opts.post.join("\n") : "",
doc: (doc, acc, opts2, topLevel = false) => {
acc.push(
...prefixLines(topLevel ? "//! " : "/// ", doc, opts2.lineWidth)
);
},
ext: (e, _, acc) => {
acc.push(
`// external type: ${e.name} (size: ${e.size}, align: ${e.align})
`
);
},
enum: (e, _, acc, opts2) => {
const lines = [];
lines.push(`pub const ${e.name} = enum(${e.tag}) {`);
for (let v of e.values) {
let line;
if (!isString(v)) {
v.doc && gen.doc(v.doc, lines, opts2);
line = v.name;
if (v.value != null) line += ` = ${v.value}`;
} else {
line = v;
}
lines.push(line + ",");
}
injectBody(lines, e.body?.zig);
lines.push("};", "");
acc.push(...withIndentation(lines, INDENT, ...SCOPES));
},
struct: (struct, coll, acc, opts2) => __structOrUnion(gen, struct, coll, acc, opts2, INDENT, SCOPES),
union: (union, coll, acc, opts2) => __structOrUnion(gen, union, coll, acc, opts2, INDENT, SCOPES),
funcptr: (ptr, coll, acc, opts2) => {
const args = ptr.args.map((a) => `${a.name}: ${fieldType(a, ptr, coll, opts2).type}`).join(", ");
const rtype = ptr.rtype === "void" ? ptr.rtype : fieldType(
{ name: "return", ...ptr.rtype },
ptr,
coll,
opts2
).type;
acc.push(
`pub const ${ptr.name} = *const fn (${args}) callconv(.c) ${rtype};`,
""
);
}
};
return gen;
};
const __structOrUnion = (gen, spec, coll, acc, opts, indent, scopes) => acc.push(
...withIndentation(
[
`pub const ${spec.name} = extern ${spec.type} {`,
...__generateFields(gen, spec, coll, opts)
],
indent,
...scopes
)
);
const __generateFields = (gen, parent, coll, opts) => {
const res = [];
const ftypes = {};
const name = parent.name;
let padID = 0;
for (let f of parent.fields) {
if (isPadding(f)) {
res.push(`__pad${padID}: [${f.pad}]u8,`);
padID++;
continue;
}
f.doc && gen.doc(f.doc, res, opts);
const { type, defaultVal } = fieldType(f, parent, coll, opts);
ftypes[f.name] = type;
res.push(`${f.name}: ${type}${defaultVal},`);
}
injectBody(res, parent.body?.zig);
res.push("};");
if (opts.debug) {
const fn = (fname, body) => res.push(
"",
`export fn ${name}_${fname}() usize {`,
`return ${body};`,
`}`
);
fn("align", `@alignOf(${name})`);
fn("size", `@sizeOf(${name})`);
for (let f of parent.fields) {
if (isPadding(f)) continue;
fn(f.name + "_align", `@alignOf(${ftypes[f.name]})`);
!isUnion(parent) && fn(f.name + "_offset", `@offsetOf(${name}, "${f.name}")`);
fn(f.name + "_size", `@sizeOf(${ftypes[f.name]})`);
}
}
res.push("");
return res;
};
const fieldType = (f, parent, coll, opts) => {
let type = f.type;
let defaultVal = defaultValue(f, "zig");
const { classifier, isConst } = classifyField(f, coll);
const $isConst = isConst ? "Const" : "";
if (isWasmString(f.type)) {
type = isStringSlice(opts.stringType) ? `bindgen.${$isConst}String` : `bindgen.${$isConst}StringPtr`;
switch (classifier) {
case "strPtr":
type = `*${type}`;
break;
case "strPtrFixed":
type = `*[${f.len}]${type}`;
break;
case "strPtrMulti":
type = `[*]${type}`;
break;
case "strSlice":
type += "Slice";
break;
case "strArray":
type = `[${f.len}]${type}`;
break;
}
} else if (isOpaque(f.type)) {
type = `bindgen.${$isConst}OpaquePtr`;
switch (classifier) {
case "opaquePtr":
type = `*${type}`;
break;
case "opaquePtrFixed":
type = `*[${f.len}]${type}`;
break;
case "opaquePtrMulti":
type = `[*]${type}`;
break;
case "opaqueSlice":
type += "Slice";
break;
case "opaqueArray":
type = `[${f.len}]${type}`;
break;
}
} else {
const $const = isConst ? "const " : "";
const sentinel = f.sentinel != null ? `:${f.sentinel}` : "";
switch (classifier) {
case "ptr":
case "enumPtr":
type = `*${$const}${type}`;
break;
case "ptrFixed":
case "enumPtrFixed":
type = `*${$const}[${f.len}${sentinel}]${type}`;
break;
case "ptrMulti":
case "enumPtrMulti":
type = `[*${sentinel}]${$const}${type}`;
break;
case "slice":
case "enumSlice":
type = `${$isConst}${capitalize(f.type)}Slice`;
break;
case "array":
case "enumArray":
type = `[${f.len}${sentinel}]${type}`;
break;
case "vec":
type = `@Vector(${f.len}, ${type})`;
break;
}
}
if (f.optional) type = "?" + type;
if (defaultVal != void 0) {
if (!(isString(defaultVal) || isNumber(defaultVal))) {
illegalArgs(
`wrong default value for ${parent.name}.${f.name} (${defaultVal})`
);
}
}
return {
type,
defaultVal: defaultVal != void 0 ? ` = ${defaultVal}` : ""
};
};
export {
ZIG,
fieldType
};