├── 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 | Buy Me A Coffee 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 | --------------------------------------------------------------------------------