Skip to content
Source
Sign inSign up
Open History to see the last commit

runtime.ts

246 lines8.4 KB

1import ghosttyWasmUrl from "./vendor/ghostty-vt.wasm?url";
2import ghosttyWritePtyWasmUrl from "./vendor/ghostty-write-pty.wasm?url&no-inline";
3​
4type WasmFunction = (...args: Array<number | bigint>) => number;
5​
6interface TypeField {
7 readonly offset: number;
8 readonly size: number;
9 readonly type: string;
11​
12interface TypeLayout {
13 readonly size: number;
14 readonly align: number;
15 readonly fields: Readonly<Record<string, TypeField>>;
17​
18type TypeLayouts = Readonly<Record<string, TypeLayout>>;
19​
20const textDecoder = new TextDecoder();
21​
22export class GhosttyRuntime {
23 readonly memory: WebAssembly.Memory;
24 readonly layouts: TypeLayouts;
25 private readonly exports: WebAssembly.Exports;
26 private memoryView: DataView;
27 private memoryU8: Uint8Array;
28 private readonly ptyWriters = new Map<number, (data: string) => void>();
29 private nextPtyWriterId = 1;
30 private writePtyFunctionIndex = 0;
31​
32 private constructor(instance: WebAssembly.Instance) {
33 this.exports = instance.exports;
34 const memory = instance.exports.memory;
35 if (!(memory instanceof WebAssembly.Memory)) {
36 throw new Error("libghostty-vt did not export WebAssembly memory");
37 }
38 this.memory = memory;
39 this.memoryView = new DataView(memory.buffer);
40 this.memoryU8 = new Uint8Array(memory.buffer);
41 const jsonPointer = this.call("ghostty_type_json");
42 const bytes = new Uint8Array(memory.buffer);
43 let end = jsonPointer;
44 while (end < bytes.length && bytes[end] !== 0) end += 1;
45 this.layouts = JSON.parse(textDecoder.decode(bytes.subarray(jsonPointer, end))) as TypeLayouts;
46 }
47​
48 static async load(): Promise<GhosttyRuntime> {
49 const response = await fetch(ghosttyWasmUrl);
50 if (!response.ok) {
51 throw new Error(`Unable to load libghostty-vt (${response.status})`);
52 }
53 let instance: WebAssembly.Instance | undefined;
54 const imports = {
55 env: {
56 log: (pointer: number, length: number) => {
57 if (!instance) return;
58 const memory = instance.exports.memory;
59 if (!(memory instanceof WebAssembly.Memory)) return;
60 const message = textDecoder.decode(new Uint8Array(memory.buffer, pointer, length));
61 console.debug("[libghostty-vt]", message);
62 },
63 },
64 };
65 const result = await WebAssembly.instantiate(await response.arrayBuffer(), imports);
66 instance = result.instance;
67 const runtime = new GhosttyRuntime(result.instance);
68 await runtime.installWritePtyTrampoline();
69 return runtime;
70 }
71​
72 call(name: string, ...args: Array<number | bigint>): number {
73 return this.fn(name)(...args);
74 }
75​
76 fn(name: string): WasmFunction {
77 const fn = this.exports[name];
78 if (typeof fn !== "function") {
79 throw new Error(`libghostty-vt export is unavailable: ${name}`);
80 }
81 return fn as WasmFunction;
82 }
83​
84 layout(name: string): TypeLayout {
85 const layout = this.layouts[name];
86 if (!layout) throw new Error(`libghostty-vt type layout is unavailable: ${name}`);
87 return layout;
88 }
89​
90 alloc(size: number): number {
91 const pointer = this.call("ghostty_wasm_alloc_u8_array", size);
92 if (pointer === 0) throw new Error(`libghostty-vt failed to allocate ${size} bytes`);
93 new Uint8Array(this.memory.buffer, pointer, size).fill(0);
94 return pointer;
95 }
96​
97 free(pointer: number, size: number): void {
98 if (pointer !== 0) this.call("ghostty_wasm_free_u8_array", pointer, size);
99 }
100​
101 allocOpaque(): number {
102 const pointer = this.call("ghostty_wasm_alloc_opaque");
103 if (pointer === 0) throw new Error("libghostty-vt failed to allocate an opaque pointer");
104 // Zero before *_new so partial-initialization cleanup cannot free a garbage pointer.
105 new DataView(this.memory.buffer).setUint32(pointer, 0, true);
106 return pointer;
107 }
108​
109 freeOpaque(pointer: number): void {
110 if (pointer !== 0) this.call("ghostty_wasm_free_opaque", pointer);
111 }
112​
113 readPointer(slot: number): number {
114 return this.memoryDataView().getUint32(slot, true);
115 }
116​
117 attachPtyWriter(terminal: number, writer: (data: string) => void): number {
118 if (this.writePtyFunctionIndex === 0) {
119 throw new Error("libghostty-vt PTY callback trampoline is unavailable");
120 }
121 const id = this.nextPtyWriterId++;
122 this.ptyWriters.set(id, writer);
123 this.call("ghostty_terminal_set", terminal, 0, id);
124 this.call("ghostty_terminal_set", terminal, 1, this.writePtyFunctionIndex);
125 return id;
126 }
127​
128 detachPtyWriter(terminal: number, id: number): void {
129 this.call("ghostty_terminal_set", terminal, 1, 0);
130 this.call("ghostty_terminal_set", terminal, 0, 0);
131 this.ptyWriters.delete(id);
132 }
133​
134 view(pointer: number, size?: number): DataView {
135 return new DataView(this.memoryDataView().buffer, pointer, size);
136 }
137​
138 bytes(pointer: number, size: number): Uint8Array {
139 return new Uint8Array(this.memoryDataView().buffer, pointer, size);
140 }
141​
142 memoryDataView(): DataView {
143 this.refreshMemoryViews();
144 return this.memoryView;
145 }
146​
147 memoryBytes(): Uint8Array {
148 this.refreshMemoryViews();
149 return this.memoryU8;
150 }
151​
152 private refreshMemoryViews(): void {
153 // Growing memory detaches the old buffer. Reading `memory.buffer` costs far more than this check.
154 if (this.memoryU8.byteLength === 0) {
155 this.memoryView = new DataView(this.memory.buffer);
156 this.memoryU8 = new Uint8Array(this.memory.buffer);
157 }
158 }
159​
160 setField(pointer: number, structName: string, fieldName: string, value: number): void {
161 const field = this.layout(structName).fields[fieldName];
162 if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`);
163 const view = this.memoryDataView();
164 const offset = pointer + field.offset;
165 switch (field.type) {
166 case "bool":
167 case "u8":
168 view.setUint8(offset, value);
169 return;
170 case "u16":
171 view.setUint16(offset, value, true);
172 return;
173 case "i32":
174 view.setInt32(offset, value, true);
175 return;
176 case "u32":
177 case "enum":
178 view.setUint32(offset, value, true);
179 return;
180 case "u64":
181 view.setBigUint64(offset, BigInt(value), true);
182 return;
183 default:
184 throw new Error(`Unsupported libghostty-vt field type: ${field.type}`);
185 }
186 }
187​
188 readField(pointer: number, structName: string, fieldName: string): number {
189 const field = this.layout(structName).fields[fieldName];
190 if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`);
191 const view = this.memoryDataView();
192 const offset = pointer + field.offset;
193 switch (field.type) {
194 case "bool":
195 case "u8":
196 return view.getUint8(offset);
197 case "u16":
198 return view.getUint16(offset, true);
199 case "i32":
200 return view.getInt32(offset, true);
201 case "u32":
202 case "enum":
203 return view.getUint32(offset, true);
204 case "u64":
205 return Number(view.getBigUint64(offset, true));
206 default:
207 throw new Error(`Unsupported libghostty-vt field type: ${field.type}`);
208 }
209 }
210​
211 private async installWritePtyTrampoline(): Promise<void> {
212 const response = await fetch(ghosttyWritePtyWasmUrl);
213 if (!response.ok) {
214 throw new Error(`Unable to load the libghostty-vt PTY trampoline (${response.status})`);
215 }
216 const result = await WebAssembly.instantiate(await response.arrayBuffer(), {
217 env: {
218 t3_write_pty: (_terminal: number, userdata: number, pointer: number, length: number) => {
219 const writer = this.ptyWriters.get(userdata);
220 if (!writer || length === 0) return;
221 writer(textDecoder.decode(new Uint8Array(this.memory.buffer, pointer, length)));
222 },
223 },
224 });
225 const trampoline = result.instance.exports.ghostty_write_pty;
226 const table = this.exports.__indirect_function_table;
227 if (typeof trampoline !== "function" || !(table instanceof WebAssembly.Table)) {
228 throw new Error("libghostty-vt did not expose its callback table");
229 }
230 const index = table.length;
231 // WebKit grow(1, fn) stores broken type info and traps on call_indirect; table.set canonicalizes it.
232 table.grow(1);
233 table.set(index, trampoline);
234 this.writePtyFunctionIndex = index;
235 }
237​
238let runtimePromise: Promise<GhosttyRuntime> | null = null;
239​
240export function loadGhosttyRuntime(): Promise<GhosttyRuntime> {
241 runtimePromise ??= GhosttyRuntime.load().catch((error) => {
242 runtimePromise = null;
243 throw error;
244 });
245 return runtimePromise;