├── .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 |
--------------------------------------------------------------------------------