diff --git a/build/plugins/wasmExports.ts b/build/plugins/wasmExports.ts new file mode 100644 index 0000000..1e9f3f9 --- /dev/null +++ b/build/plugins/wasmExports.ts @@ -0,0 +1,140 @@ +// 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; +} diff --git a/build/plugins/wasmPlugin.ts b/build/plugins/wasmPlugin.ts index 89ceb85..8201561 100644 --- a/build/plugins/wasmPlugin.ts +++ b/build/plugins/wasmPlugin.ts @@ -2,6 +2,7 @@ import { plugin, $, type BunPlugin } from "bun"; import path from 'path'; import fs from 'fs/promises'; import { renderDts } from './wasmTypes'; +import { extractExports } from './wasmExports'; interface WasmLoaderConfig { production?: boolean; @@ -10,7 +11,6 @@ interface WasmLoaderConfig { interface CompilerWithFlags { cc: string; - nm: string; wasmOpt: string | null; reactorCrt: string | null; flags: string[]; @@ -32,7 +32,6 @@ const getCompiler = (): Promise => { const wasiDir = path.resolve(import.meta.dir, '..', '..', 'dist', 'wasi'); const cc: CompilerWithFlags = { cc: 'clang', - nm: 'nm', wasmOpt: Bun.which('wasm-opt') ?? null, reactorCrt: null, flags: [ @@ -73,7 +72,6 @@ const getCompiler = (): Promise => { } cc.cc = `${path.resolve(wasiDir, 'bin', 'clang')}`; - cc.nm = `${path.resolve(wasiDir, 'bin', 'nm')}`; cc.flags = [ '--target=wasm32-wasip1', `--sysroot=${path.resolve(wasiDir, 'share', 'wasi-sysroot')}`, @@ -336,16 +334,8 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => { const allObjs = [inputObjPath, ...libCObjs, ...libCppObjs]; if (await needsRebuild(wasmPath, allObjs)) { - const symbolsResult = await $`${cc.nm} ${inputObjPath}`.quiet(); - - if (symbolsResult.exitCode !== 0) { - throw new Error('nm failed, check output'); - } - - const exports = symbolsResult.text().split('\n') - .map(l => l.match(/^-+ T ([a-z][a-z0-9_]*)/i)) - .filter(m => m != null) - .map(m => m[1]); + const exportInfos = extractExports(await Bun.file(args.path).text()); + const paramNames = new Map(exportInfos.map(e => [e.name, e.paramNames])); const linkFlags = [ '--strip-all', @@ -355,7 +345,7 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => { '--import-memory', '--export-table', '--growable-table', - ...exports.flatMap(e => ['--export-if-defined', e]), + ...exportInfos.flatMap(e => ['--export-if-defined', e.name]), ].map(f => `-Wl,${f}`); const reactorInputs = cc.reactorCrt ? [cc.reactorCrt] : []; @@ -371,7 +361,7 @@ const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => { } const wasmBytes = new Uint8Array(await Bun.file(wasmPath).arrayBuffer()); - await Bun.write(`${args.path}.d.ts`, renderDts(wasmBytes)); + await Bun.write(`${args.path}.d.ts`, renderDts(wasmBytes, paramNames)); } } diff --git a/build/plugins/wasmTypes.ts b/build/plugins/wasmTypes.ts index be28163..c22a29a 100644 --- a/build/plugins/wasmTypes.ts +++ b/build/plugins/wasmTypes.ts @@ -118,9 +118,9 @@ function tsType(valType: number): string { return VALUE_TYPE_TS[valType] ?? 'unknown'; } -function tsSignature({ params, results }: Sig): string { +function tsSignature({ params, results }: Sig, names?: string[]): string { const paramList = params - .map((t, i) => `a${i}: ${tsType(t)}`) + .map((t, i) => `${names?.[i] || `a${i}`}: ${tsType(t)}`) .join(', '); let ret: string; if (results.length === 0) ret = 'void'; @@ -129,7 +129,7 @@ function tsSignature({ params, results }: Sig): string { return `(${paramList}) => ${ret}`; } -export function renderDts(wasmBytes: Uint8Array): string { +export function renderDts(wasmBytes: Uint8Array, paramNames?: Map): string { const sigs = parseWasmExports(wasmBytes); const fns = Array.from(sigs.entries()) .filter(([name]) => !name.startsWith('_')) @@ -138,7 +138,7 @@ export function renderDts(wasmBytes: Uint8Array): string { return [ '// Auto-generated by wasmPlugin. Do not edit.', 'declare const _: {', - ...fns.map(([name, sig]) => ` ${name}: ${tsSignature(sig)};`), + ...fns.map(([name, sig]) => ` ${name}: ${tsSignature(sig, paramNames?.get(name))};`), ' memory: WebAssembly.Memory;', ' table: WebAssembly.Table;', ' readonly data: DataView;', diff --git a/src/games/playground/awoo.cpp b/src/games/playground/awoo.cpp index 022d0cc..f3cc8c3 100644 --- a/src/games/playground/awoo.cpp +++ b/src/games/playground/awoo.cpp @@ -75,12 +75,6 @@ constexpr bit bit::nor(const bit& a, const bit& b) { return result; } -static struct InitProbe { - InitProbe() { - std::cout << "[ctor] _initialize ran" << std::endl; - } -} init_probe; - JS_EXPORT void awoo() { bit a{1, "a"}; bit b{1, "b"}; @@ -91,3 +85,7 @@ JS_EXPORT void awoo() { std::cout << "rand = " << rand() << "\narc4 = " << arc4random() << std::endl; } + +JS_EXPORT_AS(something) void anything(void (*cb)()) { + cb(); +}