diff --git a/example/wasm-vt/index.html b/example/wasm-vt/index.html index f81ffdd47..96acb22ab 100644 --- a/example/wasm-vt/index.html +++ b/example/wasm-vt/index.html @@ -147,80 +147,10 @@ effectsLog.push({ label, data }); } - // Patch the WASM binary to make the function table growable by - // removing its max limit. The build already exports the table - // (via --export-table), but Zig's linker doesn't support - // --growable-table, so the table has a fixed max that prevents - // Table.grow() from JS. This patches the table section (id=4) - // to use flag=0 (no max), allowing JS to add callback entries. - function patchTableGrowable(buffer) { - const bytes = new Uint8Array(buffer); - - function readLEB128(arr, offset) { - let result = 0, shift = 0, bytesRead = 0; - while (true) { - const byte = arr[offset + bytesRead]; - result |= (byte & 0x7f) << shift; - bytesRead++; - if ((byte & 0x80) === 0) break; - shift += 7; - } - return { value: result, bytesRead }; - } - - function encodeLEB128(value) { - const out = []; - do { - let b = value & 0x7f; - value >>>= 7; - if (value !== 0) b |= 0x80; - out.push(b); - } while (value !== 0); - return out; - } - - let pos = 8; // skip magic + version - while (pos < bytes.length) { - const sectionId = bytes[pos]; - const { value: sectionSize, bytesRead: sizeLen } = readLEB128(bytes, pos + 1); - const sectionStart = pos + 1 + sizeLen; - if (sectionId === 4) { - const { value: tableCount, bytesRead: countLen } = readLEB128(bytes, sectionStart); - let tpos = sectionStart + countLen; - const elemType = bytes[tpos++]; // 0x70 = funcref - const flags = bytes[tpos]; - if (!(flags & 1)) return buffer; // already no max - // Has max — rebuild section without it - const { value: min, bytesRead: minLen } = readLEB128(bytes, tpos + 1); - const { bytesRead: maxLen } = readLEB128(bytes, tpos + 1 + minLen); - const afterLimits = tpos + 1 + minLen + maxLen; - const newPayload = new Uint8Array([ - ...encodeLEB128(tableCount), - elemType, - 0x00, // flags: no max - ...encodeLEB128(min), - ...bytes.slice(afterLimits, sectionStart + sectionSize), - ]); - const newSize = encodeLEB128(newPayload.length); - const afterSection = sectionStart + sectionSize; - const result = new Uint8Array(pos + 1 + newSize.length + newPayload.length + (bytes.length - afterSection)); - result.set(bytes.subarray(0, pos)); - let w = pos; - result[w++] = 4; // section id - for (const b of newSize) result[w++] = b; - for (const b of newPayload) result[w++] = b; - result.set(bytes.subarray(afterSection), w); - return result.buffer; - } - pos = sectionStart + sectionSize; - } - return buffer; - } - async function loadWasm() { try { const response = await fetch('../../zig-out/bin/ghostty-vt.wasm'); - const wasmBytes = patchTableGrowable(await response.arrayBuffer()); + const wasmBytes = await response.arrayBuffer(); const wasmModule = await WebAssembly.instantiate(wasmBytes, { env: { diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index c3972e42c..db7f2e2a5 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -9,8 +9,8 @@ const GhosttyZig = @import("GhosttyZig.zig"); /// The step that generates the file. step: *std.Build.Step, -/// The artifact result -artifact: *std.Build.Step.InstallArtifact, +/// The install step for the library output. +artifact: *std.Build.Step, /// The kind of library kind: Kind, @@ -51,11 +51,32 @@ pub fn initWasm( // There is no entrypoint for this wasm module. exe.entry = .disabled; + // Zig's WASM linker doesn't support --growable-table, so the table + // has a fixed max equal to its initial size. Post-process with wabt + // tools (wasm2wat → sed → wat2wasm) to remove the max limit, making + // the table growable from JS via Table.grow(). + const wasm2wat = b.addSystemCommand(&.{"wasm2wat"}); + wasm2wat.addFileArg(exe.getEmittedBin()); + + const awk = b.addSystemCommand(&.{ + "awk", + // Remove the table max from "(table (;0;) MIN MAX funcref)" + // so that it becomes "(table (;0;) MIN funcref)", making the + // table growable from JS. + "/\\(table \\(;[0-9]+;\\) [0-9]+ [0-9]+ funcref\\)/ { sub(/ [0-9]+ funcref\\)/, \" funcref)\") } 1", + }); + awk.addFileArg(wasm2wat.captureStdOut()); + + const wat2wasm = b.addSystemCommand(&.{ "wat2wasm", "--enable-all" }); + wat2wasm.addFileArg(awk.captureStdOut()); + wat2wasm.addArgs(&.{"-o"}); + const output = wat2wasm.addOutputFileArg("ghostty-vt.wasm"); + return .{ - .step = &exe.step, - .artifact = b.addInstallArtifact(exe, .{}), + .step = &wat2wasm.step, + .artifact = &b.addInstallFileWithDir(output, .bin, "ghostty-vt.wasm").step, .kind = .wasm, - .output = exe.getEmittedBin(), + .output = output, .dsym = null, .pkg_config = null, }; @@ -168,7 +189,7 @@ fn initLib( return .{ .step = &lib.step, - .artifact = b.addInstallArtifact(lib, .{}), + .artifact = &b.addInstallArtifact(lib, .{}).step, .kind = kind, .output = lib.getEmittedBin(), .dsym = dsymutil, @@ -181,7 +202,7 @@ pub fn install( step: *std.Build.Step, ) void { const b = step.owner; - step.dependOn(&self.artifact.step); + step.dependOn(self.artifact); if (self.pkg_config) |pkg_config| { step.dependOn(&b.addInstallFileWithDir( pkg_config,