| 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 { } |
| } |
| 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 { } |
| } |
| 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') { |
| |
| 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 { |
| |
| 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; |
| 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) { |
| } else if (id === 2) { |
| 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) { |
| 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) { |
| 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 }; |
| } |
|
|
| |
| 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}`); |
| }); |
|
|