UNPKG

@thi.ng/wasm-api-bindgen

Version:

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

378 lines (377 loc) 12 kB
import { BIGINT_ARRAY_CTORS, BIT_SHIFTS, TYPEDARRAY_CTORS } from "@thi.ng/api/typedarray"; import { isString } from "@thi.ng/checks/is-string"; import { unsupported } from "@thi.ng/errors/unsupported"; import { classifyField } from "./internal/classify.js"; import { ensureLines, ensureStringArray, enumName, injectBody, isEnum, isFuncPointer, isNumeric, isOpaque, isPointer, isStringSlice, isWasmPrim, isWasmString, prefixLines, usesStrings, withIndentation } from "./internal/utils.js"; import { fieldType as zigFieldType } from "./zig.js"; const TYPESCRIPT = (opts = {}) => { const { indent = " " } = opts; const SCOPES = [/\{$/, /(?<!\{.*)\}\)?[;,]?$/]; const gen = { id: "ts", pre: (coll, globalOpts) => { const str = __stringImpl(globalOpts); const res = [ "// @ts-ignore possibly includes unused imports", `import { defType, Pointer, ${str}, type IWasmMemoryAccess, type MemorySlice, type MemoryView, type WasmTypeBase, type WasmTypeKeys } from "@thi.ng/wasm-api";` ]; if (Object.values(coll).some( (t) => t.type === "struct" || t.type === "union" )) { const bits = globalOpts.target.bits; res.push( "// @ts-ignore", `import { __array, __instanceArray, __slice${bits}, __primslice${bits} } from "@thi.ng/wasm-api/memory";` ); } if (usesStrings(coll)) { res.push( "", "// @ts-ignore possibly unused", `const __str = (mem: IWasmMemoryAccess, base: number, isConst = true) => new ${str}(mem, base, isConst);` ); } 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) => { acc.push("/**", ...prefixLines(" * ", doc, opts2.lineWidth), " */"); }, ext: (e, _, acc) => { acc.push( `// external type: ${e.name} (size: ${e.size}, align: ${e.align}) ` ); }, enum: (e, _, acc, opts2) => { const res = []; res.push(`export enum ${e.name} {`); for (let v of e.values) { let line; if (!isString(v)) { v.doc && gen.doc(v.doc, res, opts2); line = enumName(opts2, v.name); if (v.value != null) line += ` = ${v.value}`; } else { line = enumName(opts2, v); } res.push(line + ","); } res.push("}", ""); acc.push(...withIndentation(res, indent, ...SCOPES)); }, struct: (struct, coll, acc, opts2) => { const strType = __stringImpl(opts2); const fields = struct.fields.map((f) => __generateField(f, coll, opts2)).filter((f) => !!f && (f.getter || f.setter)); const lines = []; lines.push( `export interface ${struct.name} extends WasmTypeBase {` ); for (let f of fields) { const doc = __docType(f.field, struct, coll, opts2); doc && gen.doc(doc, lines, opts2); const decl = `${f.field.name}: ${f.type};`; lines.push((!f.setter ? `readonly ` : "") + decl); } injectBody(lines, struct.body?.ts, "decl"); lines.push("}", ""); const pointerDecls = fields.filter((f) => isPointer(f.field.tag) && f.field.len !== 0).map((f) => { return `$${f.field.name}: ${f.type}`; }); const stringDecls = fields.filter( (f) => isWasmString(f.field.type) && !["array", "ptr", "slice"].includes(f.field.tag) ).map((f) => `$${f.field.name}: ${strType}`); lines.push( "// @ts-ignore possibly unused args", `export const $${struct.name} = defType<${struct.name}>(${struct.__align}, ${struct.__size}, (mem, base) => {`, ...pointerDecls.length ? [`let ${pointerDecls.join(", ")};`] : [], ...stringDecls.length ? [`let ${stringDecls.join(", ")};`] : [], `return {` ); for (let f of fields) { if (!f) continue; if (f.getter) { lines.push( `get ${f.field.name}(): ${f.type} {`, ...f.getter, "}," ); } if (f.setter) { lines.push( `set ${f.field.name}(x: ${f.type}) {`, ...f.setter, "}," ); } } injectBody(lines, struct.body?.ts); lines.push("};", "});", ""); acc.push(...withIndentation(lines, indent, ...SCOPES)); }, union: (type, coll, acc, opts2) => { gen.struct(type, coll, acc, opts2); }, // nothing to emit directly funcptr: () => { } }; return gen; }; const __stringImpl = (opts) => isStringSlice(opts.stringType) ? "WasmStringSlice" : "WasmStringPtr"; const __shift = (type) => BIT_SHIFTS[type]; const __addr = (offset) => offset > 0 ? `(base + ${offset})` : "base"; const __addrShift = (offset, shift) => { const bits = __shift(shift); return __addr(offset) + (bits ? " >>> " + bits : ""); }; const __ptr = (target, offset) => `mem.${target.usize}[${__addrShift(offset, target.usize)}]`; const __ptrBody = (type, name, offset, body) => [ `return $${name} || ($${name} = new ${type}(mem, ${__addr(offset)},`, ...body, `));` ]; const __mem = (type, offset) => `mem.${type}[${__addrShift(offset, type)}]`; const __mapStringArray = (target, name, type, len, isConst, isLocal = false) => [ isLocal ? `const $${name}: ${type}[] = [];` : `$${name} = [];`, `for(let i = 0; i < ${len}; i++) $${name}.push(__str(mem, addr + i * ${target.sizeBytes * (type === "WasmStringSlice" ? 2 : 1)}${isConst ? "" : ", false"}));`, `return $${name};` ]; const __primArray = (type, len, offset) => [ `const addr = ${__addrShift(offset, type)};`, `return mem.${type}.subarray(addr, addr + ${len});` ]; const __arrayType = (type) => isNumeric(type) ? TYPEDARRAY_CTORS[type].name : BIGINT_ARRAY_CTORS[type].name; const __docType = (f, parent, coll, opts) => { const doc = [...ensureLines(f.doc || [])]; if (isPointer(f.tag) && f.len === 0) { const typeInfo = `Multi pointer: \`${zigFieldType(f, parent, coll, opts).type}\``; const remarks = "Only the pointer's target address can be accessed"; if (doc.length) { doc.push("", "@remarks", typeInfo, remarks); } else { doc.push(typeInfo, "", "@remarks", remarks); } } else if (isWasmPrim(f.type)) { if (doc.length) doc.push("", "@remarks"); doc.push(`Zig type: \`${zigFieldType(f, parent, coll, opts).type}\``); } return doc.length ? doc : void 0; }; const __generateField = (field, coll, opts) => { if (field.skip) return; if (isFuncPointer(field.type, coll) || isOpaque(field.type)) { field = { ...field, type: opts.target.usize }; } const { classifier, isConst } = classifyField(field, coll); const name = field.name; const offset = field.__offset; const strType = __stringImpl(opts); const isPrim = isWasmPrim(field.type); const $isEnum = isEnum(field.type, coll); let type = field.type; let decl; let getter; let setter; let ptrType; let tag; switch (classifier) { case "str": type = strType; getter = [ `return $${name} || ($${name} = __str(mem, ${__addr(offset)}${isConst ? "" : ", false"}));` ]; break; case "strPtr": type = `Pointer<${strType}>`; decl = `let $${name}: ${type} | null = null;`; getter = __ptrBody(type, name, offset, [ `(addr) => __str(mem, addr, ${isConst})` ]); break; case "strPtrFixed": type = `Pointer<${strType}[]>`; getter = __ptrBody(type, name, offset, [ `(addr) => {`, ...__mapStringArray( opts.target, "buf", strType, field.len, isConst, true ), "}" ]); break; case "strArray": type = `${strType}[]`; getter = [ `const addr = ${__addr(offset)};`, ...__mapStringArray( opts.target, name, strType, field.len, isConst, true ) ]; break; case "strSlice": type = `${strType}[]`; getter = [ `const addr = ${__ptr(opts.target, offset)};`, `const len = ${__ptr( opts.target, offset + opts.target.sizeBytes )};`, ...__mapStringArray( opts.target, name, strType, "len", isConst, true ) ]; break; case "ptr": if (isPrim) { ptrType = `Pointer<number>`; getter = __ptrBody(ptrType, name, offset, [ `(addr) => mem.${type}[addr >>> ${__shift(type)}]` ]); } else { ptrType = `Pointer<${type}>`; getter = __ptrBody(ptrType, name, offset, [ `(addr) => $${type}(mem).instance(addr)` ]); } type = ptrType; decl = `let $${name}: ${ptrType} | null = null;`; break; case "enumPtr": tag = coll[type].tag; type = `Pointer<${type}>`; getter = __ptrBody(type, name, offset, [ `(addr) => mem.${tag}[addr >>> ${__shift(tag)}]` ]); break; case "ptrFixed": if (isPrim) { ptrType = `Pointer<${__arrayType(type)}>`; getter = __ptrBody(ptrType, name, offset, [ `(addr) => mem.${type}.subarray(addr, addr + ${field.len})` ]); } else { ptrType = `Pointer<${type}[]>`; getter = __ptrBody(ptrType, name, offset, [ `(addr) => __array(mem, $${field.type}, addr, ${field.len})` ]); } type = ptrType; break; case "enumPtrFixed": tag = coll[type].tag; type = `Pointer<${__arrayType(tag)}>`; getter = __ptrBody(type, name, offset, [ `(addr) => mem.${tag}.subarray(addr, addr + ${field.len})` ]); break; case "array": case "vec": if (isPrim) { getter = [...__primArray(type, field.len, offset)]; type = __arrayType(type); } else { type += "[]"; getter = [ `return __array(mem, $${field.type}, ${__addr(offset)}, ${field.len});` ]; } break; case "enumArray": tag = coll[type].tag; type = __arrayType(tag); getter = [...__primArray(tag, field.len, offset)]; break; case "slice": case "enumSlice": if (isPrim) { getter = [ `return __primslice${opts.target.bits}(mem, mem.${type}, ${__addr(offset)}, ${__shift(type)});` ]; type = __arrayType(type); } else if ($isEnum) { tag = coll[type].tag; getter = [ `return __primslice${opts.target.bits}(mem, mem.${tag}, ${__addr(offset)}, ${__shift(tag)});` ]; type = __arrayType(tag); } else { type += "[]"; getter = [ `return __slice${opts.target.bits}(mem, $${field.type}, ${__addr(offset)});` ]; } break; case "single": if (isPrim) { getter = [`return ${__mem(type, offset)};`]; setter = [`${__mem(type, offset)} = x;`]; type = isNumeric(type) ? "number" : "bigint"; } else { getter = [`return $${type}(mem).instance(${__addr(offset)});`]; setter = [`mem.u8.set(x.__bytes, ${__addr(offset)});`]; } break; case "enum": { getter = [`return ${__mem(coll[type].tag, offset)};`]; setter = [`${__mem(coll[type].tag, offset)} = x;`]; break; } case "ptrMulti": case "enumPtrMulti": case "strPtrMulti": type = "number"; getter = [`return ${__ptr(opts.target, offset)};`]; setter = [`${__ptr(opts.target, offset)} = x;`]; break; case "pad": return; default: unsupported(`TODO: ${classifier} - please report as issue`); } return { field, type, decl, getter: field.getter !== false ? getter : void 0, setter: field.setter !== false ? setter : void 0 }; }; export { TYPESCRIPT };