// 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(); // 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; }