UNPKG

@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
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 };