205 lines
7.8 KiB
TypeScript
205 lines
7.8 KiB
TypeScript
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<CompilerWithFlags> => {
|
|
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; |