@thi.ng/wasm-api-bindgen
Version:
Polyglot bindings code generators (TS/JS, Zig, C11) for hybrid WebAssembly projects
378 lines (377 loc) • 12 kB
JavaScript
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
};