@thi.ng/wasm-api-bindgen
Version:
Polyglot bindings code generators (TS/JS, Zig, C11) for hybrid WebAssembly projects
215 lines (213 loc) • 5.78 kB
JavaScript
import {
ARG_DRY_RUN,
THING_HEADER,
cliApp,
configureLogLevel,
flag,
oneOf,
oneOfMulti,
string,
strings
} from "@thi.ng/args";
import { isArray, isPlainObject, isString } from "@thi.ng/checks";
import { illegalArgs } from "@thi.ng/errors";
import { readJSON, readText, writeJSON, writeText } from "@thi.ng/file-io";
import { mutIn } from "@thi.ng/paths";
import { dirname, join, resolve } from "node:path";
import { C11 } from "./c11.js";
import { generateTypes } from "./codegen.js";
import {
isExternal,
isOpaque,
isPadding,
isSizeT,
isWasmPrim,
isWasmString
} from "./internal/utils.js";
import { TYPESCRIPT } from "./typescript.js";
import { ZIG } from "./zig.js";
const GENERATORS = { c11: C11, ts: TYPESCRIPT, zig: ZIG };
const PKG = readJSON(join(process.argv[2], "package.json"));
const APP_NAME = PKG.name.split("/")[1];
const HEADER = THING_HEADER(
PKG.name,
PKG.version,
"Multi-language data bindings code generator"
);
const invalidSpec = (path, msg) => {
throw new Error(`invalid typedef: ${path}${msg ? ` (${msg})` : ""}`);
};
const addTypeSpec = (ctx, path, coll, spec) => {
if (!(spec.name && spec.type)) invalidSpec(path);
if (!["enum", "ext", "funcptr", "struct", "union"].includes(spec.type))
invalidSpec(path, `${spec.name} type: ${spec.type}`);
if (coll[spec.name]) invalidSpec(path, `duplicate name: ${spec.name}`);
if (spec.body) {
if (!isPlainObject(spec.body))
invalidSpec(path, `${spec.name}.body must be an object`);
for (let lang in spec.body) {
const src = spec.body[lang];
if (isString(src) && src[0] === "@") {
spec.body[lang] = readText(src.substring(1), ctx.logger);
}
}
}
ctx.logger.debug(`registering ${spec.type}: ${spec.name}`);
coll[spec.name] = spec;
spec.__path = path;
};
const validateTypeRefs = (coll) => {
for (let spec of Object.values(coll)) {
if (!["funcptr", "struct", "union"].includes(spec.type)) continue;
for (let f of spec.fields || spec.args) {
if (!(isPadding(f) || isWasmPrim(f.type) || isSizeT(f.type) || isOpaque(f.type) || isWasmString(f.type) || isExternal(f.type, coll) || coll[f.type])) {
invalidSpec(
spec.__path,
`${spec.name}.${f.name} has unknown type: ${f.type}`
);
}
}
}
};
const parseTypeSpecs = (ctx, inputs) => {
const coll = {};
for (let path of inputs) {
try {
const spec = readJSON(resolve(path), ctx.logger);
if (isArray(spec)) {
for (let s of spec) addTypeSpec(ctx, path, coll, s);
} else if (isPlainObject(spec)) {
addTypeSpec(ctx, path, coll, spec);
} else {
invalidSpec(path);
}
} catch (e) {
process.stderr.write(e.message);
process.exit(1);
}
}
validateTypeRefs(coll);
return coll;
};
const generateOutputs = ({ config, logger, opts }, coll) => {
for (let i = 0; i < opts.lang.length; i++) {
const lang = opts.lang[i];
logger.debug(`generating ${lang.toUpperCase()} output...`);
const src = generateTypes(
coll,
GENERATORS[lang](config[lang]),
config.global
);
if (opts.out) {
writeText(resolve(opts.out[i]), src, logger, opts.dryRun);
} else {
process.stdout.write(src + "\n");
}
}
};
const resolveUserCode = (ctx, conf, key) => {
if (conf[key]) {
if (isString(conf[key]) && conf[key][0] === "@") {
conf[key] = readText(
resolve(
dirname(ctx.opts.config),
conf[key].substring(1)
),
ctx.logger
);
}
}
};
const CMD = {
desc: "",
opts: {},
inputs: [1, Infinity],
fn: async ({ opts, inputs, logger }) => {
if (opts.out && opts.lang.length !== opts.out.length) {
illegalArgs(
`expected ${opts.lang.length} outputs, but got ${opts.out.length}`
);
}
const ctx = {
logger,
config: { global: {} },
opts
};
if (opts.config) {
opts.config = resolve(opts.config);
ctx.config = readJSON(opts.config, ctx.logger);
for (let id in ctx.config) {
const conf = ctx.config[id];
resolveUserCode(ctx, conf, "pre");
resolveUserCode(ctx, conf, "post");
}
}
opts.debug && mutIn(ctx, ["config", "global", "debug"], true);
opts.string && mutIn(ctx, ["config", "global", "stringType"], opts.string);
const types = parseTypeSpecs(ctx, inputs);
generateOutputs(ctx, types);
if (ctx.opts.analytics) {
writeJSON(
resolve(ctx.opts.analytics),
types,
void 0,
" ",
ctx.logger
);
}
}
};
cliApp({
name: APP_NAME,
start: 3,
opts: {
...ARG_DRY_RUN,
analytics: string({
alias: "a",
hint: "FILE",
desc: "output file path for raw codegen analytics"
}),
config: string({
alias: "c",
hint: "FILE",
desc: "JSON config file with codegen options"
}),
debug: flag({
alias: "d",
default: false,
desc: "enable debug output & functions"
}),
lang: oneOfMulti(Object.keys(GENERATORS), {
alias: "l",
desc: "target language",
default: ["ts", "zig"],
delim: ","
}),
out: strings({ alias: "o", hint: "FILE", desc: "output file path" }),
string: oneOf(["slice", "ptr"], {
alias: "s",
hint: "TYPE",
desc: "Force string type implementation"
})
},
commands: { CMD },
single: true,
usage: {
lineWidth: process.stdout.columns,
prefix: `${HEADER}
usage: ${APP_NAME} [OPTS] JSON-INPUT-FILE(S) ...
${APP_NAME} --help
`,
showGroupNames: true,
paramWidth: 32
},
ctx: async (ctx) => {
configureLogLevel(ctx.logger, ctx.opts.debug);
return ctx;
}
});
export {
APP_NAME,
HEADER,
PKG
};