1
0
Fork 0
tsgames/build/plugins/wasmExports.ts

141 lines
4.6 KiB
TypeScript

// Extracts JS_EXPORT-marked functions from a C/C++ source via regex + balanced
// paren scanning. Handles function-pointer params, templates, and JS_EXPORT_AS
// renames. Limitations: assumes JS_EXPORT markers live in the file itself (not
// in transitively-included headers); doesn't honor #if conditionals.
export interface ExportInfo {
name: string;
paramNames: string[];
}
function stripComments(src: string): string {
return src
.replace(/\/\/.*$/gm, '')
.replace(/\/\*[\s\S]*?\*\//g, '');
}
// Pull the parameter identifier out of a single argument declaration.
// Recognizes:
// - function pointer: void (*cb)(int) → cb
// - array: float arr[4] → arr
// - default value: int x = 5 → x
// - plain: const char* name → name
function extractParamName(arg: string): string {
const noDefault = arg.replace(/\s*=\s*[\s\S]*$/, '').trim();
if (!noDefault) return '';
const fnPtr = noDefault.match(/\(\s*\*\s*(\w+)\s*\)/);
if (fnPtr) return fnPtr[1];
const m = noDefault.match(/(\w+)\s*(?:\[[^\]]*\])?\s*$/);
return m ? m[1] : '';
}
function splitArgs(argsStr: string): string[] {
const trimmed = argsStr.trim();
if (!trimmed || trimmed === 'void') return [];
const parts: string[] = [];
let depth = 0;
let start = 0;
for (let i = 0; i < argsStr.length; i++) {
const ch = argsStr[i];
if (ch === '(' || ch === '<' || ch === '[') depth++;
else if (ch === ')' || ch === '>' || ch === ']') depth--;
else if (ch === ',' && depth === 0) {
parts.push(argsStr.slice(start, i));
start = i + 1;
}
}
parts.push(argsStr.slice(start));
return parts;
}
// Find the matching close `)` starting just past an opening `(` at `openIdx`.
// Returns the index of the close paren, or -1 if unmatched.
function matchParen(src: string, openIdx: number): number {
let depth = 1;
for (let i = openIdx + 1; i < src.length; i++) {
if (src[i] === '(') depth++;
else if (src[i] === ')') {
depth--;
if (depth === 0) return i;
}
}
return -1;
}
export function extractExports(source: string): ExportInfo[] {
const src = stripComments(source);
const out: ExportInfo[] = [];
const seen = new Set<string>();
// Match either JS_EXPORT or JS_EXPORT_AS as the entry into a declaration.
const re = /\bJS_EXPORT(_AS)?\b/g;
let m: RegExpExecArray | null;
while ((m = re.exec(src)) !== null) {
let pos = m.index + m[0].length;
let alias: string | undefined;
// If the entry marker is JS_EXPORT_AS, consume its (name) argument.
if (m[1]) {
const a = /^\s*\(\s*(\w+)\s*\)/.exec(src.slice(pos));
if (!a) continue;
alias = a[1];
pos += a[0].length;
}
// Walk forward until the function args `(`. Pick up any additional
// JS_EXPORT / JS_EXPORT_AS attached to the same declaration.
let parenOpen = -1;
while (pos < src.length) {
const ch = src[pos];
if (ch === ';' || ch === '{' || ch === '}') break;
if (src.startsWith('JS_EXPORT_AS', pos)) {
const a = /^JS_EXPORT_AS\s*\(\s*(\w+)\s*\)/.exec(src.slice(pos));
if (a) {
alias = alias ?? a[1];
pos += a[0].length;
continue;
}
}
if (src.startsWith('JS_EXPORT', pos) && !src.startsWith('JS_EXPORT_AS', pos)) {
pos += 'JS_EXPORT'.length;
continue;
}
if (ch === '(') {
parenOpen = pos;
break;
}
pos++;
}
if (parenOpen === -1) continue;
let nameEnd = parenOpen;
while (nameEnd > m.index && /\s/.test(src[nameEnd - 1])) nameEnd--;
let nameStart = nameEnd;
while (nameStart > m.index && /\w/.test(src[nameStart - 1])) nameStart--;
const funcName = src.slice(nameStart, nameEnd);
if (!funcName) continue;
const parenClose = matchParen(src, parenOpen);
if (parenClose === -1) continue;
const argsStr = src.slice(parenOpen + 1, parenClose);
const paramNames = splitArgs(argsStr).map(a => extractParamName(a));
const name = alias ?? funcName;
if (!seen.has(name)) {
seen.add(name);
out.push({ name, paramNames });
}
// Skip past the consumed declaration so we don't re-match its markers.
re.lastIndex = parenClose + 1;
}
return out;
}