├── .gitignore ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── pkg └── socket │ └── manager.go ├── public ├── index.html └── wasm_exec.js └── wasm └── wasm.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | *.wasm 24 | tmp/ 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build buildwasm run clean serve 2 | 3 | # Variables 4 | WASM_DIR := ./wasm 5 | PUBLIC_DIR := ./public 6 | SERVER_FILE := main.go 7 | WASM_SOURCE := $(WASM_DIR)/wasm.go 8 | WASM_TARGET := $(PUBLIC_DIR)/main.wasm 9 | GOOS := js 10 | GOARCH := wasm 11 | 12 | # Default rule 13 | all: run 14 | 15 | # Run the server 16 | run: serve 17 | 18 | # Serve the project 19 | serve: buildwasm 20 | go run $(SERVER_FILE) 21 | 22 | # Build the WebAssembly module 23 | buildwasm: 24 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(WASM_TARGET) $(WASM_SOURCE) 25 | 26 | # Clean the built artifacts 27 | clean: 28 | rm -f $(WASM_TARGET) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go WebSocket WebAssembly Example (WIP) 2 | 3 | This project demonstrates how to set up a WebAssembly (WASM) module using Go, with a focus on establishing a WebSocket connection and interacting with the DOM from Go. 4 | 5 | ## Introduction 6 | 7 | WebAssembly allows you to run code written in multiple languages on the web at near-native speed, with client-side execution. This project specifically showcases the use of Go to handle WebSocket communications and update the DOM accordingly. 8 | 9 | | Related Blog Post : https://arifdogan.medium.com/creating-real-time-websockets-with-go-and-webassembly-c4039ff2d0ec 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/doganarif/go-wasm-socket 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.5.1 7 | nhooyr.io/websocket v1.8.10 8 | ) 9 | 10 | require golang.org/x/net v0.17.0 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 2 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 3 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 4 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 5 | nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= 6 | nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/doganarif/go-wasm-socket/pkg/socket" 8 | ) 9 | 10 | func main() { 11 | setupAPI() 12 | 13 | // Serve on port :8080, fudge yeah hardcoded port 14 | log.Fatal(http.ListenAndServe(":8080", nil)) 15 | } 16 | 17 | // setupAPI will start all Routes and their Handlers 18 | func setupAPI() { 19 | manager := socket.NewManager() 20 | 21 | // Serve the ./frontend directory at Route / 22 | http.Handle("/", http.FileServer(http.Dir("./public"))) 23 | http.Handle("/ws", http.HandlerFunc(manager.ServeWS)) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/socket/manager.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | // Pre-configure the upgrader, which is responsible for upgrading 13 | // an HTTP connection to a WebSocket connection. 14 | var ( 15 | websocketUpgrader = websocket.Upgrader{ 16 | ReadBufferSize: 1024, 17 | WriteBufferSize: 1024, 18 | } 19 | ) 20 | 21 | // NotifyEvent represents an event that contains a reference 22 | // to the client who initiated the event and the message to be notified. 23 | type NotifyEvent struct { 24 | client *Client 25 | message string 26 | } 27 | 28 | // Client represents a single WebSocket connection. 29 | // It holds the client's ID, the WebSocket connection itself, and 30 | // the manager that controls all clients. 31 | type Client struct { 32 | id uint32 33 | connection *websocket.Conn 34 | manager *Manager 35 | 36 | writeChan chan string 37 | } 38 | 39 | // Manager keeps track of all active clients and broadcasts messages. 40 | type Manager struct { 41 | clients ClientList 42 | 43 | sync.RWMutex 44 | 45 | notifyChan chan NotifyEvent 46 | } 47 | 48 | // ClientList is a map of clients to keep track of their presence. 49 | type ClientList map[*Client]bool 50 | 51 | // NewClient creates a new Client instance with a unique ID, its connection, 52 | // and a reference to the Manager. 53 | func NewClient(conn *websocket.Conn, manager *Manager) *Client { 54 | return &Client{ 55 | id: rand.Uint32(), 56 | connection: conn, 57 | manager: manager, 58 | writeChan: make(chan string), 59 | } 60 | } 61 | 62 | // readMessages continuously reads messages from the WebSocket connection. 63 | // It will send any received messages to the manager's notification channel. 64 | func (c *Client) readMessages() { 65 | defer func() { 66 | c.manager.removeClient(c) 67 | }() 68 | 69 | for { 70 | messageType, payload, err := c.connection.ReadMessage() 71 | 72 | c.manager.notifyChan <- NotifyEvent{client: c, message: string(payload)} 73 | 74 | if err != nil { 75 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 76 | log.Printf("error reading message: %v", err) 77 | } 78 | break 79 | } 80 | log.Println("MessageType: ", messageType) 81 | log.Println("Payload: ", string(payload)) 82 | } 83 | } 84 | 85 | // writeMessages listens on the client's write channel for messages 86 | // and writes any received messages to the WebSocket connection. 87 | func (c *Client) writeMessages() { 88 | defer func() { 89 | c.manager.removeClient(c) 90 | }() 91 | 92 | for { 93 | select { 94 | case data := <-c.writeChan: 95 | c.connection.WriteMessage(websocket.TextMessage, []byte(data)) 96 | } 97 | } 98 | } 99 | 100 | // NewManager creates a new Manager instance, initializes the client list, 101 | // and starts the goroutine responsible for notifying other clients. 102 | func NewManager() *Manager { 103 | m := &Manager{ 104 | clients: make(ClientList), 105 | notifyChan: make(chan NotifyEvent), 106 | } 107 | 108 | go m.notifyOtherClients() 109 | 110 | return m 111 | } 112 | 113 | // otherClients returns a slice of clients excluding the provided client. 114 | func (m *Manager) otherClients(client *Client) []*Client { 115 | clientList := make([]*Client, 0) 116 | 117 | for c := range m.clients { 118 | if c.id != client.id { 119 | clientList = append(clientList, c) 120 | } 121 | } 122 | 123 | return clientList 124 | } 125 | 126 | // notifyOtherClients waits for notify events and broadcasts the message 127 | // to all clients except the one who sent the message. 128 | func (m *Manager) notifyOtherClients() { 129 | for { 130 | select { 131 | case e := <-m.notifyChan: 132 | otherClients := m.otherClients(e.client) 133 | 134 | for _, c := range otherClients { 135 | c.writeChan <- e.message 136 | } 137 | } 138 | } 139 | } 140 | 141 | // addClient adds a new client to the manager's client list. 142 | func (m *Manager) addClient(client *Client) { 143 | m.Lock() 144 | defer m.Unlock() 145 | 146 | m.clients[client] = true 147 | } 148 | 149 | // removeClient removes a client from the manager's client list and 150 | // closes the WebSocket connection. 151 | func (m *Manager) removeClient(client *Client) { 152 | m.Lock() 153 | defer m.Unlock() 154 | 155 | if _, ok := m.clients[client]; ok { 156 | client.connection.Close() 157 | delete(m.clients, client) 158 | } 159 | } 160 | 161 | // ServeWS is an HTTP handler that upgrades the HTTP connection to a 162 | // WebSocket connection and registers the new client with the manager. 163 | func (m *Manager) ServeWS(w http.ResponseWriter, r *http.Request) { 164 | log.Println("New Connection") 165 | 166 | conn, err := websocketUpgrader.Upgrade(w, r, nil) 167 | if err != nil { 168 | log.Println(err) 169 | return 170 | } 171 | 172 | client := NewClient(conn, m) 173 | m.addClient(client) 174 | 175 | go client.readMessages() 176 | go client.writeMessages() 177 | } 178 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go WASM Button Example 7 | 8 | 16 | 17 | 18 | 19 |

