├── README.md
├── assets
├── index.html
└── wasm_exec.js
├── cmd
├── server
│ └── main.go
└── wasm
│ └── main.go
└── go.mod
/README.md:
--------------------------------------------------------------------------------
1 | ### Run Go programs in the browser using WebAssembly
2 | Source code for the WebAssembly tutorial at https://golangbot.com/webassembly-using-go/.
3 | This code has been tested on Go 1.21.0.
4 |
5 | ### Donate
6 | Please donate and support me to create more free content.
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/assets/wasm_exec.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | "use strict";
6 |
7 | (() => {
8 | const enosys = () => {
9 | const err = new Error("not implemented");
10 | err.code = "ENOSYS";
11 | return err;
12 | };
13 |
14 | if (!globalThis.fs) {
15 | let outputBuf = "";
16 | globalThis.fs = {
17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
18 | writeSync(fd, buf) {
19 | outputBuf += decoder.decode(buf);
20 | const nl = outputBuf.lastIndexOf("\n");
21 | if (nl != -1) {
22 | console.log(outputBuf.substring(0, nl));
23 | outputBuf = outputBuf.substring(nl + 1);
24 | }
25 | return buf.length;
26 | },
27 | write(fd, buf, offset, length, position, callback) {
28 | if (offset !== 0 || length !== buf.length || position !== null) {
29 | callback(enosys());
30 | return;
31 | }
32 | const n = this.writeSync(fd, buf);
33 | callback(null, n);
34 | },
35 | chmod(path, mode, callback) { callback(enosys()); },
36 | chown(path, uid, gid, callback) { callback(enosys()); },
37 | close(fd, callback) { callback(enosys()); },
38 | fchmod(fd, mode, callback) { callback(enosys()); },
39 | fchown(fd, uid, gid, callback) { callback(enosys()); },
40 | fstat(fd, callback) { callback(enosys()); },
41 | fsync(fd, callback) { callback(null); },
42 | ftruncate(fd, length, callback) { callback(enosys()); },
43 | lchown(path, uid, gid, callback) { callback(enosys()); },
44 | link(path, link, callback) { callback(enosys()); },
45 | lstat(path, callback) { callback(enosys()); },
46 | mkdir(path, perm, callback) { callback(enosys()); },
47 | open(path, flags, mode, callback) { callback(enosys()); },
48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
49 | readdir(path, callback) { callback(enosys()); },
50 | readlink(path, callback) { callback(enosys()); },
51 | rename(from, to, callback) { callback(enosys()); },
52 | rmdir(path, callback) { callback(enosys()); },
53 | stat(path, callback) { callback(enosys()); },
54 | symlink(path, link, callback) { callback(enosys()); },
55 | truncate(path, length, callback) { callback(enosys()); },
56 | unlink(path, callback) { callback(enosys()); },
57 | utimes(path, atime, mtime, callback) { callback(enosys()); },
58 | };
59 | }
60 |
61 | if (!globalThis.process) {
62 | globalThis.process = {
63 | getuid() { return -1; },
64 | getgid() { return -1; },
65 | geteuid() { return -1; },
66 | getegid() { return -1; },
67 | getgroups() { throw enosys(); },
68 | pid: -1,
69 | ppid: -1,
70 | umask() { throw enosys(); },
71 | cwd() { throw enosys(); },
72 | chdir() { throw enosys(); },
73 | }
74 | }
75 |
76 | if (!globalThis.crypto) {
77 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
78 | }
79 |
80 | if (!globalThis.performance) {
81 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
82 | }
83 |
84 | if (!globalThis.TextEncoder) {
85 | throw new Error("globalThis.TextEncoder is not available, polyfill required");
86 | }
87 |
88 | if (!globalThis.TextDecoder) {
89 | throw new Error("globalThis.TextDecoder is not available, polyfill required");
90 | }
91 |
92 | const encoder = new TextEncoder("utf-8");
93 | const decoder = new TextDecoder("utf-8");
94 |
95 | globalThis.Go = class {
96 | constructor() {
97 | this.argv = ["js"];
98 | this.env = {};
99 | this.exit = (code) => {
100 | if (code !== 0) {
101 | console.warn("exit code:", code);
102 | }
103 | };
104 | this._exitPromise = new Promise((resolve) => {
105 | this._resolveExitPromise = resolve;
106 | });
107 | this._pendingEvent = null;
108 | this._scheduledTimeouts = new Map();
109 | this._nextCallbackTimeoutID = 1;
110 |
111 | const setInt64 = (addr, v) => {
112 | this.mem.setUint32(addr + 0, v, true);
113 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
114 | }
115 |
116 | const setInt32 = (addr, v) => {
117 | this.mem.setUint32(addr + 0, v, true);
118 | }
119 |
120 | const getInt64 = (addr) => {
121 | const low = this.mem.getUint32(addr + 0, true);
122 | const high = this.mem.getInt32(addr + 4, true);
123 | return low + high * 4294967296;
124 | }
125 |
126 | const loadValue = (addr) => {
127 | const f = this.mem.getFloat64(addr, true);
128 | if (f === 0) {
129 | return undefined;
130 | }
131 | if (!isNaN(f)) {
132 | return f;
133 | }
134 |
135 | const id = this.mem.getUint32(addr, true);
136 | return this._values[id];
137 | }
138 |
139 | const storeValue = (addr, v) => {
140 | const nanHead = 0x7FF80000;
141 |
142 | if (typeof v === "number" && v !== 0) {
143 | if (isNaN(v)) {
144 | this.mem.setUint32(addr + 4, nanHead, true);
145 | this.mem.setUint32(addr, 0, true);
146 | return;
147 | }
148 | this.mem.setFloat64(addr, v, true);
149 | return;
150 | }
151 |
152 | if (v === undefined) {
153 | this.mem.setFloat64(addr, 0, true);
154 | return;
155 | }
156 |
157 | let id = this._ids.get(v);
158 | if (id === undefined) {
159 | id = this._idPool.pop();
160 | if (id === undefined) {
161 | id = this._values.length;
162 | }
163 | this._values[id] = v;
164 | this._goRefCounts[id] = 0;
165 | this._ids.set(v, id);
166 | }
167 | this._goRefCounts[id]++;
168 | let typeFlag = 0;
169 | switch (typeof v) {
170 | case "object":
171 | if (v !== null) {
172 | typeFlag = 1;
173 | }
174 | break;
175 | case "string":
176 | typeFlag = 2;
177 | break;
178 | case "symbol":
179 | typeFlag = 3;
180 | break;
181 | case "function":
182 | typeFlag = 4;
183 | break;
184 | }
185 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
186 | this.mem.setUint32(addr, id, true);
187 | }
188 |
189 | const loadSlice = (addr) => {
190 | const array = getInt64(addr + 0);
191 | const len = getInt64(addr + 8);
192 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
193 | }
194 |
195 | const loadSliceOfValues = (addr) => {
196 | const array = getInt64(addr + 0);
197 | const len = getInt64(addr + 8);
198 | const a = new Array(len);
199 | for (let i = 0; i < len; i++) {
200 | a[i] = loadValue(array + i * 8);
201 | }
202 | return a;
203 | }
204 |
205 | const loadString = (addr) => {
206 | const saddr = getInt64(addr + 0);
207 | const len = getInt64(addr + 8);
208 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
209 | }
210 |
211 | const timeOrigin = Date.now() - performance.now();
212 | this.importObject = {
213 | _gotest: {
214 | add: (a, b) => a + b,
215 | },
216 | gojs: {
217 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
218 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
219 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
220 | // This changes the SP, thus we have to update the SP used by the imported function.
221 |
222 | // func wasmExit(code int32)
223 | "runtime.wasmExit": (sp) => {
224 | sp >>>= 0;
225 | const code = this.mem.getInt32(sp + 8, true);
226 | this.exited = true;
227 | delete this._inst;
228 | delete this._values;
229 | delete this._goRefCounts;
230 | delete this._ids;
231 | delete this._idPool;
232 | this.exit(code);
233 | },
234 |
235 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
236 | "runtime.wasmWrite": (sp) => {
237 | sp >>>= 0;
238 | const fd = getInt64(sp + 8);
239 | const p = getInt64(sp + 16);
240 | const n = this.mem.getInt32(sp + 24, true);
241 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
242 | },
243 |
244 | // func resetMemoryDataView()
245 | "runtime.resetMemoryDataView": (sp) => {
246 | sp >>>= 0;
247 | this.mem = new DataView(this._inst.exports.mem.buffer);
248 | },
249 |
250 | // func nanotime1() int64
251 | "runtime.nanotime1": (sp) => {
252 | sp >>>= 0;
253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
254 | },
255 |
256 | // func walltime() (sec int64, nsec int32)
257 | "runtime.walltime": (sp) => {
258 | sp >>>= 0;
259 | const msec = (new Date).getTime();
260 | setInt64(sp + 8, msec / 1000);
261 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
262 | },
263 |
264 | // func scheduleTimeoutEvent(delay int64) int32
265 | "runtime.scheduleTimeoutEvent": (sp) => {
266 | sp >>>= 0;
267 | const id = this._nextCallbackTimeoutID;
268 | this._nextCallbackTimeoutID++;
269 | this._scheduledTimeouts.set(id, setTimeout(
270 | () => {
271 | this._resume();
272 | while (this._scheduledTimeouts.has(id)) {
273 | // for some reason Go failed to register the timeout event, log and try again
274 | // (temporary workaround for https://github.com/golang/go/issues/28975)
275 | console.warn("scheduleTimeoutEvent: missed timeout event");
276 | this._resume();
277 | }
278 | },
279 | getInt64(sp + 8),
280 | ));
281 | this.mem.setInt32(sp + 16, id, true);
282 | },
283 |
284 | // func clearTimeoutEvent(id int32)
285 | "runtime.clearTimeoutEvent": (sp) => {
286 | sp >>>= 0;
287 | const id = this.mem.getInt32(sp + 8, true);
288 | clearTimeout(this._scheduledTimeouts.get(id));
289 | this._scheduledTimeouts.delete(id);
290 | },
291 |
292 | // func getRandomData(r []byte)
293 | "runtime.getRandomData": (sp) => {
294 | sp >>>= 0;
295 | crypto.getRandomValues(loadSlice(sp + 8));
296 | },
297 |
298 | // func finalizeRef(v ref)
299 | "syscall/js.finalizeRef": (sp) => {
300 | sp >>>= 0;
301 | const id = this.mem.getUint32(sp + 8, true);
302 | this._goRefCounts[id]--;
303 | if (this._goRefCounts[id] === 0) {
304 | const v = this._values[id];
305 | this._values[id] = null;
306 | this._ids.delete(v);
307 | this._idPool.push(id);
308 | }
309 | },
310 |
311 | // func stringVal(value string) ref
312 | "syscall/js.stringVal": (sp) => {
313 | sp >>>= 0;
314 | storeValue(sp + 24, loadString(sp + 8));
315 | },
316 |
317 | // func valueGet(v ref, p string) ref
318 | "syscall/js.valueGet": (sp) => {
319 | sp >>>= 0;
320 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
321 | sp = this._inst.exports.getsp() >>> 0; // see comment above
322 | storeValue(sp + 32, result);
323 | },
324 |
325 | // func valueSet(v ref, p string, x ref)
326 | "syscall/js.valueSet": (sp) => {
327 | sp >>>= 0;
328 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
329 | },
330 |
331 | // func valueDelete(v ref, p string)
332 | "syscall/js.valueDelete": (sp) => {
333 | sp >>>= 0;
334 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
335 | },
336 |
337 | // func valueIndex(v ref, i int) ref
338 | "syscall/js.valueIndex": (sp) => {
339 | sp >>>= 0;
340 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
341 | },
342 |
343 | // valueSetIndex(v ref, i int, x ref)
344 | "syscall/js.valueSetIndex": (sp) => {
345 | sp >>>= 0;
346 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
347 | },
348 |
349 | // func valueCall(v ref, m string, args []ref) (ref, bool)
350 | "syscall/js.valueCall": (sp) => {
351 | sp >>>= 0;
352 | try {
353 | const v = loadValue(sp + 8);
354 | const m = Reflect.get(v, loadString(sp + 16));
355 | const args = loadSliceOfValues(sp + 32);
356 | const result = Reflect.apply(m, v, args);
357 | sp = this._inst.exports.getsp() >>> 0; // see comment above
358 | storeValue(sp + 56, result);
359 | this.mem.setUint8(sp + 64, 1);
360 | } catch (err) {
361 | sp = this._inst.exports.getsp() >>> 0; // see comment above
362 | storeValue(sp + 56, err);
363 | this.mem.setUint8(sp + 64, 0);
364 | }
365 | },
366 |
367 | // func valueInvoke(v ref, args []ref) (ref, bool)
368 | "syscall/js.valueInvoke": (sp) => {
369 | sp >>>= 0;
370 | try {
371 | const v = loadValue(sp + 8);
372 | const args = loadSliceOfValues(sp + 16);
373 | const result = Reflect.apply(v, undefined, args);
374 | sp = this._inst.exports.getsp() >>> 0; // see comment above
375 | storeValue(sp + 40, result);
376 | this.mem.setUint8(sp + 48, 1);
377 | } catch (err) {
378 | sp = this._inst.exports.getsp() >>> 0; // see comment above
379 | storeValue(sp + 40, err);
380 | this.mem.setUint8(sp + 48, 0);
381 | }
382 | },
383 |
384 | // func valueNew(v ref, args []ref) (ref, bool)
385 | "syscall/js.valueNew": (sp) => {
386 | sp >>>= 0;
387 | try {
388 | const v = loadValue(sp + 8);
389 | const args = loadSliceOfValues(sp + 16);
390 | const result = Reflect.construct(v, args);
391 | sp = this._inst.exports.getsp() >>> 0; // see comment above
392 | storeValue(sp + 40, result);
393 | this.mem.setUint8(sp + 48, 1);
394 | } catch (err) {
395 | sp = this._inst.exports.getsp() >>> 0; // see comment above
396 | storeValue(sp + 40, err);
397 | this.mem.setUint8(sp + 48, 0);
398 | }
399 | },
400 |
401 | // func valueLength(v ref) int
402 | "syscall/js.valueLength": (sp) => {
403 | sp >>>= 0;
404 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
405 | },
406 |
407 | // valuePrepareString(v ref) (ref, int)
408 | "syscall/js.valuePrepareString": (sp) => {
409 | sp >>>= 0;
410 | const str = encoder.encode(String(loadValue(sp + 8)));
411 | storeValue(sp + 16, str);
412 | setInt64(sp + 24, str.length);
413 | },
414 |
415 | // valueLoadString(v ref, b []byte)
416 | "syscall/js.valueLoadString": (sp) => {
417 | sp >>>= 0;
418 | const str = loadValue(sp + 8);
419 | loadSlice(sp + 16).set(str);
420 | },
421 |
422 | // func valueInstanceOf(v ref, t ref) bool
423 | "syscall/js.valueInstanceOf": (sp) => {
424 | sp >>>= 0;
425 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
426 | },
427 |
428 | // func copyBytesToGo(dst []byte, src ref) (int, bool)
429 | "syscall/js.copyBytesToGo": (sp) => {
430 | sp >>>= 0;
431 | const dst = loadSlice(sp + 8);
432 | const src = loadValue(sp + 32);
433 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
434 | this.mem.setUint8(sp + 48, 0);
435 | return;
436 | }
437 | const toCopy = src.subarray(0, dst.length);
438 | dst.set(toCopy);
439 | setInt64(sp + 40, toCopy.length);
440 | this.mem.setUint8(sp + 48, 1);
441 | },
442 |
443 | // func copyBytesToJS(dst ref, src []byte) (int, bool)
444 | "syscall/js.copyBytesToJS": (sp) => {
445 | sp >>>= 0;
446 | const dst = loadValue(sp + 8);
447 | const src = loadSlice(sp + 16);
448 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
449 | this.mem.setUint8(sp + 48, 0);
450 | return;
451 | }
452 | const toCopy = src.subarray(0, dst.length);
453 | dst.set(toCopy);
454 | setInt64(sp + 40, toCopy.length);
455 | this.mem.setUint8(sp + 48, 1);
456 | },
457 |
458 | "debug": (value) => {
459 | console.log(value);
460 | },
461 | }
462 | };
463 | }
464 |
465 | async run(instance) {
466 | if (!(instance instanceof WebAssembly.Instance)) {
467 | throw new Error("Go.run: WebAssembly.Instance expected");
468 | }
469 | this._inst = instance;
470 | this.mem = new DataView(this._inst.exports.mem.buffer);
471 | this._values = [ // JS values that Go currently has references to, indexed by reference id
472 | NaN,
473 | 0,
474 | null,
475 | true,
476 | false,
477 | globalThis,
478 | this,
479 | ];
480 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
481 | this._ids = new Map([ // mapping from JS values to reference ids
482 | [0, 1],
483 | [null, 2],
484 | [true, 3],
485 | [false, 4],
486 | [globalThis, 5],
487 | [this, 6],
488 | ]);
489 | this._idPool = []; // unused ids that have been garbage collected
490 | this.exited = false; // whether the Go program has exited
491 |
492 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
493 | let offset = 4096;
494 |
495 | const strPtr = (str) => {
496 | const ptr = offset;
497 | const bytes = encoder.encode(str + "\0");
498 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
499 | offset += bytes.length;
500 | if (offset % 8 !== 0) {
501 | offset += 8 - (offset % 8);
502 | }
503 | return ptr;
504 | };
505 |
506 | const argc = this.argv.length;
507 |
508 | const argvPtrs = [];
509 | this.argv.forEach((arg) => {
510 | argvPtrs.push(strPtr(arg));
511 | });
512 | argvPtrs.push(0);
513 |
514 | const keys = Object.keys(this.env).sort();
515 | keys.forEach((key) => {
516 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
517 | });
518 | argvPtrs.push(0);
519 |
520 | const argv = offset;
521 | argvPtrs.forEach((ptr) => {
522 | this.mem.setUint32(offset, ptr, true);
523 | this.mem.setUint32(offset + 4, 0, true);
524 | offset += 8;
525 | });
526 |
527 | // The linker guarantees global data starts from at least wasmMinDataAddr.
528 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
529 | const wasmMinDataAddr = 4096 + 8192;
530 | if (offset >= wasmMinDataAddr) {
531 | throw new Error("total length of command line and environment variables exceeds limit");
532 | }
533 |
534 | this._inst.exports.run(argc, argv);
535 | if (this.exited) {
536 | this._resolveExitPromise();
537 | }
538 | await this._exitPromise;
539 | }
540 |
541 | _resume() {
542 | if (this.exited) {
543 | throw new Error("Go program has already exited");
544 | }
545 | this._inst.exports.resume();
546 | if (this.exited) {
547 | this._resolveExitPromise();
548 | }
549 | }
550 |
551 | _makeFuncWrapper(id) {
552 | const go = this;
553 | return function () {
554 | const event = { id: id, this: this, args: arguments };
555 | go._pendingEvent = event;
556 | go._resume();
557 | return event.result;
558 | };
559 | }
560 | }
561 | })();
562 |
--------------------------------------------------------------------------------
/cmd/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | func main() {
9 | err := http.ListenAndServe(":9090", http.FileServer(http.Dir("../../assets")))
10 | if err != nil {
11 | fmt.Println("Failed to start server", err)
12 | return
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cmd/wasm/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "syscall/js"
7 | )
8 |
9 | func jsonWrapper() js.Func {
10 | jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
11 | if len(args) != 1 {
12 | return "Invalid no of arguments passed"
13 | }
14 | inputJSON := args[0].String()
15 | fmt.Printf("input %s\n", inputJSON)
16 | pretty, err := prettyJson(inputJSON)
17 | if err != nil {
18 | fmt.Printf("unable to convert to json %s\n", err)
19 | return err.Error()
20 | }
21 | return pretty
22 | })
23 | return jsonFunc
24 | }
25 |
26 | func prettyJson(input string) (string, error) {
27 | var raw any
28 | if err := json.Unmarshal([]byte(input), &raw); err != nil {
29 | return "", err
30 | }
31 | pretty, err := json.MarshalIndent(raw, "", " ")
32 | if err != nil {
33 | return "", err
34 | }
35 | return string(pretty), nil
36 | }
37 |
38 | func main() {
39 | fmt.Println("Go Web Assembly")
40 | js.Global().Set("formatJSON", jsonWrapper())
41 | <-make(chan struct{})
42 | }
43 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/golangbot/webassembly
2 |
3 | go 1.19
4 |
--------------------------------------------------------------------------------