josephrw's picture
WASM Compiler Bridge deploy
fc8f83a verified
Raw
History Blame Contribute Delete
12.4 kB
import express, { Request, Response } from 'express';
import cors from 'cors';
import { exec } from 'child_process';
import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { promisify } from 'util';
const execAsync = promisify(exec);
const app = express();
const PORT = process.env.PORT || 3456;
const EMSDK_PATH = process.env.EMSDK_PATH || `${process.env.HOME}/wasm-toolchains/emsdk`;
const SWIFTWASM_PATH = process.env.SWIFTWASM_PATH || `${process.env.HOME}/wasm-toolchains/swiftwasm`;
app.use(cors());
app.use(express.json({ limit: '5mb' }));
app.use(express.static('public'));
interface CompileRequest {
code: string;
flags?: string;
}
interface Diagnostic {
line?: number;
column?: number;
message: string;
severity: 'error' | 'warning' | 'note';
file?: string;
}
function getEmsdkEnvCommand(): string {
return `source "${EMSDK_PATH}/emsdk_env.sh"`;
}
function getSwiftPath(): string {
return `${SWIFTWASM_PATH}/usr/bin`;
}
async function findCompiler(compilerName: string, extraPaths: string[] = []): Promise<string | null> {
const paths = [...extraPaths, ...(process.env.PATH?.split(':') || [])];
for (const p of paths) {
try {
await fs.access(join(p, compilerName));
return join(p, compilerName);
} catch { /* ignore */ }
}
return null;
}
async function findInPathsOnly(compilerName: string, searchPaths: string[]): Promise<string | null> {
for (const p of searchPaths) {
try {
await fs.access(join(p, compilerName));
return join(p, compilerName);
} catch { /* ignore */ }
}
return null;
}
async function canTargetWasm32(swiftcPath: string): Promise<boolean> {
try {
const testDir = await fs.mkdtemp(join(tmpdir(), 'swift-wasm-test-'));
const testFile = join(testDir, 'test.swift');
await fs.writeFile(testFile, 'print("hello")\n');
await execAsync(`"${swiftcPath}" -target wasm32-unknown-wasi -o "${join(testDir, 'test.wasm')}" "${testFile}"`, { timeout: 15000 });
await fs.rm(testDir, { recursive: true, force: true }).catch(() => { });
return true;
} catch {
return false;
}
}
function parseDiagnostics(stderr: string, lang: 'cpp' | 'swift'): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
if (!stderr) return diagnostics;
const lines = stderr.split('\n');
if (lang === 'cpp') {
// emcc/clang: file.cpp:12:34: error: message
const re = /^(.+?):(\d+):(\d+):\s*(error|warning|note):\s*(.+)$/;
for (const line of lines) {
const m = line.match(re);
if (m) {
diagnostics.push({
file: m[1],
line: parseInt(m[2], 10),
column: parseInt(m[3], 10),
severity: m[4] as Diagnostic['severity'],
message: m[5],
});
}
}
} else {
// swiftc: file.swift:12:34: error: message
const re = /^(.+?):(\d+):(\d+):\s*(error|warning|note):\s*(.+)$/;
for (const line of lines) {
const m = line.match(re);
if (m) {
diagnostics.push({
file: m[1],
line: parseInt(m[2], 10),
column: parseInt(m[3], 10),
severity: m[4] as Diagnostic['severity'],
message: m[5],
});
}
}
}
return diagnostics;
}
function parseWasmExportsImports(wasmBytes: Buffer): { exports: any[]; imports: any[]; memories: any[] } {
const result = { exports: [] as any[], imports: [] as any[], memories: [] as any[] };
let pos = 8; // skip magic + version
while (pos < wasmBytes.length) {
const id = wasmBytes[pos++];
if (id > 11) break;
let len = 0;
let shift = 0;
while (true) {
const b = wasmBytes[pos++];
len |= (b & 0x7f) << shift;
shift += 7;
if ((b & 0x80) === 0) break;
}
const end = pos + len;
if (id === 1) { // Type section - skip
} else if (id === 2) { // Import section
let count = readLeb(wasmBytes, pos);
for (let i = 0; i < count.value; i++) {
const mod = readString(wasmBytes, pos + count.bytes);
const name = readString(wasmBytes, pos + count.bytes + mod.bytes);
const kind = wasmBytes[pos + count.bytes + mod.bytes + name.bytes];
result.imports.push({ module: mod.str, name: name.str, kind });
}
} else if (id === 7) { // Export section
let count = readLeb(wasmBytes, pos);
for (let i = 0; i < count.value; i++) {
const name = readString(wasmBytes, pos + count.bytes);
const kind = wasmBytes[pos + count.bytes + name.bytes];
const idx = readLeb(wasmBytes, pos + count.bytes + name.bytes + 1);
result.exports.push({ name: name.str, kind, index: idx.value });
}
} else if (id === 5) { // Memory section
let count = readLeb(wasmBytes, pos);
for (let i = 0; i < count.value; i++) {
const flags = wasmBytes[pos + count.bytes];
const min = readLeb(wasmBytes, pos + count.bytes + 1);
result.memories.push({ flags, min: min.value });
}
}
pos = end;
}
return result;
}
function readLeb(buf: Buffer, offset: number): { value: number; bytes: number } {
let value = 0, shift = 0, bytes = 0;
while (true) {
const b = buf[offset + bytes++];
value |= (b & 0x7f) << shift;
shift += 7;
if ((b & 0x80) === 0) break;
}
return { value, bytes };
}
function readString(buf: Buffer, offset: number): { str: string; bytes: number } {
const len = readLeb(buf, offset);
const str = buf.slice(offset + len.bytes, offset + len.bytes + len.value).toString('utf8');
return { str, bytes: len.bytes + len.value };
}
// --- EXAMPLES ---
const examples = {
cpp: [
{
name: 'Hello WASM',
code: `#include <emscripten.h>
#include <stdio.h>
int main() {
printf("Hello from C++ WASM!\\n");
return 0;
}
`
},
{
name: 'Exported Functions',
code: `#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
}
int main() {
return 0;
}
`
},
{
name: 'Memory & Pointer',
code: `#include <emscripten.h>
#include <string.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int sumArray(int* arr, int len) {
int sum = 0;
for (int i = 0; i < len; i++) sum += arr[i];
return sum;
}
EMSCRIPTEN_KEEPALIVE
void reverseInPlace(int* arr, int len) {
for (int i = 0; i < len / 2; i++) {
int tmp = arr[i];
arr[i] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
}
}
}
int main() { return 0; }
`
},
{
name: 'Fibonacci Benchmark',
code: `#include <emscripten.h>
#include <stdio.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
}
int main() {
for (int i = 0; i <= 20; i++) {
printf("fib(%d) = %d\\n", i, fib(i));
}
return 0;
}
`
}
],
swift: [
{
name: 'Hello WASM',
code: `print("Hello from Swift WASM!")
`
},
{
name: 'Exported Functions',
code: `@_cdecl("add")
public func add(a: Int32, b: Int32) -> Int32 {
return a + b
}
@_cdecl("multiply")
public func multiply(a: Int32, b: Int32) -> Int32 {
return a * b
}
`
},
{
name: 'Recursive Factorial',
code: `@_cdecl("factorial")
public func factorial(n: Int32) -> Int32 {
if n <= 1 { return 1 }
return n * factorial(n: n - 1)
}
`
},
{
name: 'Array Sum',
code: `@_cdecl("sumArray")
public func sumArray(ptr: UnsafePointer<Int32>, count: Int32) -> Int32 {
var sum: Int32 = 0
for i in 0..<count {
sum += ptr[Int(i)]
}
return sum
}
`
}
]
};
app.get('/api/examples', (_req: Request, res: Response) => {
res.json(examples);
});
app.get('/api/status', async (_req: Request, res: Response) => {
const emcc = await findCompiler('emcc', [`${EMSDK_PATH}/upstream/emscripten`]);
const swiftc = await findCompiler('swiftc', [getSwiftPath()]);
const swiftWasmCapable = swiftc ? await canTargetWasm32(swiftc) : false;
res.json({
emccAvailable: !!emcc,
emccPath: emcc || null,
swiftcAvailable: swiftWasmCapable,
swiftcPath: swiftWasmCapable ? swiftc : null,
emsdkPath: EMSDK_PATH,
swiftwasmPath: SWIFTWASM_PATH
});
});
app.post('/api/compile/cpp', async (req: Request, res: Response) => {
const { code, flags } = req.body as CompileRequest;
if (!code || typeof code !== 'string') {
res.status(400).json({ success: false, error: 'Missing code field' });
return;
}
const workDir = await fs.mkdtemp(join(tmpdir(), 'cpp-wasm-'));
try {
const emcc = await findInPathsOnly('emcc', [`${EMSDK_PATH}/upstream/emscripten`]);
if (!emcc) {
res.status(503).json({ success: false, error: 'emcc not found. Run: ./scripts/install-toolchains.sh' });
return;
}
await fs.writeFile(join(workDir, 'main.cpp'), code);
const extraFlags = flags ? ` ${flags}` : '';
const cmd = `bash -c '${getEmsdkEnvCommand()} && emcc "${workDir}/main.cpp" -o "${workDir}/main.js" -s EXPORTED_FUNCTIONS="['_main']" -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','UTF8ToString']" -s ALLOW_MEMORY_GROWTH=1 -s INITIAL_MEMORY=16MB -s STACK_SIZE=1MB -s SINGLE_FILE=1 -O2${extraFlags}'`;
const { stdout, stderr } = await execAsync(cmd, {
timeout: 60000,
env: { ...process.env, HOME: process.env.HOME || '' }
});
const jsFile = await fs.readFile(join(workDir, 'main.js'), 'utf-8');
const diagnostics = parseDiagnostics(stderr, 'cpp');
res.json({
success: true,
js: jsFile,
stdout,
stderr,
diagnostics
});
} catch (err: any) {
const diagnostics = parseDiagnostics(err.stderr || '', 'cpp');
res.json({
success: false,
error: err.stderr || err.message,
stdout: err.stdout || '',
diagnostics
});
} finally {
await fs.rm(workDir, { recursive: true, force: true }).catch(() => { });
}
});
app.post('/api/compile/swift', async (req: Request, res: Response) => {
const { code, flags } = req.body as CompileRequest;
if (!code || typeof code !== 'string') {
res.status(400).json({ success: false, error: 'Missing code field' });
return;
}
const workDir = await fs.mkdtemp(join(tmpdir(), 'swift-wasm-'));
try {
const swiftc = await findInPathsOnly('swiftc', [getSwiftPath()]);
if (!swiftc) {
res.status(503).json({ success: false, error: 'swiftc (SwiftWasm) not found. Run: ./scripts/install-toolchains.sh' });
return;
}
await fs.writeFile(join(workDir, 'main.swift'), code);
const extraFlags = flags ? ` ${flags}` : '';
const cmd = `PATH="${getSwiftPath()}:$PATH" swiftc -target wasm32-unknown-wasi -o "${workDir}/main.wasm" "${workDir}/main.swift"${extraFlags}`;
const { stdout, stderr } = await execAsync(cmd, {
timeout: 60000,
env: { ...process.env, HOME: process.env.HOME || '' }
});
const wasmBytes = await fs.readFile(join(workDir, 'main.wasm'));
const wasmInfo = parseWasmExportsImports(wasmBytes);
const diagnostics = parseDiagnostics(stderr, 'swift');
res.json({
success: true,
wasm: wasmBytes.toString('base64'),
stdout,
stderr,
diagnostics,
wasmInfo
});
} catch (err: any) {
const diagnostics = parseDiagnostics(err.stderr || '', 'swift');
res.json({
success: false,
error: err.stderr || err.message,
stdout: err.stdout || '',
diagnostics
});
} finally {
await fs.rm(workDir, { recursive: true, force: true }).catch(() => { });
}
});
app.post('/api/inspect', async (req: Request, res: Response) => {
const { wasmBase64 } = req.body;
if (!wasmBase64 || typeof wasmBase64 !== 'string') {
res.status(400).json({ success: false, error: 'Missing wasmBase64 field' });
return;
}
try {
const wasmBytes = Buffer.from(wasmBase64, 'base64');
const info = parseWasmExportsImports(wasmBytes);
res.json({ success: true, info });
} catch (err: any) {
res.status(500).json({ success: false, error: err.message });
}
});
app.listen(PORT, () => {
console.log(`WASM Compiler Bridge running at http://localhost:${PORT}`);
console.log(`EMSDK_PATH: ${EMSDK_PATH}`);
console.log(`SWIFTWASM_PATH: ${SWIFTWASM_PATH}`);
});