0

20 | 21 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/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 | -------------------------------------------------------------------------------- /wasm/wasm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "syscall/js" 8 | 9 | "nhooyr.io/websocket" 10 | ) 11 | 12 | // Conn wraps a WebSocket connection. 13 | type Conn struct { 14 | wsConn *websocket.Conn 15 | } 16 | 17 | // NewConn establishes a new WebSocket connection to a specified URL. 18 | func NewConn() *Conn { 19 | c, _, err := websocket.Dial(context.Background(), "ws://localhost:8080/ws", nil) 20 | if err != nil { 21 | fmt.Println(err, "ERROR") 22 | } 23 | 24 | return &Conn{ 25 | wsConn: c, 26 | } 27 | } 28 | 29 | func main() { 30 | // Channel to keep the main function running until it's closed. 31 | c := make(chan struct{}, 0) 32 | 33 | println("WASM Go Initialized") 34 | // Establish a new WebSocket connection. 35 | conn := NewConn() 36 | 37 | // Register the onButtonClick function in the global JavaScript context. 38 | js.Global().Set("onButtonClick", onButtonClickFunc(conn)) 39 | 40 | // Start reading messages in a new goroutine. 41 | go conn.readMessage() 42 | 43 | // Wait indefinitely. 44 | <-c 45 | } 46 | 47 | // onButtonClickFunc returns a js.Func that sends a "HELLO" message over WebSocket when invoked. 48 | func onButtonClickFunc(conn *Conn) js.Func { 49 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 50 | println("Button Clicked!") 51 | // Send a message through the WebSocket connection. 52 | err := conn.wsConn.Write(context.Background(), websocket.MessageText, []byte("HELLO")) 53 | if err != nil { 54 | log.Println("Error writing to WebSocket:", err) 55 | } 56 | return nil 57 | }) 58 | } 59 | 60 | // readMessage handles incoming WebSocket messages and updates the DOM accordingly. 61 | func (c *Conn) readMessage() { 62 | defer func() { 63 | // Close the WebSocket connection when the function returns. 64 | c.wsConn.Close(websocket.StatusGoingAway, "BYE") 65 | }() 66 | 67 | for { 68 | // Read a message from the WebSocket connection. 69 | messageType, payload, err := c.wsConn.Read(context.Background()) 70 | 71 | if err != nil { 72 | // Log and panic if there is an error reading the message. 73 | log.Panicf(err.Error()) 74 | } 75 | 76 | // Update the DOM with the received message. 77 | updateDOMContent(string(payload)) 78 | 79 | // Log the message type and payload for debugging. 80 | log.Println("MessageType: ", messageType) 81 | log.Println("Payload: ", string(payload)) 82 | } 83 | } 84 | 85 | // updateDOMContent updates the text content of the DOM element with the given text. 86 | func updateDOMContent(text string) { 87 | // Get the document object from the global JavaScript context. 88 | document := js.Global().Get("document") 89 | // Get the DOM element by its ID. 90 | element := document.Call("getElementById", "text") 91 | // Set the innerText of the element to the provided text. 92 | element.Set("innerText", text) 93 | } 94 | --------------------------------------------------------------------------------