UNPKG

@thi.ng/wasm-api-bindgen

Version:

Polyglot bindings code generators (TS/JS, Zig, C11) for hybrid WebAssembly projects

217 lines (215 loc) 6.18 kB
import { ParseError, flag, oneOf, oneOfMulti, parse, string, strings, usage } 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 { ConsoleLogger } from "@thi.ng/logger"; 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 argOpts = { 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" }), dryRun: flag({ default: false, desc: "enable dry run (don't overwrite files)" }), 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" }) }; const PKG = readJSON(join(process.argv[2], "package.json")); const APP_NAME = PKG.name.split("/")[1]; const HEADER = ` \u2588 \u2588 \u2588 \u2502 \u2588\u2588 \u2588 \u2502 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 ${PKG.name} ${PKG.version} \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2502 Multi-language data bindings code generator \u2588 \u2502 \u2588 \u2588 \u2502 `; const usageOpts = { lineWidth: process.stdout.columns, prefix: `${HEADER} usage: ${APP_NAME} [OPTS] JSON-INPUT-FILE(S) ... ${APP_NAME} --help `, showGroupNames: true, paramWidth: 32 }; const showUsage = () => { process.stderr.write(usage(argOpts, usageOpts)); process.exit(1); }; 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 ); } } }; try { const result = parse(argOpts, process.argv, { start: 3, usageOpts }); if (!result) process.exit(1); const { result: opts, rest } = result; if (!rest.length) showUsage(); if (opts.out && opts.lang.length != opts.out.length) { illegalArgs( `expected ${opts.lang.length} outputs, but got ${opts.out.length}` ); } const ctx = { logger: new ConsoleLogger("wasm-api", opts.debug ? "DEBUG" : "INFO"), 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, rest); generateOutputs(ctx, types); if (ctx.opts.analytics) { writeJSON( resolve(ctx.opts.analytics), types, void 0, " ", ctx.logger ); } } catch (e) { if (!(e instanceof ParseError)) process.stderr.write(e.message); process.exit(1); } export { APP_NAME, HEADER, PKG };