Read parame names
This commit is contained in:
parent
850fe0f071
commit
a2bec8a4cc
|
|
@ -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<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;
|
||||
}
|
||||
|
|
@ -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<CompilerWithFlags> => {
|
|||
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<CompilerWithFlags> => {
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, string[]>): 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;',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue