// 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; } 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 (*)();` 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 { const out = new Map(); 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, ): { 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(); 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(); 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; }