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,