wasm_exec.js 11.1 KB
Newer Older
1 2 3 4
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

5 6 7 8 9 10
(() => {
	// Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API).
	const isNodeJS = typeof process !== "undefined";
	if (isNodeJS) {
		global.require = require;
		global.fs = require("fs");
11

12 13 14 15
		const nodeCrypto = require("crypto");
		global.crypto = {
			getRandomValues(b) {
				nodeCrypto.randomFillSync(b);
16
			},
17 18 19
		};

		global.performance = {
20 21 22 23
			now() {
				const [sec, nsec] = process.hrtime();
				return sec * 1000 + nsec / 1000000;
			},
24 25 26 27 28 29
		};

		const util = require("util");
		global.TextEncoder = util.TextEncoder;
		global.TextDecoder = util.TextDecoder;
	} else {
30 31 32 33 34 35 36
		if (typeof window !== "undefined") {
			window.global = window;
		} else if (typeof self !== "undefined") {
			self.global = self;
		} else {
			throw new Error("cannot export Go (neither window nor self is defined)");
		}
37

38 39
		let outputBuf = "";
		global.fs = {
40
			constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
41 42 43 44 45 46 47 48
			writeSync(fd, buf) {
				outputBuf += decoder.decode(buf);
				const nl = outputBuf.lastIndexOf("\n");
				if (nl != -1) {
					console.log(outputBuf.substr(0, nl));
					outputBuf = outputBuf.substr(nl + 1);
				}
				return buf.length;
49
			},
50 51 52 53 54
			openSync(path, flags, mode) {
				const err = new Error("not implemented");
				err.code = "ENOSYS";
				throw err;
			},
55 56
		};
	}
57

58 59
	const encoder = new TextEncoder("utf-8");
	const decoder = new TextDecoder("utf-8");
60

61 62
	global.Go = class {
		constructor() {
63
			this.argv = ["js"];
64 65 66 67 68 69
			this.env = {};
			this.exit = (code) => {
				if (code !== 0) {
					console.warn("exit code:", code);
				}
			};
70 71
			this._callbackTimeouts = new Map();
			this._nextCallbackTimeoutID = 1;
72

73 74 75 76
			const mem = () => {
				// The buffer may change when requesting more memory.
				return new DataView(this._inst.exports.mem.buffer);
			}
77

78 79 80 81
			const setInt64 = (addr, v) => {
				mem().setUint32(addr + 0, v, true);
				mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
			}
82

83 84 85 86 87
			const getInt64 = (addr) => {
				const low = mem().getUint32(addr + 0, true);
				const high = mem().getInt32(addr + 4, true);
				return low + high * 4294967296;
			}
88

89
			const loadValue = (addr) => {
90 91 92 93 94
				const f = mem().getFloat64(addr, true);
				if (!isNaN(f)) {
					return f;
				}

95 96 97
				const id = mem().getUint32(addr, true);
				return this._values[id];
			}
98

99
			const storeValue = (addr, v) => {
Richard Musiol's avatar
Richard Musiol committed
100 101
				const nanHead = 0x7FF80000;

102 103
				if (typeof v === "number") {
					if (isNaN(v)) {
Richard Musiol's avatar
Richard Musiol committed
104
						mem().setUint32(addr + 4, nanHead, true);
105 106 107 108
						mem().setUint32(addr, 0, true);
						return;
					}
					mem().setFloat64(addr, v, true);
109 110
					return;
				}
111 112 113

				switch (v) {
					case undefined:
Richard Musiol's avatar
Richard Musiol committed
114
						mem().setUint32(addr + 4, nanHead, true);
115 116 117
						mem().setUint32(addr, 1, true);
						return;
					case null:
Richard Musiol's avatar
Richard Musiol committed
118
						mem().setUint32(addr + 4, nanHead, true);
119 120 121
						mem().setUint32(addr, 2, true);
						return;
					case true:
Richard Musiol's avatar
Richard Musiol committed
122
						mem().setUint32(addr + 4, nanHead, true);
123 124 125
						mem().setUint32(addr, 3, true);
						return;
					case false:
Richard Musiol's avatar
Richard Musiol committed
126
						mem().setUint32(addr + 4, nanHead, true);
127 128 129 130
						mem().setUint32(addr, 4, true);
						return;
				}

131 132
				let ref = this._refs.get(v);
				if (ref === undefined) {
133 134
					ref = this._values.length;
					this._values.push(v);
135
					this._refs.set(v, ref);
136
				}
Richard Musiol's avatar
Richard Musiol committed
137 138 139 140 141 142 143 144 145 146 147 148 149
				let typeFlag = 0;
				switch (typeof v) {
					case "string":
						typeFlag = 1;
						break;
					case "symbol":
						typeFlag = 2;
						break;
					case "function":
						typeFlag = 3;
						break;
				}
				mem().setUint32(addr + 4, nanHead | typeFlag, true);
150
				mem().setUint32(addr, ref, true);
151
			}
152

153 154 155 156 157
			const loadSlice = (addr) => {
				const array = getInt64(addr + 0);
				const len = getInt64(addr + 8);
				return new Uint8Array(this._inst.exports.mem.buffer, array, len);
			}
158

159 160 161 162 163
			const loadSliceOfValues = (addr) => {
				const array = getInt64(addr + 0);
				const len = getInt64(addr + 8);
				const a = new Array(len);
				for (let i = 0; i < len; i++) {
164
					a[i] = loadValue(array + i * 8);
165 166 167
				}
				return a;
			}
168

169 170 171 172 173
			const loadString = (addr) => {
				const saddr = getInt64(addr + 0);
				const len = getInt64(addr + 8);
				return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
			}
174

175
			const timeOrigin = Date.now() - performance.now();
176
			this.importObject = {
177 178 179
				go: {
					// func wasmExit(code int32)
					"runtime.wasmExit": (sp) => {
180
						const code = mem().getInt32(sp + 8, true);
181
						this.exited = true;
182 183 184 185
						delete this._inst;
						delete this._values;
						delete this._refs;
						this.exit(code);
186 187 188 189 190 191 192
					},

					// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
					"runtime.wasmWrite": (sp) => {
						const fd = getInt64(sp + 8);
						const p = getInt64(sp + 16);
						const n = mem().getInt32(sp + 24, true);
193
						fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
194 195 196 197
					},

					// func nanotime() int64
					"runtime.nanotime": (sp) => {
198
						setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
199 200 201 202 203 204 205 206 207
					},

					// func walltime() (sec int64, nsec int32)
					"runtime.walltime": (sp) => {
						const msec = (new Date).getTime();
						setInt64(sp + 8, msec / 1000);
						mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
					},

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
					// func scheduleCallback(delay int64) int32
					"runtime.scheduleCallback": (sp) => {
						const id = this._nextCallbackTimeoutID;
						this._nextCallbackTimeoutID++;
						this._callbackTimeouts.set(id, setTimeout(
							() => { this._resolveCallbackPromise(); },
							getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
						));
						mem().setInt32(sp + 16, id, true);
					},

					// func clearScheduledCallback(id int32)
					"runtime.clearScheduledCallback": (sp) => {
						const id = mem().getInt32(sp + 8, true);
						clearTimeout(this._callbackTimeouts.get(id));
						this._callbackTimeouts.delete(id);
					},

Richard Musiol's avatar
Richard Musiol committed
226 227 228 229 230
					// func getRandomData(r []byte)
					"runtime.getRandomData": (sp) => {
						crypto.getRandomValues(loadSlice(sp + 8));
					},

231
					// func stringVal(value string) ref
232 233 234 235
					"syscall/js.stringVal": (sp) => {
						storeValue(sp + 24, loadString(sp + 8));
					},

236 237
					// func valueGet(v ref, p string) ref
					"syscall/js.valueGet": (sp) => {
238 239 240
						storeValue(sp + 32, Reflect.get(loadValue(sp + 8), loadString(sp + 16)));
					},

241 242
					// func valueSet(v ref, p string, x ref)
					"syscall/js.valueSet": (sp) => {
243 244 245
						Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
					},

246 247
					// func valueIndex(v ref, i int) ref
					"syscall/js.valueIndex": (sp) => {
248 249 250
						storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
					},

251 252
					// valueSetIndex(v ref, i int, x ref)
					"syscall/js.valueSetIndex": (sp) => {
253 254 255
						Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
					},

256 257
					// func valueCall(v ref, m string, args []ref) (ref, bool)
					"syscall/js.valueCall": (sp) => {
258 259 260 261 262
						try {
							const v = loadValue(sp + 8);
							const m = Reflect.get(v, loadString(sp + 16));
							const args = loadSliceOfValues(sp + 32);
							storeValue(sp + 56, Reflect.apply(m, v, args));
263
							mem().setUint8(sp + 64, 1);
264 265
						} catch (err) {
							storeValue(sp + 56, err);
266
							mem().setUint8(sp + 64, 0);
267 268 269
						}
					},

270 271
					// func valueInvoke(v ref, args []ref) (ref, bool)
					"syscall/js.valueInvoke": (sp) => {
272 273 274 275
						try {
							const v = loadValue(sp + 8);
							const args = loadSliceOfValues(sp + 16);
							storeValue(sp + 40, Reflect.apply(v, undefined, args));
276
							mem().setUint8(sp + 48, 1);
277 278
						} catch (err) {
							storeValue(sp + 40, err);
279
							mem().setUint8(sp + 48, 0);
280 281 282
						}
					},

283 284
					// func valueNew(v ref, args []ref) (ref, bool)
					"syscall/js.valueNew": (sp) => {
285 286 287 288
						try {
							const v = loadValue(sp + 8);
							const args = loadSliceOfValues(sp + 16);
							storeValue(sp + 40, Reflect.construct(v, args));
289
							mem().setUint8(sp + 48, 1);
290 291
						} catch (err) {
							storeValue(sp + 40, err);
292
							mem().setUint8(sp + 48, 0);
293 294 295
						}
					},

296 297
					// func valueLength(v ref) int
					"syscall/js.valueLength": (sp) => {
298 299 300
						setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
					},

301 302
					// valuePrepareString(v ref) (ref, int)
					"syscall/js.valuePrepareString": (sp) => {
303 304 305 306 307
						const str = encoder.encode(String(loadValue(sp + 8)));
						storeValue(sp + 16, str);
						setInt64(sp + 24, str.length);
					},

308 309
					// valueLoadString(v ref, b []byte)
					"syscall/js.valueLoadString": (sp) => {
310 311 312 313
						const str = loadValue(sp + 8);
						loadSlice(sp + 16).set(str);
					},

314 315
					// func valueInstanceOf(v ref, t ref) bool
					"syscall/js.valueInstanceOf": (sp) => {
316
						mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
317 318
					},

319 320 321 322 323
					"debug": (value) => {
						console.log(value);
					},
				}
			};
324 325 326 327
		}

		async run(instance) {
			this._inst = instance;
328
			this._values = [ // TODO: garbage collection
329
				NaN,
330 331
				undefined,
				null,
332 333
				true,
				false,
334 335
				global,
				this._inst.exports.mem,
336
				this,
337
			];
338
			this._refs = new Map();
339
			this._callbackShutdown = false;
340
			this.exited = false;
341

342
			const mem = new DataView(this._inst.exports.mem.buffer)
343 344 345 346 347 348

			// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
			let offset = 4096;

			const strPtr = (str) => {
				let ptr = offset;
349
				new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0"));
350 351 352 353
				offset += str.length + (8 - (str.length % 8));
				return ptr;
			};

354
			const argc = this.argv.length;
355 356

			const argvPtrs = [];
357
			this.argv.forEach((arg) => {
358 359 360
				argvPtrs.push(strPtr(arg));
			});

361
			const keys = Object.keys(this.env).sort();
362 363
			argvPtrs.push(keys.length);
			keys.forEach((key) => {
364
				argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
365 366 367 368
			});

			const argv = offset;
			argvPtrs.forEach((ptr) => {
369 370
				mem.setUint32(offset, ptr, true);
				mem.setUint32(offset + 4, 0, true);
371 372 373
				offset += 8;
			});

374 375
			while (true) {
				const callbackPromise = new Promise((resolve) => {
376 377 378 379 380 381
					this._resolveCallbackPromise = () => {
						if (this.exited) {
							throw new Error("bad callback: Go program has already exited");
						}
						setTimeout(resolve, 0); // make sure it is asynchronous
					};
382 383 384 385 386 387 388
				});
				this._inst.exports.run(argc, argv);
				if (this.exited) {
					break;
				}
				await callbackPromise;
			}
389 390
		}
	}
391 392

	if (isNodeJS) {
393 394 395 396 397 398 399 400 401 402
		if (process.argv.length < 3) {
			process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n");
			process.exit(1);
		}

		const go = new Go();
		go.argv = process.argv.slice(2);
		go.env = process.env;
		go.exit = process.exit;
		WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
403 404 405 406 407
			process.on("exit", (code) => { // Node.js exits if no callback is pending
				if (code === 0 && !go.exited) {
					// deadlock, make Go print error and stack traces
					go._callbackShutdown = true;
					go._inst.exports.run();
408 409
				}
			});
410 411
			return go.run(result.instance);
		}).catch((err) => {
412
			throw err;
413 414 415
		});
	}
})();