import { plugin, $, type BunPlugin } from "bun"; import path from 'path'; import fs from 'fs/promises'; interface WasmLoaderConfig { production?: boolean; portable?: boolean; } interface CompilerWithFlags { cc: string; flags: string[]; } const wasiArchiveURL = 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz'; const rtURL = 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/libclang_rt.builtins-wasm32-wasi-25.0.tar.gz'; const getCompiler = async (): Promise => { const wasiDir = path.resolve(import.meta.dir, '..', 'dist', 'wasi'); const cc: CompilerWithFlags = { cc: 'clang', flags: [ '--target=wasm32', '--no-standard-libraries', '-fno-builtin', ], }; await fs.mkdir(wasiDir, { recursive: true }); if (!await Bun.file(path.resolve(wasiDir, 'VERSION')).exists()) { const response = await fetch(wasiArchiveURL); if (!response.ok) { return cc; } const bytes = await response.bytes(); await $`tar -xzv -C ${wasiDir} --strip-components=1 < ${bytes}`; const rtResponse = await fetch(rtURL); if (!rtResponse.ok) { return cc; } const rtBytes = await rtResponse.bytes(); await $`tar -xzv -C ${wasiDir} --strip-components=1 < ${rtBytes}`; } cc.cc = `${path.resolve(wasiDir, 'bin', 'clang')}`; cc.flags = [ '--target=wasm32-wasi', `--sysroot=${path.resolve(wasiDir, 'share', 'wasi-sysroot')}`, ]; return cc; } async function instantiate(url: string) { const memory = new WebAssembly.Memory({ initial: 32, }); let data = new DataView(memory.buffer); const decoder = new TextDecoder(); let buf = ''; let errBuf = ''; const { instance } = await WebAssembly.instantiateStreaming(fetch(url), { env: { memory }, wasi_snapshot_preview1: { random_get: (ptr: number, length: number) => { for (let i = 0; i < length; i++) { data.setUint8(ptr + i, Math.random() * 256); } }, environ_sizes_get: (...args: any[]) => { console.debug(`[environ_sizes_get]`, args); return 0; }, environ_get: (...args: any[]) => { console.debug(`[environ_get]`, args); return 0; }, proc_exit: (...args: any[]) => { console.debug(`[proc_exit]`, args); return 0; }, fd_close: (...args: any[]) => { console.debug(`[fd_close]`, args); return 0; }, fd_seek: (...args: any[]) => { console.debug(`[fd_seek]`, args); return 0; }, fd_write: (fd: number, iovsPtr: number, iovsLength: number, bytesWrittenPtr: number) => { const iovs = new Uint32Array(memory.buffer, iovsPtr, iovsLength * 2); if (fd === 1 || fd === 2) { let text = ""; let totalBytesWritten = 0; for (let i = 0; i < iovsLength * 2; i += 2) { const offset = iovs[i]; const length = iovs[i + 1]; const textChunk = decoder.decode(new Int8Array(memory.buffer, offset, length)); text += textChunk; totalBytesWritten += length; } data.setInt32(bytesWrittenPtr, totalBytesWritten, true); if (fd === 1) { const lines = (buf + text).split('\n'); buf = lines.pop() ?? ''; lines.forEach(l => console.log(`[wasm] ${l}`)); } else { const lines = (errBuf + text).split('\n'); errBuf = lines.pop() ?? ''; lines.forEach(l => console.error(`[wasm] ${l}`)); } } return 0; }, fd_read: (...args: any[]) => { console.debug(`[fd_read]`, args); return 0; }, fd_fdstat_get: (fd: number, fdstatPtr: number) => { console.debug(`[fd_fdstat_get] fd=${fd}, ptr=${fdstatPtr}`); return 0; }, fd_prestat_get: (...args: any[]) => { console.debug(`[fd_prestat_get]`, args); return 0; }, fd_prestat_dir_name: (...args: any[]) => { console.debug(`[fd_prestat_dir_name]`, args); return 0; }, } }); return { ...instance.exports, memory, get data() { return data; }, }; } const wasmPlugin = ({ production }: WasmLoaderConfig = {}): BunPlugin => { const p: BunPlugin = { name: "WASM loader", async setup(build) { build.onLoad({ filter: /\.(c(pp)?|wasm)$/ }, async (args) => { let wasmPath = path.resolve(import.meta.dir, '..', 'dist', 'tmp.wasm'); let jsContent: string = ` ${instantiate} const module = await instantiate(new URL($WASM$)); export default module; `; if (args.path.endsWith('.wasm')) { wasmPath = args.path; } else { const buildAssets = path.resolve(import.meta.dir, 'assets'); const include = `${buildAssets}/include`; const glob = new Bun.Glob(`${buildAssets}/lib/**/*.c`); const stdlib = await Array.fromAsync(glob.scan()); const objPath = wasmPath + '.o'; const cc = await getCompiler(); const features = [ 'bulk-memory', 'extended-const', 'relaxed-simd', 'simd128', 'tail-call', 'sign-ext', 'nontrapping-fptoint', 'reference-types', 'multivalue', ].map(f => `-m${f}`); const flags = [ ...cc.flags, production ? '-O3' : '-O0', '-flto', '-fno-exceptions', '-Wall', '-Wextra', '-Wpedantic', '-Werror', '-Wshadow', '-Wconversion', ...features, '-I', include, ]; const std = args.path.endsWith('.cpp') ? '-std=gnu++26' : '-std=gnu23'; const compileResult = await $`${cc.cc} -c ${flags} ${std} -o ${objPath} ${args.path}`; if (compileResult.exitCode !== 0) { throw new Error('Compile failed, check output'); } const linkFlags = [ '--lto-O3', '--no-entry', '--import-memory', ].map(f => `-Wl,${f}`); const linkResult = await $`${cc.cc} ${flags} -std=gnu23 ${linkFlags} -lstdc++ -nostartfiles -o ${wasmPath} ${objPath} ${stdlib}`; if (linkResult.exitCode !== 0) { throw new Error('Link failed, check output'); } } const wasmContent = await Bun.file(wasmPath).arrayBuffer(); const wasmBuffer = Buffer.from(wasmContent).toString('base64'); const wasmURL = `data:application/wasm;base64,${wasmBuffer}`; return { loader: 'js', contents: jsContent.replace(/new URL\([^)]*\)/, `new URL(${JSON.stringify(wasmURL)})`), }; }); } }; plugin(p); return p; }; export default wasmPlugin;