206 lines
6.7 KiB
TypeScript
206 lines
6.7 KiB
TypeScript
// Extracts JS_EXPORT-marked functions from a C/C++ source via regex + balanced
|
|
// paren scanning. Handles function-pointer params (inline and typedef'd),
|
|
// templates, and JS_EXPORT_AS renames. Limitations: assumes JS_EXPORT markers
|
|
// and any fn-ptr typedefs they use live in the source file itself, not in
|
|
// transitively-included headers; doesn't honor #if conditionals.
|
|
|
|
export type WasmType = 'i32' | 'i64' | 'f32' | 'f64';
|
|
|
|
export interface FnSig {
|
|
params: WasmType[];
|
|
results: WasmType[];
|
|
}
|
|
|
|
export interface ExportInfo {
|
|
name: string;
|
|
paramNames: string[];
|
|
// Index → wasm signature, for params that are C function pointers.
|
|
fnPtrSigs: Map<number, FnSig>;
|
|
}
|
|
|
|
function cTypeToWasm(cType: string): WasmType {
|
|
const t = cType.trim();
|
|
if (/[*&]/.test(t) || /\[/.test(t)) return 'i32';
|
|
if (/\b(int64_t|uint64_t|long\s+long)\b/.test(t)) return 'i64';
|
|
if (/\bdouble\b/.test(t)) return 'f64';
|
|
if (/\bfloat\b/.test(t)) return 'f32';
|
|
return 'i32';
|
|
}
|
|
|
|
function buildFnSig(retType: string, argList: string): FnSig {
|
|
const ret = retType.trim() === 'void' ? null : cTypeToWasm(retType);
|
|
const results: WasmType[] = ret ? [ret] : [];
|
|
|
|
const params: WasmType[] = [];
|
|
const trimmed = argList.trim();
|
|
if (trimmed && trimmed !== 'void') {
|
|
for (const a of splitArgs(trimmed)) {
|
|
const typeOnly = a.replace(/\s+\w+\s*$/, '').replace(/\[[^\]]*\]\s*$/, '').trim();
|
|
params.push(cTypeToWasm(typeOnly));
|
|
}
|
|
}
|
|
|
|
return { params, results };
|
|
}
|
|
|
|
// Parse a single function-pointer-shaped argument like "void (*cb)(int, float)".
|
|
// Returns null if `arg` doesn't match the inline fn-ptr pattern.
|
|
function parseInlineFnPtrSig(arg: string): FnSig | null {
|
|
const m = arg.match(/^([\s\S]+?)\(\s*\*\s*\w*\s*\)\s*\(([\s\S]*)\)\s*$/);
|
|
if (!m) return null;
|
|
return buildFnSig(m[1], m[2]);
|
|
}
|
|
|
|
// Find all `typedef <ret> (*<name>)(<args>);` declarations and build a name → sig map.
|
|
// The retType char class excludes `;{}` so we don't span across nearby struct
|
|
// declarations or other statements.
|
|
function findFnPtrTypedefs(src: string): Map<string, FnSig> {
|
|
const out = new Map<string, FnSig>();
|
|
const re = /\btypedef\s+([^;{}]+?)\(\s*\*\s*(\w+)\s*\)\s*\(([^)]*)\)\s*;/g;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = re.exec(src)) !== null) {
|
|
out.set(m[2], buildFnSig(m[1], m[3]));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function stripComments(src: string): string {
|
|
return src
|
|
.replace(/\/\/.*$/gm, '')
|
|
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Parse one C parameter declaration, recovering the identifier name and (if any)
|
|
// the function-pointer signature it carries — either via an inline `(*name)(...)`
|
|
// shape or via a typedef name we've previously collected.
|
|
function parseParam(
|
|
arg: string,
|
|
typedefs: Map<string, FnSig>,
|
|
): { name: string; fnSig: FnSig | null } {
|
|
const noDefault = arg.replace(/\s*=\s*[\s\S]*$/, '').trim();
|
|
if (!noDefault) return { name: '', fnSig: null };
|
|
|
|
const inlineSig = parseInlineFnPtrSig(noDefault);
|
|
if (inlineSig) {
|
|
const m = noDefault.match(/\(\s*\*\s*(\w+)\s*\)/);
|
|
return { name: m?.[1] ?? '', fnSig: inlineSig };
|
|
}
|
|
|
|
const nameMatch = noDefault.match(/(\w+)\s*(?:\[[^\]]*\])?\s*$/);
|
|
const name = nameMatch ? nameMatch[1] : '';
|
|
|
|
const typeOnly = noDefault.replace(/\s+\w+\s*$/, '').replace(/\[[^\]]*\]\s*$/, '').trim();
|
|
const typedefSig = typedefs.get(typeOnly);
|
|
if (typedefSig) return { name, fnSig: typedefSig };
|
|
|
|
return { name, fnSig: null };
|
|
}
|
|
|
|
export function extractExports(source: string): ExportInfo[] {
|
|
const src = stripComments(source);
|
|
const typedefs = findFnPtrTypedefs(src);
|
|
const out: ExportInfo[] = [];
|
|
const seen = new Set<string>();
|
|
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 (m[1]) {
|
|
const a = /^\s*\(\s*(\w+)\s*\)/.exec(src.slice(pos));
|
|
if (!a) continue;
|
|
alias = a[1];
|
|
pos += a[0].length;
|
|
}
|
|
|
|
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 argParts = splitArgs(src.slice(parenOpen + 1, parenClose));
|
|
const paramNames: string[] = [];
|
|
const fnPtrSigs = new Map<number, FnSig>();
|
|
argParts.forEach((a, i) => {
|
|
const { name, fnSig } = parseParam(a, typedefs);
|
|
paramNames.push(name);
|
|
if (fnSig) fnPtrSigs.set(i, fnSig);
|
|
});
|
|
|
|
const name = alias ?? funcName;
|
|
if (!seen.has(name)) {
|
|
seen.add(name);
|
|
out.push({ name, paramNames, fnPtrSigs });
|
|
}
|
|
|
|
re.lastIndex = parenClose + 1;
|
|
}
|
|
|
|
return out;
|
|
}
|