├── .air.toml ├── .github └── demo.mov ├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── index.html ├── main.go └── wasm_exec.js /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "." 8 | cmd = "GOOS=js GOARCH=wasm go build -o main.wasm" 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | time = false 40 | 41 | [misc] 42 | clean_on_exit = false 43 | 44 | [screen] 45 | clear_on_rebuild = false 46 | keep_scroll = true 47 | -------------------------------------------------------------------------------- /.github/demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semanser/tinygo-wasm-webgl-demo/d8f5602fb33ae0035a9d2722f4471e28c3704fda/.github/demo.mov -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | *.wasm 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tinygo-wasm-webgl-demo 2 | 3 | This repository contains a modified gowebapi/webapi [demo](https://github.com/gowebapi/webapi/blob/41cedfc27a0bd35c1220dd0fe4b4c4505c33b0ea/graphics/webgl/example_cube_test.go) that works with tinygo. 4 | 5 | https://github.com/semanser/tinygo-wasm-webgl-demo/assets/4020045/2047caee-491a-4db6-bb49-8e24ec42bb43 6 | 7 | # Prerequisites 8 | - Install [tinygo](https://tinygo.org/) 9 | - Install [http-server](https://github.com/http-party/http-server) (or any other static web server of your choice). This is required to host the wasm file. 10 | 11 | # Compilation 12 | ```bash 13 | $ tinygo build -o=main.wasm -target=wasm -no-debug ./main.go 14 | 15 | 184K main.wasm 16 | ``` 17 | 18 | # Running 19 | ```bash 20 | $ http-server . 21 | ``` 22 | 23 | # Resources 24 | - https://tinygo.org/ 25 | - https://github.com/gowebapi/webapi 26 | - https://github.com/http-party/http-server 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/semanser/tinygo-wasm-webgl-demo 2 | 3 | go 1.21 4 | 5 | require github.com/gowebapi/webapi v0.0.0-20221221115732-41cedfc27a0b 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gowebapi/webapi v0.0.0-20221221115732-41cedfc27a0b h1:ziwlwRTFt5kSst3238Ndwce+wHZh3BC05nxBThB08XE= 2 | github.com/gowebapi/webapi v0.0.0-20221221115732-41cedfc27a0b/go.mod h1:idYMKBl+9tqA6sZrzVqN+3XGWANtIRP6CLZsxZOiIFg= 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | go webassembly - bouncy 4 | 5 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/gowebapi/webapi" 9 | "github.com/gowebapi/webapi/core/js" 10 | "github.com/gowebapi/webapi/core/jsconv" 11 | "github.com/gowebapi/webapi/graphics/webgl" 12 | "github.com/gowebapi/webapi/html/canvas" 13 | ) 14 | 15 | //see https://github.com/golang/go/wiki/WebAssembly 16 | //see https://github.com/bobcob7/wasm-basic-triangle 17 | 18 | var gl *webgl.RenderingContext 19 | var vBuffer, iBuffer *webgl.Buffer 20 | var icount int 21 | var prog *webgl.Program 22 | var angle float32 23 | var width, height int 24 | 25 | func main() { 26 | c := make(chan struct{}, 0) 27 | fmt.Println("Go/WASM loaded") 28 | 29 | addCanvas() 30 | 31 | <-c 32 | } 33 | 34 | func addCanvas() { 35 | doc := webapi.GetWindow().Document() 36 | app := doc.GetElementById("app") 37 | body := doc.GetElementById("body") 38 | width := body.ClientWidth() 39 | height := body.ClientHeight() 40 | 41 | canvasE := webapi.GetWindow().Document().CreateElement("canvas", &webapi.Union{js.ValueOf("dom.Node")}) 42 | canvasE.SetId("canvas") 43 | app.AppendChild(&canvasE.Node) 44 | canvasHTML := canvas.HTMLCanvasElementFromWrapper(canvasE) 45 | canvasHTML.SetWidth(uint(width)) 46 | canvasHTML.SetHeight(uint(height)) 47 | 48 | contextU := canvasHTML.GetContext("webgl", nil) 49 | gl = webgl.RenderingContextFromWrapper(contextU) 50 | 51 | vBuffer, iBuffer, icount = createBuffers(gl) 52 | 53 | prog = setupShaders(gl) 54 | 55 | // Start the animation loop 56 | js.Global().Call("requestAnimationFrame", js.FuncOf(drawScene)) 57 | } 58 | 59 | func drawScene(this js.Value, p []js.Value) interface{} { 60 | // Start a timer 61 | startTime := time.Now() 62 | 63 | angle += 0.01 // Update the angle for rotation 64 | 65 | gl.ClearColor(0.5, 0.5, 0.5, 0.9) 66 | gl.Clear(webgl.COLOR_BUFFER_BIT) 67 | 68 | // Enable the depth test 69 | gl.Enable(webgl.DEPTH_TEST) 70 | 71 | // Set the view port 72 | gl.Viewport(0, 0, 800, 800) 73 | 74 | // Update the model-view matrix for rotation 75 | rotationMatrix := getRotationMatrix(angle) 76 | coord := gl.GetAttribLocation(prog, "coordinates") 77 | 78 | // Bind vertex buffer object 79 | gl.BindBuffer(webgl.ARRAY_BUFFER, vBuffer) 80 | 81 | // Bind index buffer object 82 | gl.BindBuffer(webgl.ELEMENT_ARRAY_BUFFER, iBuffer) 83 | 84 | // Point an attribute to the currently bound VBO 85 | gl.VertexAttribPointer(uint(coord), 3, webgl.FLOAT, false, 0, 0) 86 | 87 | // Enable the attribute 88 | gl.EnableVertexAttribArray(uint(coord)) 89 | 90 | // Set the model-view matrix in the vertex shader 91 | modelviewLoc := gl.GetUniformLocation(prog, "modelview") 92 | gl.UniformMatrix4fv(modelviewLoc, false, rotationMatrix) 93 | 94 | // Draw the triangle 95 | gl.DrawElements(webgl.TRIANGLES, icount, webgl.UNSIGNED_SHORT, 0) 96 | 97 | // Stop the timer and calculate the FPS 98 | endTime := time.Now() 99 | elapsedTime := endTime.Sub(startTime) 100 | fps := float64(1.0 / elapsedTime.Seconds()) 101 | 102 | // Display the FPS 103 | fpsDisplay := fmt.Sprintf("FPS: %.2f", fps) 104 | doc := webapi.GetWindow().Document() 105 | fpsElem := doc.GetElementById("fps") 106 | fpsElem.SetTextContent(&fpsDisplay) 107 | 108 | // Request the next animation frame 109 | js.Global().Call("requestAnimationFrame", js.FuncOf(drawScene)) 110 | return nil 111 | } 112 | 113 | func createBuffers(gl *webgl.RenderingContext) (*webgl.Buffer, *webgl.Buffer, int) { 114 | //// VERTEX BUFFER //// 115 | var verticesNative = []float32{ 116 | -0.5, 0.5, 0, 117 | -0.5, -0.5, 0, 118 | 0.5, -0.5, 0, 119 | } 120 | var vertices = jsconv.Float32ToJs(verticesNative) 121 | // Create buffer 122 | vBuffer := gl.CreateBuffer() 123 | // Bind to buffer 124 | gl.BindBuffer(webgl.ARRAY_BUFFER, vBuffer) 125 | // Pass data to buffer 126 | gl.BufferData2(webgl.ARRAY_BUFFER, webgl.UnionFromJS(vertices), webgl.STATIC_DRAW) 127 | // Unbind buffer 128 | gl.BindBuffer(webgl.ARRAY_BUFFER, &webgl.Buffer{}) 129 | 130 | // INDEX BUFFER //// 131 | var indicesNative = []uint32{ 132 | 2, 1, 0, 133 | } 134 | var indices = jsconv.UInt32ToJs(indicesNative) 135 | 136 | // Create buffer 137 | iBuffer := gl.CreateBuffer() 138 | 139 | // Bind to buffer 140 | gl.BindBuffer(webgl.ELEMENT_ARRAY_BUFFER, iBuffer) 141 | 142 | // Pass data to buffer 143 | gl.BufferData2(webgl.ELEMENT_ARRAY_BUFFER, webgl.UnionFromJS(indices), webgl.STATIC_DRAW) 144 | 145 | // Unbind buffer 146 | gl.BindBuffer(webgl.ELEMENT_ARRAY_BUFFER, &webgl.Buffer{}) 147 | return vBuffer, iBuffer, len(indicesNative) 148 | } 149 | 150 | func setupShaders(gl *webgl.RenderingContext) *webgl.Program { 151 | // Vertex shader source code 152 | vertCode := ` 153 | attribute vec3 coordinates; 154 | uniform mat4 modelview; 155 | 156 | void main(void) { 157 | gl_Position = modelview * vec4(coordinates, 1.0); 158 | } 159 | ` 160 | 161 | // Create a vertex shader object 162 | vShader := gl.CreateShader(webgl.VERTEX_SHADER) 163 | 164 | // Attach vertex shader source code 165 | gl.ShaderSource(vShader, vertCode) 166 | 167 | // Compile the vertex shader 168 | gl.CompileShader(vShader) 169 | 170 | //fragment shader source code 171 | fragCode := ` 172 | void main(void) { 173 | gl_FragColor = vec4(0.0, 1.0, 0.0, 0.7); 174 | }` 175 | 176 | // Create fragment shader object 177 | fShader := gl.CreateShader(webgl.FRAGMENT_SHADER) 178 | 179 | // Attach fragment shader source code 180 | gl.ShaderSource(fShader, fragCode) 181 | 182 | // Compile the fragmentt shader 183 | gl.CompileShader(fShader) 184 | 185 | // Create a shader program object to store 186 | // the combined shader program 187 | prog := gl.CreateProgram() 188 | 189 | // Attach a vertex shader 190 | gl.AttachShader(prog, vShader) 191 | 192 | // Attach a fragment shader 193 | gl.AttachShader(prog, fShader) 194 | 195 | // Link both the programs 196 | gl.LinkProgram(prog) 197 | 198 | // Use the combined shader program object 199 | gl.UseProgram(prog) 200 | 201 | // Get the location of the model-view matrix in the vertex shader 202 | modelviewLoc := gl.GetUniformLocation(prog, "modelview") 203 | 204 | // Set the initial model-view matrix 205 | rotationMatrix := getRotationMatrix(angle) 206 | gl.UniformMatrix4fv(modelviewLoc, false, rotationMatrix) 207 | 208 | return prog 209 | } 210 | 211 | func getRotationMatrix(angle float32) *webgl.Union { 212 | // Create a 2D rotation matrix for the given angle 213 | cosA := float32(math.Cos(float64(angle))) 214 | sinA := float32(math.Sin(float64(angle))) 215 | 216 | return webgl.UnionFromJS(jsconv.Float32ToJs([]float32{ 217 | cosA, -sinA, 0, 0, 218 | sinA, cosA, 0, 0, 219 | 0, 0, 1, 0, 220 | 0, 0, 0, 1, 221 | })) 222 | } 223 | -------------------------------------------------------------------------------- /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 | // This file has been modified for use by the TinyGo compiler. 6 | 7 | (() => { 8 | // Map multiple JavaScript environments to a single common API, 9 | // preferring web standards over Node.js API. 10 | // 11 | // Environments considered: 12 | // - Browsers 13 | // - Node.js 14 | // - Electron 15 | // - Parcel 16 | 17 | if (typeof global !== "undefined") { 18 | // global already exists 19 | } else if (typeof window !== "undefined") { 20 | window.global = window; 21 | } else if (typeof self !== "undefined") { 22 | self.global = self; 23 | } else { 24 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 25 | } 26 | 27 | if (!global.require && typeof require !== "undefined") { 28 | global.require = require; 29 | } 30 | 31 | if (!global.fs && global.require) { 32 | global.fs = require("fs"); 33 | } 34 | 35 | const enosys = () => { 36 | const err = new Error("not implemented"); 37 | err.code = "ENOSYS"; 38 | return err; 39 | }; 40 | 41 | if (!global.fs) { 42 | let outputBuf = ""; 43 | global.fs = { 44 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 45 | writeSync(fd, buf) { 46 | outputBuf += decoder.decode(buf); 47 | const nl = outputBuf.lastIndexOf("\n"); 48 | if (nl != -1) { 49 | console.log(outputBuf.substr(0, nl)); 50 | outputBuf = outputBuf.substr(nl + 1); 51 | } 52 | return buf.length; 53 | }, 54 | write(fd, buf, offset, length, position, callback) { 55 | if (offset !== 0 || length !== buf.length || position !== null) { 56 | callback(enosys()); 57 | return; 58 | } 59 | const n = this.writeSync(fd, buf); 60 | callback(null, n); 61 | }, 62 | chmod(path, mode, callback) { callback(enosys()); }, 63 | chown(path, uid, gid, callback) { callback(enosys()); }, 64 | close(fd, callback) { callback(enosys()); }, 65 | fchmod(fd, mode, callback) { callback(enosys()); }, 66 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 67 | fstat(fd, callback) { callback(enosys()); }, 68 | fsync(fd, callback) { callback(null); }, 69 | ftruncate(fd, length, callback) { callback(enosys()); }, 70 | lchown(path, uid, gid, callback) { callback(enosys()); }, 71 | link(path, link, callback) { callback(enosys()); }, 72 | lstat(path, callback) { callback(enosys()); }, 73 | mkdir(path, perm, callback) { callback(enosys()); }, 74 | open(path, flags, mode, callback) { callback(enosys()); }, 75 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 76 | readdir(path, callback) { callback(enosys()); }, 77 | readlink(path, callback) { callback(enosys()); }, 78 | rename(from, to, callback) { callback(enosys()); }, 79 | rmdir(path, callback) { callback(enosys()); }, 80 | stat(path, callback) { callback(enosys()); }, 81 | symlink(path, link, callback) { callback(enosys()); }, 82 | truncate(path, length, callback) { callback(enosys()); }, 83 | unlink(path, callback) { callback(enosys()); }, 84 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 85 | }; 86 | } 87 | 88 | if (!global.process) { 89 | global.process = { 90 | getuid() { return -1; }, 91 | getgid() { return -1; }, 92 | geteuid() { return -1; }, 93 | getegid() { return -1; }, 94 | getgroups() { throw enosys(); }, 95 | pid: -1, 96 | ppid: -1, 97 | umask() { throw enosys(); }, 98 | cwd() { throw enosys(); }, 99 | chdir() { throw enosys(); }, 100 | } 101 | } 102 | 103 | if (!global.crypto) { 104 | const nodeCrypto = require("crypto"); 105 | global.crypto = { 106 | getRandomValues(b) { 107 | nodeCrypto.randomFillSync(b); 108 | }, 109 | }; 110 | } 111 | 112 | if (!global.performance) { 113 | global.performance = { 114 | now() { 115 | const [sec, nsec] = process.hrtime(); 116 | return sec * 1000 + nsec / 1000000; 117 | }, 118 | }; 119 | } 120 | 121 | if (!global.TextEncoder) { 122 | global.TextEncoder = require("util").TextEncoder; 123 | } 124 | 125 | if (!global.TextDecoder) { 126 | global.TextDecoder = require("util").TextDecoder; 127 | } 128 | 129 | // End of polyfills for common API. 130 | 131 | const encoder = new TextEncoder("utf-8"); 132 | const decoder = new TextDecoder("utf-8"); 133 | let reinterpretBuf = new DataView(new ArrayBuffer(8)); 134 | var logLine = []; 135 | 136 | global.Go = class { 137 | constructor() { 138 | this._callbackTimeouts = new Map(); 139 | this._nextCallbackTimeoutID = 1; 140 | 141 | const mem = () => { 142 | // The buffer may change when requesting more memory. 143 | return new DataView(this._inst.exports.memory.buffer); 144 | } 145 | 146 | const unboxValue = (v_ref) => { 147 | reinterpretBuf.setBigInt64(0, v_ref, true); 148 | const f = reinterpretBuf.getFloat64(0, true); 149 | if (f === 0) { 150 | return undefined; 151 | } 152 | if (!isNaN(f)) { 153 | return f; 154 | } 155 | 156 | const id = v_ref & 0xffffffffn; 157 | return this._values[id]; 158 | } 159 | 160 | 161 | const loadValue = (addr) => { 162 | let v_ref = mem().getBigUint64(addr, true); 163 | return unboxValue(v_ref); 164 | } 165 | 166 | const boxValue = (v) => { 167 | const nanHead = 0x7FF80000n; 168 | 169 | if (typeof v === "number") { 170 | if (isNaN(v)) { 171 | return nanHead << 32n; 172 | } 173 | if (v === 0) { 174 | return (nanHead << 32n) | 1n; 175 | } 176 | reinterpretBuf.setFloat64(0, v, true); 177 | return reinterpretBuf.getBigInt64(0, true); 178 | } 179 | 180 | switch (v) { 181 | case undefined: 182 | return 0n; 183 | case null: 184 | return (nanHead << 32n) | 2n; 185 | case true: 186 | return (nanHead << 32n) | 3n; 187 | case false: 188 | return (nanHead << 32n) | 4n; 189 | } 190 | 191 | let id = this._ids.get(v); 192 | if (id === undefined) { 193 | id = this._idPool.pop(); 194 | if (id === undefined) { 195 | id = BigInt(this._values.length); 196 | } 197 | this._values[id] = v; 198 | this._goRefCounts[id] = 0; 199 | this._ids.set(v, id); 200 | } 201 | this._goRefCounts[id]++; 202 | let typeFlag = 1n; 203 | switch (typeof v) { 204 | case "string": 205 | typeFlag = 2n; 206 | break; 207 | case "symbol": 208 | typeFlag = 3n; 209 | break; 210 | case "function": 211 | typeFlag = 4n; 212 | break; 213 | } 214 | return id | ((nanHead | typeFlag) << 32n); 215 | } 216 | 217 | const storeValue = (addr, v) => { 218 | let v_ref = boxValue(v); 219 | mem().setBigUint64(addr, v_ref, true); 220 | } 221 | 222 | const loadSlice = (array, len, cap) => { 223 | return new Uint8Array(this._inst.exports.memory.buffer, array, len); 224 | } 225 | 226 | const loadSliceOfValues = (array, len, cap) => { 227 | const a = new Array(len); 228 | for (let i = 0; i < len; i++) { 229 | a[i] = loadValue(array + i * 8); 230 | } 231 | return a; 232 | } 233 | 234 | const loadString = (ptr, len) => { 235 | return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); 236 | } 237 | 238 | const timeOrigin = Date.now() - performance.now(); 239 | this.importObject = { 240 | wasi_snapshot_preview1: { 241 | // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write 242 | fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { 243 | let nwritten = 0; 244 | if (fd == 1) { 245 | for (let iovs_i=0; iovs_i 0, // dummy 271 | fd_fdstat_get: () => 0, // dummy 272 | fd_seek: () => 0, // dummy 273 | "proc_exit": (code) => { 274 | if (global.process) { 275 | // Node.js 276 | process.exit(code); 277 | } else { 278 | // Can't exit in a browser. 279 | throw 'trying to exit with code ' + code; 280 | } 281 | }, 282 | random_get: (bufPtr, bufLen) => { 283 | crypto.getRandomValues(loadSlice(bufPtr, bufLen)); 284 | return 0; 285 | }, 286 | }, 287 | gojs: { 288 | // func ticks() float64 289 | "runtime.ticks": () => { 290 | return timeOrigin + performance.now(); 291 | }, 292 | 293 | // func sleepTicks(timeout float64) 294 | "runtime.sleepTicks": (timeout) => { 295 | // Do not sleep, only reactivate scheduler after the given timeout. 296 | setTimeout(this._inst.exports.go_scheduler, timeout); 297 | }, 298 | 299 | // func finalizeRef(v ref) 300 | "syscall/js.finalizeRef": (v_ref) => { 301 | // Note: TinyGo does not support finalizers so this should never be 302 | // called. 303 | console.error('syscall/js.finalizeRef not implemented'); 304 | }, 305 | 306 | // func stringVal(value string) ref 307 | "syscall/js.stringVal": (value_ptr, value_len) => { 308 | const s = loadString(value_ptr, value_len); 309 | return boxValue(s); 310 | }, 311 | 312 | // func valueGet(v ref, p string) ref 313 | "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { 314 | let prop = loadString(p_ptr, p_len); 315 | let v = unboxValue(v_ref); 316 | let result = Reflect.get(v, prop); 317 | return boxValue(result); 318 | }, 319 | 320 | // func valueSet(v ref, p string, x ref) 321 | "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { 322 | const v = unboxValue(v_ref); 323 | const p = loadString(p_ptr, p_len); 324 | const x = unboxValue(x_ref); 325 | Reflect.set(v, p, x); 326 | }, 327 | 328 | // func valueDelete(v ref, p string) 329 | "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { 330 | const v = unboxValue(v_ref); 331 | const p = loadString(p_ptr, p_len); 332 | Reflect.deleteProperty(v, p); 333 | }, 334 | 335 | // func valueIndex(v ref, i int) ref 336 | "syscall/js.valueIndex": (v_ref, i) => { 337 | return boxValue(Reflect.get(unboxValue(v_ref), i)); 338 | }, 339 | 340 | // valueSetIndex(v ref, i int, x ref) 341 | "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { 342 | Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); 343 | }, 344 | 345 | // func valueCall(v ref, m string, args []ref) (ref, bool) 346 | "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { 347 | const v = unboxValue(v_ref); 348 | const name = loadString(m_ptr, m_len); 349 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 350 | try { 351 | const m = Reflect.get(v, name); 352 | storeValue(ret_addr, Reflect.apply(m, v, args)); 353 | mem().setUint8(ret_addr + 8, 1); 354 | } catch (err) { 355 | storeValue(ret_addr, err); 356 | mem().setUint8(ret_addr + 8, 0); 357 | } 358 | }, 359 | 360 | // func valueInvoke(v ref, args []ref) (ref, bool) 361 | "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { 362 | try { 363 | const v = unboxValue(v_ref); 364 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 365 | storeValue(ret_addr, Reflect.apply(v, undefined, args)); 366 | mem().setUint8(ret_addr + 8, 1); 367 | } catch (err) { 368 | storeValue(ret_addr, err); 369 | mem().setUint8(ret_addr + 8, 0); 370 | } 371 | }, 372 | 373 | // func valueNew(v ref, args []ref) (ref, bool) 374 | "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { 375 | const v = unboxValue(v_ref); 376 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 377 | try { 378 | storeValue(ret_addr, Reflect.construct(v, args)); 379 | mem().setUint8(ret_addr + 8, 1); 380 | } catch (err) { 381 | storeValue(ret_addr, err); 382 | mem().setUint8(ret_addr+ 8, 0); 383 | } 384 | }, 385 | 386 | // func valueLength(v ref) int 387 | "syscall/js.valueLength": (v_ref) => { 388 | return unboxValue(v_ref).length; 389 | }, 390 | 391 | // valuePrepareString(v ref) (ref, int) 392 | "syscall/js.valuePrepareString": (ret_addr, v_ref) => { 393 | const s = String(unboxValue(v_ref)); 394 | const str = encoder.encode(s); 395 | storeValue(ret_addr, str); 396 | mem().setInt32(ret_addr + 8, str.length, true); 397 | }, 398 | 399 | // valueLoadString(v ref, b []byte) 400 | "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { 401 | const str = unboxValue(v_ref); 402 | loadSlice(slice_ptr, slice_len, slice_cap).set(str); 403 | }, 404 | 405 | // func valueInstanceOf(v ref, t ref) bool 406 | "syscall/js.valueInstanceOf": (v_ref, t_ref) => { 407 | return unboxValue(v_ref) instanceof unboxValue(t_ref); 408 | }, 409 | 410 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 411 | "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { 412 | let num_bytes_copied_addr = ret_addr; 413 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 414 | 415 | const dst = loadSlice(dest_addr, dest_len); 416 | const src = unboxValue(src_ref); 417 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 418 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 419 | return; 420 | } 421 | const toCopy = src.subarray(0, dst.length); 422 | dst.set(toCopy); 423 | mem().setUint32(num_bytes_copied_addr, toCopy.length, true); 424 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 425 | }, 426 | 427 | // copyBytesToJS(dst ref, src []byte) (int, bool) 428 | // Originally copied from upstream Go project, then modified: 429 | // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 430 | "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { 431 | let num_bytes_copied_addr = ret_addr; 432 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 433 | 434 | const dst = unboxValue(dst_ref); 435 | const src = loadSlice(src_addr, src_len); 436 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 437 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 438 | return; 439 | } 440 | const toCopy = src.subarray(0, dst.length); 441 | dst.set(toCopy); 442 | mem().setUint32(num_bytes_copied_addr, toCopy.length, true); 443 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 444 | }, 445 | } 446 | }; 447 | 448 | // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. 449 | // For compatibility, we use both as long as Go 1.20 is supported. 450 | this.importObject.env = this.importObject.gojs; 451 | } 452 | 453 | async run(instance) { 454 | this._inst = instance; 455 | this._values = [ // JS values that Go currently has references to, indexed by reference id 456 | NaN, 457 | 0, 458 | null, 459 | true, 460 | false, 461 | global, 462 | this, 463 | ]; 464 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 465 | this._ids = new Map(); // mapping from JS values to reference ids 466 | this._idPool = []; // unused ids that have been garbage collected 467 | this.exited = false; // whether the Go program has exited 468 | 469 | const mem = new DataView(this._inst.exports.memory.buffer) 470 | 471 | while (true) { 472 | const callbackPromise = new Promise((resolve) => { 473 | this._resolveCallbackPromise = () => { 474 | if (this.exited) { 475 | throw new Error("bad callback: Go program has already exited"); 476 | } 477 | setTimeout(resolve, 0); // make sure it is asynchronous 478 | }; 479 | }); 480 | this._inst.exports._start(); 481 | if (this.exited) { 482 | break; 483 | } 484 | await callbackPromise; 485 | } 486 | } 487 | 488 | _resume() { 489 | if (this.exited) { 490 | throw new Error("Go program has already exited"); 491 | } 492 | this._inst.exports.resume(); 493 | if (this.exited) { 494 | this._resolveExitPromise(); 495 | } 496 | } 497 | 498 | _makeFuncWrapper(id) { 499 | const go = this; 500 | return function () { 501 | const event = { id: id, this: this, args: arguments }; 502 | go._pendingEvent = event; 503 | go._resume(); 504 | return event.result; 505 | }; 506 | } 507 | } 508 | 509 | if ( 510 | global.require && 511 | global.require.main === module && 512 | global.process && 513 | global.process.versions && 514 | !global.process.versions.electron 515 | ) { 516 | if (process.argv.length != 3) { 517 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 518 | process.exit(1); 519 | } 520 | 521 | const go = new Go(); 522 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 523 | return go.run(result.instance); 524 | }).catch((err) => { 525 | console.error(err); 526 | process.exit(1); 527 | }); 528 | } 529 | })(); 530 | --------------------------------------------------------------------------------