├── README.md ├── gowasmeval ├── Makefile ├── README.md ├── evaluator │ ├── evaluator.go │ └── govaluate │ │ └── govaluate.go ├── go.mod ├── go.sum ├── main.go └── site │ ├── go.mod │ ├── go.sum │ ├── index.html │ ├── main.wasm │ ├── server.go │ ├── start.sh │ └── wasm_exec.js ├── gowasmfetch ├── Makefile ├── README.md ├── fetcher │ ├── fetcher.go │ └── gofetch │ │ └── gofetch.go ├── go.mod ├── main.go ├── responder │ ├── go.mod │ ├── go.sum │ ├── responder.go │ └── start.sh └── site │ ├── go.mod │ ├── go.sum │ ├── index.html │ ├── main.wasm │ ├── server.go │ ├── start.sh │ └── wasm_exec.js └── gowasmsum ├── Makefile ├── README.md ├── go.mod ├── main.go ├── site ├── go.mod ├── go.sum ├── index.html ├── main.wasm ├── server.go ├── start.sh └── wasm_exec.js └── wasm_exporter └── sumof.go /README.md: -------------------------------------------------------------------------------- 1 | # Understanding WebAssembly and interoperability with Go and Javascript 2 | 3 | _(This is a gentle introduction to the world of WebAssembly from the lens of Go and Javascript. The purpose of this primer is to introduce WebAssembly to people who are already familiar with Go and want to use their understanding to build fast programs for the web and other environments outside the world of Go. If you are already substantially familiar with WebAssembly, you may want to directly jump to the experiments - https://github.com/gptankit/go-wasm#experiments)_ 4 | 5 | Traditionally Javascript has been the language of choice for doing any kind of logic on browsers. Because of its interpreted nature, it is usually slow in execution. Not to mention that writing complex logics in Javascript quickly becomes cumbersome and bloated. A new system was needed to allow for complex and faster code execution in the browser-land. 6 | 7 | ## Introducing WebAssembly 8 | 9 | WebAssembly (or wasm) is a new binary code format capable of executing in browser (and some non-browser) environments. It - 10 | 11 | - Is portable, fast and safe 12 | - Is primarily made for running in the browser alongside Javascript (same VM can execute both JS and Wasm) 13 | - Can also be executed by other runtimes like _Node.js_ and wasm specific runtimes (like [wasmer](https://wasmer.io/) and [wasmtime](https://wasmtime.dev/)) 14 | - Runs in a sandbox thus providing no direct access to host environment 15 | - Has a binary representation (_.wasm_) and a text representation (_.wat_ - uses [S-expressions](https://en.wikipedia.org/wiki/S-expression)) 16 | - Is a compilation target for C, C++, Rust, Go and many other languages 17 | 18 | Most languages use [emscripten](https://emscripten.org/) to compile to wasm (like C, C++, Rust, but not Go). Most languages also consider wasm module as a library but Go considers it as an application running alonside Javascript. 19 | 20 | ## Grammar 21 | 22 | WebAssembly has two representational formats - **binary** and **text**. Both of them are generated from a common abstract syntax. This abstract syntax follows from the same grammar rules but is represented differently in the final output. What it means is that the binary output (_wasm_) is a _linear encoding_ of abstract syntax to be executed by the virtual machine, while the text output (_wat_) is _S-expressions_ rendering of abstract syntax useful for analyzing and understanding the underlying logic. 23 | 24 | ## Internals 25 | 26 | #### Types 27 | 28 | WebAssembly supports **i32**, **i64**, **f32** and **f64** fundamental types only (also called *numtypes*). So, it is the responsibility of the glue code (or embedder) to convert other fundamental/composite types to _numtypes_ so wasm can understand them. 29 | 30 | Apart from these, wasm internally maintains other non-fundamental types - *reference types*, *value types*, *result types*, *function types*, *limits*, *memory types*, *table types*, *global types* and *external types* which are used in the wasm execution life cycle. 31 | 32 | #### Values 33 | 34 | While wasm operates only on numeric values (integers and floating points), the program can represent other entities in terms of *bytes* and *names* (strings) as well. These are then converted to integers while executing. 35 | 36 | #### Memories 37 | 38 | Wasm memory is *linear* - just a contiguous block of untyped bytes. This memory is exported and both wasm and Javascript can share data using this memory space. 39 | 40 | #### Tables 41 | 42 | Wasm table is a vector of reference types - references to functions, global or memory addresses etc. 43 | 44 | #### Module 45 | 46 | Wasm module is the result of final compilation of a wasm binary by the browser. It can define a set of exports and imports to interact with external environments. 47 | 48 | #### Instance 49 | 50 | A wasm instance is a runtime representation of a wasm module. It is treated as running in a sandbox environment seperate from host environment and other wasm instances and can only interact with them via well defined APIs. 51 | 52 | ## Inspecting 53 | 54 | Inspecting wasm by hand is hard as it is a binary format. Primary tool that we can use for inspecting its internal structure is [wabt](https://github.com/WebAssembly/wabt). It can be used to do a multitude of things with wasm including conversion to text format (and vice versa), printing info on the binary, validation etc. Most important programs included in *wabt* project are - 55 | 56 | - **wat2wasm**: translate from WebAssembly text format to the WebAssembly binary format 57 | - **wasm2wat**: the inverse of wat2wasm, translate from the binary format back to the text format (also known as a .wat) 58 | - **wasm-objdump**: print information about a wasm binary. Similiar to objdump. 59 | - **wasm-strip**: remove sections of a WebAssembly binary file 60 | - **wasm-validate**: validate a file in the WebAssembly binary format 61 | 62 | ## Go and WebAssembly 63 | 64 | WebAssembly is primarily built to be a compilation target to most high level programming languages (you can also code in WebAssembly text format and have it compiled to the binary though its not recommended). Below we'll limit ourselves to generating wasm binary from Go. 65 | 66 | a) In order for Javascript to call into Go/wasm, the Go functions must follow below signature - 67 | 68 | `fn func(this js.Value, args []js.Value) interface{}` 69 | 70 | here, *this* represents a Javascript context and *args* array represent the parameters to be passed to this function. The return value can be any value understood by both Go and Javascript lifted to an interface{} as the return type. 71 | 72 | This *fn* is to be wrapped in a *js.Func* object by passing fn to a *js.FuncOf* call - 73 | 74 | `fn_exported js.FuncOf(fn) js.Func` 75 | 76 | This *fn_exported* is then set on the Javascript *'window'* object using [syscall/js](https://golang.org/pkg/syscall/js/) package - 77 | 78 | `js.Global().Set("fn_exported_name", fn_exported)` 79 | 80 | b) Wasm cannot access DOM directly, therefore Go provides the package *syscall/js* which can be used to interact with DOM. Functions like *js.Global()* return Javascript *'window'* object that Go can use to get or set DOM elements. 81 | 82 | c) Wasm allows for single return value only. The allowed return types are *js.Value*, *js.Func*, *nil*, *bool*, *integers*, *floats*, *strings*, *[]interface{}* and *map[string]interface{}* which are then converted to corresponding Javascript types while crossing function boundaries. 83 | 84 | d) Go code can be compiled to wasm by setting **GOOS=js** and **GOARCH=wasm** directives in *go build* - 85 | 86 | `GOOS=js GOARCH=wasm go build -o $(BINPATH)` 87 | 88 | The generated *.wasm* file must be instantiated in the Javascript so the browser can compile it and make it ready for use. Go provides the instantiation mechanism through a special script called *wasm_exec.js* (found in *$GOROOT/misc/wasm/wasm_exec.js* location) which must also be copied to the web server and included in the html file. Apart from this, *wasm_exec.js* also enables mechanisms through which Go/wasm code can interact with Javascript APIs. 89 | 90 | ## Usage from Javascript 91 | 92 | a) One virtual machine is capable of executing both Javascript and wasm binary\ 93 | b) VM is responsible to compile wasm binary (either AOT and/or JIT) depending on the host environment\ 94 | c) Javascript has access to exported functions and memories of a wasm binary while wasm has access to the DOM via Javascript APIs\ 95 | d) Data can be shared between Javascript and wasm environment through function calls or linear memory but given that they differ in the data types, some glue code is usually needed to convert values from one environment to another. 96 | 97 | #### Instantiating wasm 98 | 99 | In order to instantiate a *.wasm* file, the Javascript file must import *wasm_exec.js* script. 100 | 101 | `` 102 | 103 | Then, we can use a little glue code provided by _wasm_exec.js_ to instantiate the wasm file (say _main.wasm_). 104 | 105 | ``` 106 | (async function loadAndRunGoWasm() { 107 | const go = new Go(); 108 | const result = await WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject); 109 | go.run(result.instance) 110 | })() 111 | ``` 112 | 113 | The _WebAssembly.instantiateStreaming_ call accepts two arguments - first, a _response_ object which contains the *.wasm* file fetched from server, and second, an _importObject_ which we get from the go runtime in `const go = new Go()` call. The *instantiateStreaming* call returns a result whose instance property can be passed to `go.run()` to start this wasm instance. 114 | 115 | #### Calling into wasm 116 | 117 | As Go exports functions to the Javascript _window_ object, we can call wasm functions normally as we would do for any Javascript functions - by using the function name `fn_exported_name` set in Go exports and passing in the required parameters. 118 | 119 | ## Experiments 120 | 121 | - **gowasmsum** (experiment demonstrating use of memory copying techniques between wasm and Javascript): https://github.com/gptankit/go-wasm/tree/main/gowasmsum 122 | - **gowasmeval** (experiment demonstrating use of external Go packages and suggested code structure): https://github.com/gptankit/go-wasm/tree/main/gowasmeval 123 | - **gowasmfetch** (experiment demonstrating use of I/O techniques and working around same-origin policy): https://github.com/gptankit/go-wasm/tree/main/gowasmfetch 124 | -------------------------------------------------------------------------------- /gowasmeval/Makefile: -------------------------------------------------------------------------------- 1 | GOOS=js 2 | GOARCH=wasm 3 | BINPATH=site/main.wasm 4 | 5 | all: build 6 | 7 | build: 8 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="-s -w" -trimpath -o $(BINPATH) main.go 9 | @echo "done" 10 | -------------------------------------------------------------------------------- /gowasmeval/README.md: -------------------------------------------------------------------------------- 1 | ## go-wasm-eval 2 | 3 | Evaluates expressions. This example demonstrates usage of external Go packages and how to structure Go/wasm code. 4 | 5 | ![image](https://user-images.githubusercontent.com/16796393/118148639-64594780-b42e-11eb-8271-3d0304232ca0.png) 6 | 7 | a) External Go packages can be imported and used in the same way as you would do for any normal Go program. Since we can only export functions of the type _js.Func_, we should ensure that the input/return parameters are loosely coupled with the core Go functionality (provided by either external package or layers above). Decoupling the two will enable us to extend/change both modules easily in the light of future contract changes (Go/wasm or external package). 8 | 9 | ``` 10 | func (e *NormalEvaluator) Bind() js.Func { 11 | 12 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 13 | expression := args[0].String() 14 | result, err := e.EvaluateFn(expression) 15 | if isErr(err) { 16 | return false 17 | } 18 | return js.ValueOf(result) 19 | }) 20 | } 21 | ``` 22 | 23 | b) While exporting functions through `js.Global().Set(, )` method, there can be a mistake in assigning the same function name to more than one function definitions. There is no compile or runtime error thrown in such a case, rather the wasm module simply exports the last bound function definition in order. To avoid this, suggest to use Go `map` to first set up <_func_name_, _func_def_> key value pairs, so the mistake is caught during compilation time as `duplicate key in map literal`. 24 | 25 | To run, cd to _gowasmeval/site_ and then *./start.sh* 26 | -------------------------------------------------------------------------------- /gowasmeval/evaluator/evaluator.go: -------------------------------------------------------------------------------- 1 | package evaluator 2 | 3 | import ( 4 | govaluate "gowasmeval/evaluator/govaluate" 5 | "syscall/js" 6 | ) 7 | 8 | type GenericEvaluator interface { 9 | Bind() js.Func 10 | } 11 | 12 | type NormalEvaluator struct { 13 | EvaluateFn func(string) (interface{}, error) 14 | } 15 | 16 | // NewEvaluator returns the evaluator object 17 | func NewEvaluator() GenericEvaluator { 18 | 19 | evaluator := &NormalEvaluator{ 20 | EvaluateFn: govaluate.InitGovaluate(), 21 | } 22 | 23 | return evaluator 24 | } 25 | 26 | // Bind creates the wrapper go function exposed to js 27 | func (e *NormalEvaluator) Bind() js.Func { 28 | 29 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 30 | expression := args[0].String() 31 | result, err := e.EvaluateFn(expression) 32 | if isErr(err) { 33 | return js.ValueOf(err.Error()) 34 | } 35 | return js.ValueOf(result) 36 | }) 37 | } 38 | 39 | func isErr(err error) bool { 40 | if err != nil { 41 | //log.Printf("error encountered: %s\n", err.Error()) 42 | return true 43 | } 44 | return false 45 | } 46 | -------------------------------------------------------------------------------- /gowasmeval/evaluator/govaluate/govaluate.go: -------------------------------------------------------------------------------- 1 | package govaluate 2 | 3 | import ( 4 | govaluate "github.com/Knetic/govaluate" 5 | ) 6 | 7 | // InitGovaluate returns function that can evaulate expression 8 | func InitGovaluate() func(string) (interface{}, error) { 9 | 10 | return func(expression string) (interface{}, error) { 11 | var evaluable_expression *govaluate.EvaluableExpression 12 | var result interface{} 13 | var err error 14 | 15 | evaluable_expression, err = govaluate.NewEvaluableExpression(expression) 16 | if err == nil { 17 | result, err = evaluable_expression.Evaluate(nil) 18 | if err == nil { 19 | return result, nil 20 | } 21 | } 22 | return nil, err 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gowasmeval/go.mod: -------------------------------------------------------------------------------- 1 | module gowasmeval 2 | 3 | go 1.16 4 | 5 | require github.com/Knetic/govaluate v3.0.0+incompatible 6 | -------------------------------------------------------------------------------- /gowasmeval/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= 2 | github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | -------------------------------------------------------------------------------- /gowasmeval/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gowasmeval/evaluator" 5 | "syscall/js" 6 | ) 7 | 8 | func main() { 9 | // channel is used so we can block on it, otherwise go process will exit when this module is loaded 10 | // and we want the exported functions still available for later executions 11 | c := make(chan struct{}, 0) 12 | 13 | wasmExportsMap := getWasmExportables() 14 | 15 | // export functions to js 16 | exportFuncs(wasmExportsMap) 17 | 18 | <-c 19 | } 20 | 21 | // getWasmExportables creates map of js func to go func 22 | func getWasmExportables() map[string]js.Func { 23 | 24 | return map[string]js.Func{ 25 | "evaluate": evaluator.NewEvaluator().Bind(), 26 | // add more funcs here 27 | } 28 | } 29 | 30 | /// exportFuncs exports go/wasm functions so as to be callable from js 31 | func exportFuncs(wasmExportsMap map[string]js.Func) { 32 | 33 | for k, v := range wasmExportsMap { 34 | js.Global().Set(k, v) // set function definition on js 'window' object 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gowasmeval/site/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.16 4 | 5 | require github.com/NYTimes/gziphandler v1.1.1 6 | -------------------------------------------------------------------------------- /gowasmeval/site/go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | -------------------------------------------------------------------------------- /gowasmeval/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go + WebAssembly 7 | 8 | 29 | 30 | 31 | 32 | Enter Expression (e.g. 2-4+23 < 232 or 9+1 == 8+2): 33 | 34 |
Answer:
35 | 36 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /gowasmeval/site/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptankit/go-wasm/0173cfcff40fc308b96deff094d3d54a4e47dc04/gowasmeval/site/main.wasm -------------------------------------------------------------------------------- /gowasmeval/site/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "github.com/NYTimes/gziphandler" 8 | ) 9 | 10 | var ( 11 | listen = flag.String("listen", ":8181", "listen address") 12 | dir = flag.String("dir", ".", "directory to serve") 13 | ) 14 | 15 | func main() { 16 | 17 | flag.Parse() 18 | 19 | log.Printf("listening on %q...", *listen) 20 | log.Fatal(http.ListenAndServe(*listen, gziphandler.GzipHandler(http.FileServer(http.Dir(*dir))))) 21 | } 22 | -------------------------------------------------------------------------------- /gowasmeval/site/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # start site 4 | 5 | go run server.go -listen=:8181 -dir=. 6 | -------------------------------------------------------------------------------- /gowasmeval/site/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 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | const enosys = () => { 34 | const err = new Error("not implemented"); 35 | err.code = "ENOSYS"; 36 | return err; 37 | }; 38 | 39 | if (!global.fs) { 40 | let outputBuf = ""; 41 | global.fs = { 42 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 43 | writeSync(fd, buf) { 44 | outputBuf += decoder.decode(buf); 45 | const nl = outputBuf.lastIndexOf("\n"); 46 | if (nl != -1) { 47 | console.log(outputBuf.substr(0, nl)); 48 | outputBuf = outputBuf.substr(nl + 1); 49 | } 50 | return buf.length; 51 | }, 52 | write(fd, buf, offset, length, position, callback) { 53 | if (offset !== 0 || length !== buf.length || position !== null) { 54 | callback(enosys()); 55 | return; 56 | } 57 | const n = this.writeSync(fd, buf); 58 | callback(null, n); 59 | }, 60 | chmod(path, mode, callback) { callback(enosys()); }, 61 | chown(path, uid, gid, callback) { callback(enosys()); }, 62 | close(fd, callback) { callback(enosys()); }, 63 | fchmod(fd, mode, callback) { callback(enosys()); }, 64 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 65 | fstat(fd, callback) { callback(enosys()); }, 66 | fsync(fd, callback) { callback(null); }, 67 | ftruncate(fd, length, callback) { callback(enosys()); }, 68 | lchown(path, uid, gid, callback) { callback(enosys()); }, 69 | link(path, link, callback) { callback(enosys()); }, 70 | lstat(path, callback) { callback(enosys()); }, 71 | mkdir(path, perm, callback) { callback(enosys()); }, 72 | open(path, flags, mode, callback) { callback(enosys()); }, 73 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 74 | readdir(path, callback) { callback(enosys()); }, 75 | readlink(path, callback) { callback(enosys()); }, 76 | rename(from, to, callback) { callback(enosys()); }, 77 | rmdir(path, callback) { callback(enosys()); }, 78 | stat(path, callback) { callback(enosys()); }, 79 | symlink(path, link, callback) { callback(enosys()); }, 80 | truncate(path, length, callback) { callback(enosys()); }, 81 | unlink(path, callback) { callback(enosys()); }, 82 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 83 | }; 84 | } 85 | 86 | if (!global.process) { 87 | global.process = { 88 | getuid() { return -1; }, 89 | getgid() { return -1; }, 90 | geteuid() { return -1; }, 91 | getegid() { return -1; }, 92 | getgroups() { throw enosys(); }, 93 | pid: -1, 94 | ppid: -1, 95 | umask() { throw enosys(); }, 96 | cwd() { throw enosys(); }, 97 | chdir() { throw enosys(); }, 98 | } 99 | } 100 | 101 | if (!global.crypto) { 102 | const nodeCrypto = require("crypto"); 103 | global.crypto = { 104 | getRandomValues(b) { 105 | nodeCrypto.randomFillSync(b); 106 | }, 107 | }; 108 | } 109 | 110 | if (!global.performance) { 111 | global.performance = { 112 | now() { 113 | const [sec, nsec] = process.hrtime(); 114 | return sec * 1000 + nsec / 1000000; 115 | }, 116 | }; 117 | } 118 | 119 | if (!global.TextEncoder) { 120 | global.TextEncoder = require("util").TextEncoder; 121 | } 122 | 123 | if (!global.TextDecoder) { 124 | global.TextDecoder = require("util").TextDecoder; 125 | } 126 | 127 | // End of polyfills for common API. 128 | 129 | const encoder = new TextEncoder("utf-8"); 130 | const decoder = new TextDecoder("utf-8"); 131 | 132 | global.Go = class { 133 | constructor() { 134 | this.argv = ["js"]; 135 | this.env = {}; 136 | this.exit = (code) => { 137 | if (code !== 0) { 138 | console.warn("exit code:", code); 139 | } 140 | }; 141 | this._exitPromise = new Promise((resolve) => { 142 | this._resolveExitPromise = resolve; 143 | }); 144 | this._pendingEvent = null; 145 | this._scheduledTimeouts = new Map(); 146 | this._nextCallbackTimeoutID = 1; 147 | 148 | const setInt64 = (addr, v) => { 149 | this.mem.setUint32(addr + 0, v, true); 150 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 151 | } 152 | 153 | const getInt64 = (addr) => { 154 | const low = this.mem.getUint32(addr + 0, true); 155 | const high = this.mem.getInt32(addr + 4, true); 156 | return low + high * 4294967296; 157 | } 158 | 159 | const loadValue = (addr) => { 160 | const f = this.mem.getFloat64(addr, true); 161 | if (f === 0) { 162 | return undefined; 163 | } 164 | if (!isNaN(f)) { 165 | return f; 166 | } 167 | 168 | const id = this.mem.getUint32(addr, true); 169 | return this._values[id]; 170 | } 171 | 172 | const storeValue = (addr, v) => { 173 | const nanHead = 0x7FF80000; 174 | 175 | if (typeof v === "number") { 176 | if (isNaN(v)) { 177 | this.mem.setUint32(addr + 4, nanHead, true); 178 | this.mem.setUint32(addr, 0, true); 179 | return; 180 | } 181 | if (v === 0) { 182 | this.mem.setUint32(addr + 4, nanHead, true); 183 | this.mem.setUint32(addr, 1, true); 184 | return; 185 | } 186 | this.mem.setFloat64(addr, v, true); 187 | return; 188 | } 189 | 190 | switch (v) { 191 | case undefined: 192 | this.mem.setFloat64(addr, 0, true); 193 | return; 194 | case null: 195 | this.mem.setUint32(addr + 4, nanHead, true); 196 | this.mem.setUint32(addr, 2, true); 197 | return; 198 | case true: 199 | this.mem.setUint32(addr + 4, nanHead, true); 200 | this.mem.setUint32(addr, 3, true); 201 | return; 202 | case false: 203 | this.mem.setUint32(addr + 4, nanHead, true); 204 | this.mem.setUint32(addr, 4, true); 205 | return; 206 | } 207 | 208 | let id = this._ids.get(v); 209 | if (id === undefined) { 210 | id = this._idPool.pop(); 211 | if (id === undefined) { 212 | id = this._values.length; 213 | } 214 | this._values[id] = v; 215 | this._goRefCounts[id] = 0; 216 | this._ids.set(v, id); 217 | } 218 | this._goRefCounts[id]++; 219 | let typeFlag = 1; 220 | switch (typeof v) { 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | const code = this.mem.getInt32(sp + 8, true); 268 | this.exited = true; 269 | delete this._inst; 270 | delete this._values; 271 | delete this._goRefCounts; 272 | delete this._ids; 273 | delete this._idPool; 274 | this.exit(code); 275 | }, 276 | 277 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 278 | "runtime.wasmWrite": (sp) => { 279 | const fd = getInt64(sp + 8); 280 | const p = getInt64(sp + 16); 281 | const n = this.mem.getInt32(sp + 24, true); 282 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 283 | }, 284 | 285 | // func resetMemoryDataView() 286 | "runtime.resetMemoryDataView": (sp) => { 287 | this.mem = new DataView(this._inst.exports.mem.buffer); 288 | }, 289 | 290 | // func nanotime1() int64 291 | "runtime.nanotime1": (sp) => { 292 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 293 | }, 294 | 295 | // func walltime1() (sec int64, nsec int32) 296 | "runtime.walltime1": (sp) => { 297 | const msec = (new Date).getTime(); 298 | setInt64(sp + 8, msec / 1000); 299 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 300 | }, 301 | 302 | // func scheduleTimeoutEvent(delay int64) int32 303 | "runtime.scheduleTimeoutEvent": (sp) => { 304 | const id = this._nextCallbackTimeoutID; 305 | this._nextCallbackTimeoutID++; 306 | this._scheduledTimeouts.set(id, setTimeout( 307 | () => { 308 | this._resume(); 309 | while (this._scheduledTimeouts.has(id)) { 310 | // for some reason Go failed to register the timeout event, log and try again 311 | // (temporary workaround for https://github.com/golang/go/issues/28975) 312 | console.warn("scheduleTimeoutEvent: missed timeout event"); 313 | this._resume(); 314 | } 315 | }, 316 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 317 | )); 318 | this.mem.setInt32(sp + 16, id, true); 319 | }, 320 | 321 | // func clearTimeoutEvent(id int32) 322 | "runtime.clearTimeoutEvent": (sp) => { 323 | const id = this.mem.getInt32(sp + 8, true); 324 | clearTimeout(this._scheduledTimeouts.get(id)); 325 | this._scheduledTimeouts.delete(id); 326 | }, 327 | 328 | // func getRandomData(r []byte) 329 | "runtime.getRandomData": (sp) => { 330 | crypto.getRandomValues(loadSlice(sp + 8)); 331 | }, 332 | 333 | // func finalizeRef(v ref) 334 | "syscall/js.finalizeRef": (sp) => { 335 | const id = this.mem.getUint32(sp + 8, true); 336 | this._goRefCounts[id]--; 337 | if (this._goRefCounts[id] === 0) { 338 | const v = this._values[id]; 339 | this._values[id] = null; 340 | this._ids.delete(v); 341 | this._idPool.push(id); 342 | } 343 | }, 344 | 345 | // func stringVal(value string) ref 346 | "syscall/js.stringVal": (sp) => { 347 | storeValue(sp + 24, loadString(sp + 8)); 348 | }, 349 | 350 | // func valueGet(v ref, p string) ref 351 | "syscall/js.valueGet": (sp) => { 352 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 353 | sp = this._inst.exports.getsp(); // see comment above 354 | storeValue(sp + 32, result); 355 | }, 356 | 357 | // func valueSet(v ref, p string, x ref) 358 | "syscall/js.valueSet": (sp) => { 359 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 360 | }, 361 | 362 | // func valueDelete(v ref, p string) 363 | "syscall/js.valueDelete": (sp) => { 364 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 365 | }, 366 | 367 | // func valueIndex(v ref, i int) ref 368 | "syscall/js.valueIndex": (sp) => { 369 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 370 | }, 371 | 372 | // valueSetIndex(v ref, i int, x ref) 373 | "syscall/js.valueSetIndex": (sp) => { 374 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 375 | }, 376 | 377 | // func valueCall(v ref, m string, args []ref) (ref, bool) 378 | "syscall/js.valueCall": (sp) => { 379 | try { 380 | const v = loadValue(sp + 8); 381 | const m = Reflect.get(v, loadString(sp + 16)); 382 | const args = loadSliceOfValues(sp + 32); 383 | const result = Reflect.apply(m, v, args); 384 | sp = this._inst.exports.getsp(); // see comment above 385 | storeValue(sp + 56, result); 386 | this.mem.setUint8(sp + 64, 1); 387 | } catch (err) { 388 | storeValue(sp + 56, err); 389 | this.mem.setUint8(sp + 64, 0); 390 | } 391 | }, 392 | 393 | // func valueInvoke(v ref, args []ref) (ref, bool) 394 | "syscall/js.valueInvoke": (sp) => { 395 | try { 396 | const v = loadValue(sp + 8); 397 | const args = loadSliceOfValues(sp + 16); 398 | const result = Reflect.apply(v, undefined, args); 399 | sp = this._inst.exports.getsp(); // see comment above 400 | storeValue(sp + 40, result); 401 | this.mem.setUint8(sp + 48, 1); 402 | } catch (err) { 403 | storeValue(sp + 40, err); 404 | this.mem.setUint8(sp + 48, 0); 405 | } 406 | }, 407 | 408 | // func valueNew(v ref, args []ref) (ref, bool) 409 | "syscall/js.valueNew": (sp) => { 410 | try { 411 | const v = loadValue(sp + 8); 412 | const args = loadSliceOfValues(sp + 16); 413 | const result = Reflect.construct(v, args); 414 | sp = this._inst.exports.getsp(); // see comment above 415 | storeValue(sp + 40, result); 416 | this.mem.setUint8(sp + 48, 1); 417 | } catch (err) { 418 | storeValue(sp + 40, err); 419 | this.mem.setUint8(sp + 48, 0); 420 | } 421 | }, 422 | 423 | // func valueLength(v ref) int 424 | "syscall/js.valueLength": (sp) => { 425 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 426 | }, 427 | 428 | // valuePrepareString(v ref) (ref, int) 429 | "syscall/js.valuePrepareString": (sp) => { 430 | const str = encoder.encode(String(loadValue(sp + 8))); 431 | storeValue(sp + 16, str); 432 | setInt64(sp + 24, str.length); 433 | }, 434 | 435 | // valueLoadString(v ref, b []byte) 436 | "syscall/js.valueLoadString": (sp) => { 437 | const str = loadValue(sp + 8); 438 | loadSlice(sp + 16).set(str); 439 | }, 440 | 441 | // func valueInstanceOf(v ref, t ref) bool 442 | "syscall/js.valueInstanceOf": (sp) => { 443 | this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 444 | }, 445 | 446 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 447 | "syscall/js.copyBytesToGo": (sp) => { 448 | const dst = loadSlice(sp + 8); 449 | const src = loadValue(sp + 32); 450 | if (!(src instanceof Uint8Array)) { 451 | this.mem.setUint8(sp + 48, 0); 452 | return; 453 | } 454 | const toCopy = src.subarray(0, dst.length); 455 | dst.set(toCopy); 456 | setInt64(sp + 40, toCopy.length); 457 | this.mem.setUint8(sp + 48, 1); 458 | }, 459 | 460 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 461 | "syscall/js.copyBytesToJS": (sp) => { 462 | const dst = loadValue(sp + 8); 463 | const src = loadSlice(sp + 16); 464 | if (!(dst instanceof Uint8Array)) { 465 | this.mem.setUint8(sp + 48, 0); 466 | return; 467 | } 468 | const toCopy = src.subarray(0, dst.length); 469 | dst.set(toCopy); 470 | setInt64(sp + 40, toCopy.length); 471 | this.mem.setUint8(sp + 48, 1); 472 | }, 473 | 474 | "debug": (value) => { 475 | console.log(value); 476 | }, 477 | } 478 | }; 479 | } 480 | 481 | async run(instance) { 482 | this._inst = instance; 483 | this.mem = new DataView(this._inst.exports.mem.buffer); 484 | this._values = [ // JS values that Go currently has references to, indexed by reference id 485 | NaN, 486 | 0, 487 | null, 488 | true, 489 | false, 490 | global, 491 | this, 492 | ]; 493 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 494 | this._ids = new Map(); // mapping from JS values to reference ids 495 | this._idPool = []; // unused ids that have been garbage collected 496 | this.exited = false; // whether the Go program has exited 497 | 498 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 499 | let offset = 4096; 500 | 501 | const strPtr = (str) => { 502 | const ptr = offset; 503 | const bytes = encoder.encode(str + "\0"); 504 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 505 | offset += bytes.length; 506 | if (offset % 8 !== 0) { 507 | offset += 8 - (offset % 8); 508 | } 509 | return ptr; 510 | }; 511 | 512 | const argc = this.argv.length; 513 | 514 | const argvPtrs = []; 515 | this.argv.forEach((arg) => { 516 | argvPtrs.push(strPtr(arg)); 517 | }); 518 | argvPtrs.push(0); 519 | 520 | const keys = Object.keys(this.env).sort(); 521 | keys.forEach((key) => { 522 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 523 | }); 524 | argvPtrs.push(0); 525 | 526 | const argv = offset; 527 | argvPtrs.forEach((ptr) => { 528 | this.mem.setUint32(offset, ptr, true); 529 | this.mem.setUint32(offset + 4, 0, true); 530 | offset += 8; 531 | }); 532 | 533 | this._inst.exports.run(argc, argv); 534 | if (this.exited) { 535 | this._resolveExitPromise(); 536 | } 537 | await this._exitPromise; 538 | } 539 | 540 | _resume() { 541 | if (this.exited) { 542 | throw new Error("Go program has already exited"); 543 | } 544 | this._inst.exports.resume(); 545 | if (this.exited) { 546 | this._resolveExitPromise(); 547 | } 548 | } 549 | 550 | _makeFuncWrapper(id) { 551 | const go = this; 552 | return function () { 553 | const event = { id: id, this: this, args: arguments }; 554 | go._pendingEvent = event; 555 | go._resume(); 556 | return event.result; 557 | }; 558 | } 559 | } 560 | 561 | if ( 562 | global.require && 563 | global.require.main === module && 564 | global.process && 565 | global.process.versions && 566 | !global.process.versions.electron 567 | ) { 568 | if (process.argv.length < 3) { 569 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 570 | process.exit(1); 571 | } 572 | 573 | const go = new Go(); 574 | go.argv = process.argv.slice(2); 575 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 576 | go.exit = process.exit; 577 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 578 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 579 | if (code === 0 && !go.exited) { 580 | // deadlock, make Go print error and stack traces 581 | go._pendingEvent = { id: 0 }; 582 | go._resume(); 583 | } 584 | }); 585 | return go.run(result.instance); 586 | }).catch((err) => { 587 | console.error(err); 588 | process.exit(1); 589 | }); 590 | } 591 | })(); 592 | -------------------------------------------------------------------------------- /gowasmfetch/Makefile: -------------------------------------------------------------------------------- 1 | GOOS=js 2 | GOARCH=wasm 3 | BINPATH=site/main.wasm 4 | 5 | all: build 6 | 7 | build: 8 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="-s -w" -trimpath -o $(BINPATH) main.go 9 | @echo "done" 10 | -------------------------------------------------------------------------------- /gowasmfetch/README.md: -------------------------------------------------------------------------------- 1 | ## go-wasm-fetch 2 | 3 | Fetches data from a url. This example demonstrates I/O handling in Go/wasm. 4 | 5 | ![image](https://user-images.githubusercontent.com/16796393/119686733-40e9c000-be64-11eb-9f17-edc8ebcb8ae5.png) 6 | 7 | a) I/O in WebAssembly is limited by I/O capabilities of JS itself. When you do a http request from Go/wasm, it gets translated to _fetch_ call in JS. As http calls in Go are blocking in nature and WebAssembly does not allow blocking (on I/O) from JS, all such calls must be done in a goroutine. Problem comes when we have to return a response back to JS. As the function cannot block, we cannot use Go channels to communicate the response to JS. The response, in this case, must be set on the JS _Response_ object itself, which JS can then stream. 8 | 9 | b) To enable communication between wasm and JS, we need to return a _Promise_ object instead of the actual Go function doing I/O. This is needed so we can have _resolve_ and _reject_ functions passed to our actual Go function and _Response_ streamed in JS. 10 | 11 | ``` 12 | promiseFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 13 | 14 | resolve := args[0] 15 | reject := args[1] 16 | 17 | // calling fetcher in a goroutine which will do http request and accordingly call resolve/reject callbacks 18 | go f.FetcherFn(url, resolve, reject) 19 | return nil 20 | }) 21 | 22 | // return 'Promise' object 23 | jsPromise := js.Global().Get("Promise") 24 | return jsPromise.New(promiseFunc) 25 | ``` 26 | 27 | ``` 28 | res, resErr := httpClient.Do(req) 29 | 30 | if resErr != nil { 31 | reject.Invoke(newJSError(resErr)) 32 | return 33 | } else if res != nil && res.Body != nil && res.StatusCode == http.StatusOK { 34 | defer res.Body.Close() 35 | body, _ := ioutil.ReadAll(res.Body) 36 | resolve.Invoke(newJSResponse(body)) 37 | return 38 | } 39 | ``` 40 | 41 | c) [Same-origin policy](https://en.wikipedia.org/wiki/Same-origin_policy) is applicable to all http requests made from wasm (same as JS). If you try to enter a url from another domain (given that this experiment is running locally on _localhost:8181_), you will get a '_Access blocked by CORS policy_' error. For the sake of this experiment, I added a _responder_ test server in the project (which listens on a different port _9191_ than the main site) with CORS handled. You need to cd to _gowasmfetch/responder_ and then *./start.sh* to start this test server. Once done, a url such as 'http://localhost:9191/fetchme' must return a successful response. 42 | 43 | To run, cd to _gowasmfetch/site_ and then *./start.sh* 44 | -------------------------------------------------------------------------------- /gowasmfetch/fetcher/fetcher.go: -------------------------------------------------------------------------------- 1 | package fetcher 2 | 3 | import ( 4 | gofetch "gowasmfetch/fetcher/gofetch" 5 | "syscall/js" 6 | ) 7 | 8 | type GenericFetcher interface { 9 | Bind() js.Func 10 | } 11 | 12 | type NormalFetcher struct { 13 | FetcherFn func(string, js.Value, js.Value) 14 | } 15 | 16 | // NewFetcher returns the fetcher object 17 | func NewFetcher() GenericFetcher { 18 | 19 | fetcher := &NormalFetcher{ 20 | FetcherFn: gofetch.InitHttpReq(), 21 | } 22 | 23 | return fetcher 24 | } 25 | 26 | // Bind returns a js promise wrapper that can be set on js window object 27 | func (f *NormalFetcher) Bind() js.Func { 28 | 29 | return js.FuncOf(func(this js.Value, args []js.Value) interface{} { 30 | 31 | url := args[0].String() 32 | // return a promise that can be called from JS to fetch url 33 | return newJSPromise(f, url) 34 | }) 35 | } 36 | 37 | // newJSPromise returns a new js promise 38 | func newJSPromise(f *NormalFetcher, url string) interface{} { 39 | 40 | promiseFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 41 | 42 | resolve := args[0] 43 | reject := args[1] 44 | 45 | // calling fetcher in a goroutine which will do http request and accordingly call resolve/reject callbacks 46 | go f.FetcherFn(url, resolve, reject) 47 | return nil 48 | }) 49 | 50 | // return 'Promise' object 51 | jsPromise := js.Global().Get("Promise") 52 | return jsPromise.New(promiseFunc) 53 | } 54 | -------------------------------------------------------------------------------- /gowasmfetch/fetcher/gofetch/gofetch.go: -------------------------------------------------------------------------------- 1 | package gofetch 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "syscall/js" 8 | "time" 9 | ) 10 | 11 | // InitHttpReq sets up a js func that can fetch from a url and accordingly call resolve/reject callbacks 12 | func InitHttpReq() func(string, js.Value, js.Value) { 13 | 14 | return func(url string, resolve js.Value, reject js.Value) { 15 | 16 | httpClient := &http.Client{ 17 | Timeout: 2 * time.Second, 18 | } 19 | 20 | req, reqErr := http.NewRequest("GET", url, nil) 21 | if reqErr != nil { 22 | reject.Invoke(newJSError(reqErr)) 23 | return 24 | } 25 | 26 | res, resErr := httpClient.Do(req) 27 | if resErr != nil { 28 | reject.Invoke(newJSError(resErr)) 29 | return 30 | } else if res != nil && res.Body != nil && res.StatusCode == http.StatusOK { 31 | defer res.Body.Close() 32 | body, _ := ioutil.ReadAll(res.Body) 33 | resolve.Invoke(newJSResponse(body)) 34 | return 35 | } 36 | 37 | reject.Invoke(newJSError(errors.New("Something went wrong!"))) 38 | } 39 | } 40 | 41 | // newJSError returns a new js error object 42 | func newJSError(err error) interface{} { 43 | 44 | // setting error to the error object 45 | jsError := js.Global().Get("Error") 46 | return jsError.New(err.Error()) 47 | } 48 | 49 | // newJSResponse returns a new js response object with response body set 50 | func newJSResponse(body []byte) interface{} { 51 | 52 | // converting "data" to a js Uint8Array object 53 | jsArray := js.Global().Get("Uint8Array") 54 | jsBody := jsArray.New(len(body)) 55 | js.CopyBytesToJS(jsBody, body) 56 | 57 | // setting js array to the response object 58 | jsResponse := js.Global().Get("Response") 59 | return jsResponse.New(jsBody) 60 | } 61 | -------------------------------------------------------------------------------- /gowasmfetch/go.mod: -------------------------------------------------------------------------------- 1 | module gowasmfetch 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /gowasmfetch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gowasmfetch/fetcher" 5 | "syscall/js" 6 | ) 7 | 8 | func main() { 9 | // channel is used so we can block on it, otherwise go process will exit when this module is loaded 10 | // and we want the exported functions still available for later executions 11 | c := make(chan struct{}, 0) 12 | 13 | wasmExportsMap := getWasmExportables() 14 | 15 | // export functions to js 16 | exportFuncs(wasmExportsMap) 17 | 18 | <-c 19 | } 20 | 21 | // getWasmExportables creates map of js func to go func 22 | func getWasmExportables() map[string]js.Func { 23 | 24 | return map[string]js.Func{ 25 | "fetchme": fetcher.NewFetcher().Bind(), 26 | // add more funcs here 27 | } 28 | } 29 | 30 | /// exportFuncs exports go/wasm functions so as to be callable from js 31 | func exportFuncs(wasmExportsMap map[string]js.Func) { 32 | 33 | for k, v := range wasmExportsMap { 34 | js.Global().Set(k, v) // set function definition on js 'window' object 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gowasmfetch/responder/go.mod: -------------------------------------------------------------------------------- 1 | module responder 2 | 3 | go 1.16 4 | 5 | require github.com/rs/cors v1.7.0 6 | -------------------------------------------------------------------------------- /gowasmfetch/responder/go.sum: -------------------------------------------------------------------------------- 1 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 2 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 3 | -------------------------------------------------------------------------------- /gowasmfetch/responder/responder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/rs/cors" 9 | ) 10 | 11 | var ( 12 | listen = flag.String("listen", ":9191", "listen address") 13 | ) 14 | 15 | func main() { 16 | 17 | flag.Parse() 18 | 19 | log.Printf("listening on %q...", *listen) 20 | 21 | smux := http.NewServeMux() 22 | smux.HandleFunc("/fetchme", func(res http.ResponseWriter, req *http.Request) { 23 | res.Header().Set("Content-Type", "application/json") 24 | res.Write([]byte(`{"Data": "OK!"}`)) 25 | }) 26 | 27 | // adding cors headers to the server responses 28 | crs := cors.New(cors.Options{ 29 | AllowedOrigins: []string{"http://localhost:8181"}, 30 | AllowedMethods: []string{"GET", "POST"}, 31 | }) 32 | 33 | handler := crs.Handler(smux) 34 | log.Fatal(http.ListenAndServe(*listen, handler)) 35 | } 36 | -------------------------------------------------------------------------------- /gowasmfetch/responder/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # start test server 4 | 5 | go run responder.go -listen=:9191 6 | -------------------------------------------------------------------------------- /gowasmfetch/site/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.16 4 | 5 | require github.com/NYTimes/gziphandler v1.1.1 6 | -------------------------------------------------------------------------------- /gowasmfetch/site/go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | -------------------------------------------------------------------------------- /gowasmfetch/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go + WebAssembly 7 | 8 | 29 | 30 | 31 | 32 | Enter URL (e.g. http://localhost:9191/fetchme or https://www.reddit.com): 33 | 34 |
Result:
35 | 36 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /gowasmfetch/site/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptankit/go-wasm/0173cfcff40fc308b96deff094d3d54a4e47dc04/gowasmfetch/site/main.wasm -------------------------------------------------------------------------------- /gowasmfetch/site/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "github.com/NYTimes/gziphandler" 8 | ) 9 | 10 | var ( 11 | listen = flag.String("listen", ":8181", "listen address") 12 | dir = flag.String("dir", ".", "directory to serve") 13 | ) 14 | 15 | func main() { 16 | 17 | flag.Parse() 18 | 19 | log.Printf("listening on %q...", *listen) 20 | log.Fatal(http.ListenAndServe(*listen, gziphandler.GzipHandler(http.FileServer(http.Dir(*dir))))) 21 | } 22 | -------------------------------------------------------------------------------- /gowasmfetch/site/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # start site 4 | 5 | go run server.go -listen=:8181 -dir=. 6 | -------------------------------------------------------------------------------- /gowasmfetch/site/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 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | const enosys = () => { 34 | const err = new Error("not implemented"); 35 | err.code = "ENOSYS"; 36 | return err; 37 | }; 38 | 39 | if (!global.fs) { 40 | let outputBuf = ""; 41 | global.fs = { 42 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 43 | writeSync(fd, buf) { 44 | outputBuf += decoder.decode(buf); 45 | const nl = outputBuf.lastIndexOf("\n"); 46 | if (nl != -1) { 47 | console.log(outputBuf.substr(0, nl)); 48 | outputBuf = outputBuf.substr(nl + 1); 49 | } 50 | return buf.length; 51 | }, 52 | write(fd, buf, offset, length, position, callback) { 53 | if (offset !== 0 || length !== buf.length || position !== null) { 54 | callback(enosys()); 55 | return; 56 | } 57 | const n = this.writeSync(fd, buf); 58 | callback(null, n); 59 | }, 60 | chmod(path, mode, callback) { callback(enosys()); }, 61 | chown(path, uid, gid, callback) { callback(enosys()); }, 62 | close(fd, callback) { callback(enosys()); }, 63 | fchmod(fd, mode, callback) { callback(enosys()); }, 64 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 65 | fstat(fd, callback) { callback(enosys()); }, 66 | fsync(fd, callback) { callback(null); }, 67 | ftruncate(fd, length, callback) { callback(enosys()); }, 68 | lchown(path, uid, gid, callback) { callback(enosys()); }, 69 | link(path, link, callback) { callback(enosys()); }, 70 | lstat(path, callback) { callback(enosys()); }, 71 | mkdir(path, perm, callback) { callback(enosys()); }, 72 | open(path, flags, mode, callback) { callback(enosys()); }, 73 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 74 | readdir(path, callback) { callback(enosys()); }, 75 | readlink(path, callback) { callback(enosys()); }, 76 | rename(from, to, callback) { callback(enosys()); }, 77 | rmdir(path, callback) { callback(enosys()); }, 78 | stat(path, callback) { callback(enosys()); }, 79 | symlink(path, link, callback) { callback(enosys()); }, 80 | truncate(path, length, callback) { callback(enosys()); }, 81 | unlink(path, callback) { callback(enosys()); }, 82 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 83 | }; 84 | } 85 | 86 | if (!global.process) { 87 | global.process = { 88 | getuid() { return -1; }, 89 | getgid() { return -1; }, 90 | geteuid() { return -1; }, 91 | getegid() { return -1; }, 92 | getgroups() { throw enosys(); }, 93 | pid: -1, 94 | ppid: -1, 95 | umask() { throw enosys(); }, 96 | cwd() { throw enosys(); }, 97 | chdir() { throw enosys(); }, 98 | } 99 | } 100 | 101 | if (!global.crypto) { 102 | const nodeCrypto = require("crypto"); 103 | global.crypto = { 104 | getRandomValues(b) { 105 | nodeCrypto.randomFillSync(b); 106 | }, 107 | }; 108 | } 109 | 110 | if (!global.performance) { 111 | global.performance = { 112 | now() { 113 | const [sec, nsec] = process.hrtime(); 114 | return sec * 1000 + nsec / 1000000; 115 | }, 116 | }; 117 | } 118 | 119 | if (!global.TextEncoder) { 120 | global.TextEncoder = require("util").TextEncoder; 121 | } 122 | 123 | if (!global.TextDecoder) { 124 | global.TextDecoder = require("util").TextDecoder; 125 | } 126 | 127 | // End of polyfills for common API. 128 | 129 | const encoder = new TextEncoder("utf-8"); 130 | const decoder = new TextDecoder("utf-8"); 131 | 132 | global.Go = class { 133 | constructor() { 134 | this.argv = ["js"]; 135 | this.env = {}; 136 | this.exit = (code) => { 137 | if (code !== 0) { 138 | console.warn("exit code:", code); 139 | } 140 | }; 141 | this._exitPromise = new Promise((resolve) => { 142 | this._resolveExitPromise = resolve; 143 | }); 144 | this._pendingEvent = null; 145 | this._scheduledTimeouts = new Map(); 146 | this._nextCallbackTimeoutID = 1; 147 | 148 | const setInt64 = (addr, v) => { 149 | this.mem.setUint32(addr + 0, v, true); 150 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 151 | } 152 | 153 | const getInt64 = (addr) => { 154 | const low = this.mem.getUint32(addr + 0, true); 155 | const high = this.mem.getInt32(addr + 4, true); 156 | return low + high * 4294967296; 157 | } 158 | 159 | const loadValue = (addr) => { 160 | const f = this.mem.getFloat64(addr, true); 161 | if (f === 0) { 162 | return undefined; 163 | } 164 | if (!isNaN(f)) { 165 | return f; 166 | } 167 | 168 | const id = this.mem.getUint32(addr, true); 169 | return this._values[id]; 170 | } 171 | 172 | const storeValue = (addr, v) => { 173 | const nanHead = 0x7FF80000; 174 | 175 | if (typeof v === "number") { 176 | if (isNaN(v)) { 177 | this.mem.setUint32(addr + 4, nanHead, true); 178 | this.mem.setUint32(addr, 0, true); 179 | return; 180 | } 181 | if (v === 0) { 182 | this.mem.setUint32(addr + 4, nanHead, true); 183 | this.mem.setUint32(addr, 1, true); 184 | return; 185 | } 186 | this.mem.setFloat64(addr, v, true); 187 | return; 188 | } 189 | 190 | switch (v) { 191 | case undefined: 192 | this.mem.setFloat64(addr, 0, true); 193 | return; 194 | case null: 195 | this.mem.setUint32(addr + 4, nanHead, true); 196 | this.mem.setUint32(addr, 2, true); 197 | return; 198 | case true: 199 | this.mem.setUint32(addr + 4, nanHead, true); 200 | this.mem.setUint32(addr, 3, true); 201 | return; 202 | case false: 203 | this.mem.setUint32(addr + 4, nanHead, true); 204 | this.mem.setUint32(addr, 4, true); 205 | return; 206 | } 207 | 208 | let id = this._ids.get(v); 209 | if (id === undefined) { 210 | id = this._idPool.pop(); 211 | if (id === undefined) { 212 | id = this._values.length; 213 | } 214 | this._values[id] = v; 215 | this._goRefCounts[id] = 0; 216 | this._ids.set(v, id); 217 | } 218 | this._goRefCounts[id]++; 219 | let typeFlag = 1; 220 | switch (typeof v) { 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | const code = this.mem.getInt32(sp + 8, true); 268 | this.exited = true; 269 | delete this._inst; 270 | delete this._values; 271 | delete this._goRefCounts; 272 | delete this._ids; 273 | delete this._idPool; 274 | this.exit(code); 275 | }, 276 | 277 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 278 | "runtime.wasmWrite": (sp) => { 279 | const fd = getInt64(sp + 8); 280 | const p = getInt64(sp + 16); 281 | const n = this.mem.getInt32(sp + 24, true); 282 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 283 | }, 284 | 285 | // func resetMemoryDataView() 286 | "runtime.resetMemoryDataView": (sp) => { 287 | this.mem = new DataView(this._inst.exports.mem.buffer); 288 | }, 289 | 290 | // func nanotime1() int64 291 | "runtime.nanotime1": (sp) => { 292 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 293 | }, 294 | 295 | // func walltime1() (sec int64, nsec int32) 296 | "runtime.walltime1": (sp) => { 297 | const msec = (new Date).getTime(); 298 | setInt64(sp + 8, msec / 1000); 299 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 300 | }, 301 | 302 | // func scheduleTimeoutEvent(delay int64) int32 303 | "runtime.scheduleTimeoutEvent": (sp) => { 304 | const id = this._nextCallbackTimeoutID; 305 | this._nextCallbackTimeoutID++; 306 | this._scheduledTimeouts.set(id, setTimeout( 307 | () => { 308 | this._resume(); 309 | while (this._scheduledTimeouts.has(id)) { 310 | // for some reason Go failed to register the timeout event, log and try again 311 | // (temporary workaround for https://github.com/golang/go/issues/28975) 312 | console.warn("scheduleTimeoutEvent: missed timeout event"); 313 | this._resume(); 314 | } 315 | }, 316 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 317 | )); 318 | this.mem.setInt32(sp + 16, id, true); 319 | }, 320 | 321 | // func clearTimeoutEvent(id int32) 322 | "runtime.clearTimeoutEvent": (sp) => { 323 | const id = this.mem.getInt32(sp + 8, true); 324 | clearTimeout(this._scheduledTimeouts.get(id)); 325 | this._scheduledTimeouts.delete(id); 326 | }, 327 | 328 | // func getRandomData(r []byte) 329 | "runtime.getRandomData": (sp) => { 330 | crypto.getRandomValues(loadSlice(sp + 8)); 331 | }, 332 | 333 | // func finalizeRef(v ref) 334 | "syscall/js.finalizeRef": (sp) => { 335 | const id = this.mem.getUint32(sp + 8, true); 336 | this._goRefCounts[id]--; 337 | if (this._goRefCounts[id] === 0) { 338 | const v = this._values[id]; 339 | this._values[id] = null; 340 | this._ids.delete(v); 341 | this._idPool.push(id); 342 | } 343 | }, 344 | 345 | // func stringVal(value string) ref 346 | "syscall/js.stringVal": (sp) => { 347 | storeValue(sp + 24, loadString(sp + 8)); 348 | }, 349 | 350 | // func valueGet(v ref, p string) ref 351 | "syscall/js.valueGet": (sp) => { 352 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 353 | sp = this._inst.exports.getsp(); // see comment above 354 | storeValue(sp + 32, result); 355 | }, 356 | 357 | // func valueSet(v ref, p string, x ref) 358 | "syscall/js.valueSet": (sp) => { 359 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 360 | }, 361 | 362 | // func valueDelete(v ref, p string) 363 | "syscall/js.valueDelete": (sp) => { 364 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 365 | }, 366 | 367 | // func valueIndex(v ref, i int) ref 368 | "syscall/js.valueIndex": (sp) => { 369 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 370 | }, 371 | 372 | // valueSetIndex(v ref, i int, x ref) 373 | "syscall/js.valueSetIndex": (sp) => { 374 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 375 | }, 376 | 377 | // func valueCall(v ref, m string, args []ref) (ref, bool) 378 | "syscall/js.valueCall": (sp) => { 379 | try { 380 | const v = loadValue(sp + 8); 381 | const m = Reflect.get(v, loadString(sp + 16)); 382 | const args = loadSliceOfValues(sp + 32); 383 | const result = Reflect.apply(m, v, args); 384 | sp = this._inst.exports.getsp(); // see comment above 385 | storeValue(sp + 56, result); 386 | this.mem.setUint8(sp + 64, 1); 387 | } catch (err) { 388 | storeValue(sp + 56, err); 389 | this.mem.setUint8(sp + 64, 0); 390 | } 391 | }, 392 | 393 | // func valueInvoke(v ref, args []ref) (ref, bool) 394 | "syscall/js.valueInvoke": (sp) => { 395 | try { 396 | const v = loadValue(sp + 8); 397 | const args = loadSliceOfValues(sp + 16); 398 | const result = Reflect.apply(v, undefined, args); 399 | sp = this._inst.exports.getsp(); // see comment above 400 | storeValue(sp + 40, result); 401 | this.mem.setUint8(sp + 48, 1); 402 | } catch (err) { 403 | storeValue(sp + 40, err); 404 | this.mem.setUint8(sp + 48, 0); 405 | } 406 | }, 407 | 408 | // func valueNew(v ref, args []ref) (ref, bool) 409 | "syscall/js.valueNew": (sp) => { 410 | try { 411 | const v = loadValue(sp + 8); 412 | const args = loadSliceOfValues(sp + 16); 413 | const result = Reflect.construct(v, args); 414 | sp = this._inst.exports.getsp(); // see comment above 415 | storeValue(sp + 40, result); 416 | this.mem.setUint8(sp + 48, 1); 417 | } catch (err) { 418 | storeValue(sp + 40, err); 419 | this.mem.setUint8(sp + 48, 0); 420 | } 421 | }, 422 | 423 | // func valueLength(v ref) int 424 | "syscall/js.valueLength": (sp) => { 425 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 426 | }, 427 | 428 | // valuePrepareString(v ref) (ref, int) 429 | "syscall/js.valuePrepareString": (sp) => { 430 | const str = encoder.encode(String(loadValue(sp + 8))); 431 | storeValue(sp + 16, str); 432 | setInt64(sp + 24, str.length); 433 | }, 434 | 435 | // valueLoadString(v ref, b []byte) 436 | "syscall/js.valueLoadString": (sp) => { 437 | const str = loadValue(sp + 8); 438 | loadSlice(sp + 16).set(str); 439 | }, 440 | 441 | // func valueInstanceOf(v ref, t ref) bool 442 | "syscall/js.valueInstanceOf": (sp) => { 443 | this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 444 | }, 445 | 446 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 447 | "syscall/js.copyBytesToGo": (sp) => { 448 | const dst = loadSlice(sp + 8); 449 | const src = loadValue(sp + 32); 450 | if (!(src instanceof Uint8Array)) { 451 | this.mem.setUint8(sp + 48, 0); 452 | return; 453 | } 454 | const toCopy = src.subarray(0, dst.length); 455 | dst.set(toCopy); 456 | setInt64(sp + 40, toCopy.length); 457 | this.mem.setUint8(sp + 48, 1); 458 | }, 459 | 460 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 461 | "syscall/js.copyBytesToJS": (sp) => { 462 | const dst = loadValue(sp + 8); 463 | const src = loadSlice(sp + 16); 464 | if (!(dst instanceof Uint8Array)) { 465 | this.mem.setUint8(sp + 48, 0); 466 | return; 467 | } 468 | const toCopy = src.subarray(0, dst.length); 469 | dst.set(toCopy); 470 | setInt64(sp + 40, toCopy.length); 471 | this.mem.setUint8(sp + 48, 1); 472 | }, 473 | 474 | "debug": (value) => { 475 | console.log(value); 476 | }, 477 | } 478 | }; 479 | } 480 | 481 | async run(instance) { 482 | this._inst = instance; 483 | this.mem = new DataView(this._inst.exports.mem.buffer); 484 | this._values = [ // JS values that Go currently has references to, indexed by reference id 485 | NaN, 486 | 0, 487 | null, 488 | true, 489 | false, 490 | global, 491 | this, 492 | ]; 493 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 494 | this._ids = new Map(); // mapping from JS values to reference ids 495 | this._idPool = []; // unused ids that have been garbage collected 496 | this.exited = false; // whether the Go program has exited 497 | 498 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 499 | let offset = 4096; 500 | 501 | const strPtr = (str) => { 502 | const ptr = offset; 503 | const bytes = encoder.encode(str + "\0"); 504 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 505 | offset += bytes.length; 506 | if (offset % 8 !== 0) { 507 | offset += 8 - (offset % 8); 508 | } 509 | return ptr; 510 | }; 511 | 512 | const argc = this.argv.length; 513 | 514 | const argvPtrs = []; 515 | this.argv.forEach((arg) => { 516 | argvPtrs.push(strPtr(arg)); 517 | }); 518 | argvPtrs.push(0); 519 | 520 | const keys = Object.keys(this.env).sort(); 521 | keys.forEach((key) => { 522 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 523 | }); 524 | argvPtrs.push(0); 525 | 526 | const argv = offset; 527 | argvPtrs.forEach((ptr) => { 528 | this.mem.setUint32(offset, ptr, true); 529 | this.mem.setUint32(offset + 4, 0, true); 530 | offset += 8; 531 | }); 532 | 533 | this._inst.exports.run(argc, argv); 534 | if (this.exited) { 535 | this._resolveExitPromise(); 536 | } 537 | await this._exitPromise; 538 | } 539 | 540 | _resume() { 541 | if (this.exited) { 542 | throw new Error("Go program has already exited"); 543 | } 544 | this._inst.exports.resume(); 545 | if (this.exited) { 546 | this._resolveExitPromise(); 547 | } 548 | } 549 | 550 | _makeFuncWrapper(id) { 551 | const go = this; 552 | return function () { 553 | const event = { id: id, this: this, args: arguments }; 554 | go._pendingEvent = event; 555 | go._resume(); 556 | return event.result; 557 | }; 558 | } 559 | } 560 | 561 | if ( 562 | global.require && 563 | global.require.main === module && 564 | global.process && 565 | global.process.versions && 566 | !global.process.versions.electron 567 | ) { 568 | if (process.argv.length < 3) { 569 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 570 | process.exit(1); 571 | } 572 | 573 | const go = new Go(); 574 | go.argv = process.argv.slice(2); 575 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 576 | go.exit = process.exit; 577 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 578 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 579 | if (code === 0 && !go.exited) { 580 | // deadlock, make Go print error and stack traces 581 | go._pendingEvent = { id: 0 }; 582 | go._resume(); 583 | } 584 | }); 585 | return go.run(result.instance); 586 | }).catch((err) => { 587 | console.error(err); 588 | process.exit(1); 589 | }); 590 | } 591 | })(); 592 | -------------------------------------------------------------------------------- /gowasmsum/Makefile: -------------------------------------------------------------------------------- 1 | GOOS=js 2 | GOARCH=wasm 3 | BINPATH=site/main.wasm 4 | 5 | all: build 6 | 7 | build: 8 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="-s -w" -trimpath -o $(BINPATH) main.go 9 | @echo "done" 10 | -------------------------------------------------------------------------------- /gowasmsum/README.md: -------------------------------------------------------------------------------- 1 | ## go-wasm-sum 2 | 3 | 4 | Calculates sum of an array of values. This example demonstrates three ways to share data between JS and WebAssembly. 5 | 6 | ![image](https://user-images.githubusercontent.com/16796393/118147990-c1a0c900-b42d-11eb-9095-93a767be392d.png) 7 | 8 | **Sum1: Considering value as uint8 and using in-built function for copying memory.** 9 | 10 | Create a new go array of the same len as js array - 11 | 12 | ``` 13 | goArray := make([]uint8, jsArrayLen) 14 | ``` 15 | 16 | And then copy using `js.CopyBytesToGo()` - 17 | 18 | ``` 19 | js.CopyBytesToGo(goArray, jsArray) 20 | ``` 21 | 22 | **Sum2: Considering value as string and manually copying memory.** 23 | 24 | Create a new go array of the same len as js array - 25 | 26 | ``` 27 | goArray := make([]string, jsArrayLen) 28 | ``` 29 | 30 | And then copy contents by looping over js array - 31 | 32 | ``` 33 | for i := 0; i < jsArrayLen; i++ { 34 | goArray[i] = jsArray.Index(i).String() 35 | } 36 | ``` 37 | 38 | **Sum3: Considering value as uint8 and using pointer to share and populate memory.** 39 | 40 | Have two functions - one for returning a pointer to linear memory (that JS can populate data with) and another to read the populated data from that pointer location. 41 | 42 | Crucial thing to note while returning pointer is that Go provides no mapping for a pointer to a JS object, so I converted it to an _unsafe.Pointer_ and set it in a map (so Go can convert the `map[string]interface{}` to a JS _Object_ with the pointer value casted to a _uintptr_). 43 | 44 | ``` 45 | goArray := make([]uint8, goArrayLen) 46 | ptr = &goArray 47 | 48 | boxedPtr := unsafe.Pointer(ptr) 49 | boxedPtrMap := map[string]interface{}{ 50 | "internalptr": boxedPtr, 51 | } 52 | return js.ValueOf(boxedPtrMap) 53 | ``` 54 | 55 | Another thing to note while receiving pointer is that the pointer value - _uintptr_ - is just a number with no pointer semantics, so I converted it to a _*[]uint8_ which can then be used to iterate over. 56 | 57 | ``` 58 | sliceHeader := &reflect.SliceHeader{ 59 | Data: uintptr(args[0].Int()), 60 | Len: len, 61 | Cap: len, 62 | } 63 | 64 | var ptr = (*[]uint8)(unsafe.Pointer(sliceHeader)) 65 | ``` 66 | 67 | To run, cd to _gowasmsum/site_ and then *./start.sh* 68 | -------------------------------------------------------------------------------- /gowasmsum/go.mod: -------------------------------------------------------------------------------- 1 | module gowasmsum 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /gowasmsum/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | wasmex "gowasmsum/wasm_exporter" 5 | "syscall/js" 6 | ) 7 | 8 | func main() { 9 | // channel is used so we can block on it, otherwise go process will exit when this module is loaded 10 | // and we want the exported functions still available for later executions 11 | c := make(chan struct{}, 0) 12 | 13 | // export functions to js 14 | exportFuncs() 15 | 16 | <-c 17 | } 18 | 19 | /// exportFuncs exports go functions to be callable from js 20 | func exportFuncs() { 21 | 22 | js.Global().Set("sumOf", js.FuncOf(wasmex.SumOf)) 23 | js.Global().Set("sumStringOf", js.FuncOf(wasmex.SumStringOf)) 24 | js.Global().Set("initializeWasmMemory", js.FuncOf(wasmex.InitializeWasmMemory)) 25 | js.Global().Set("sumOf_ZeroCopy", js.FuncOf(wasmex.SumOf_ZeroCopy)) 26 | } 27 | -------------------------------------------------------------------------------- /gowasmsum/site/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.16 4 | 5 | require github.com/NYTimes/gziphandler v1.1.1 6 | -------------------------------------------------------------------------------- /gowasmsum/site/go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | -------------------------------------------------------------------------------- /gowasmsum/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go + WebAssembly 7 | 8 | 29 | 30 | 31 | Enter Array Items (that can fit in uint8, e.g 1,10,100): 32 | 33 |
34 |
35 | Sum1 (considering uint8arr and copy):
36 | Sum2 (considering stringarr and copy):
37 | Sum3 (considering uint8arr and zero-copy): 38 |
39 |
40 | 41 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /gowasmsum/site/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptankit/go-wasm/0173cfcff40fc308b96deff094d3d54a4e47dc04/gowasmsum/site/main.wasm -------------------------------------------------------------------------------- /gowasmsum/site/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "github.com/NYTimes/gziphandler" 8 | ) 9 | 10 | var ( 11 | listen = flag.String("listen", ":8181", "listen address") 12 | dir = flag.String("dir", ".", "directory to serve") 13 | ) 14 | 15 | func main() { 16 | 17 | flag.Parse() 18 | log.Printf("listening on %q...", *listen) 19 | log.Fatal(http.ListenAndServe(*listen, gziphandler.GzipHandler(http.FileServer(http.Dir(*dir))))) 20 | } 21 | -------------------------------------------------------------------------------- /gowasmsum/site/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # start site 4 | 5 | go run server.go -listen=:8181 -dir=. 6 | -------------------------------------------------------------------------------- /gowasmsum/site/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 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | const enosys = () => { 34 | const err = new Error("not implemented"); 35 | err.code = "ENOSYS"; 36 | return err; 37 | }; 38 | 39 | if (!global.fs) { 40 | let outputBuf = ""; 41 | global.fs = { 42 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 43 | writeSync(fd, buf) { 44 | outputBuf += decoder.decode(buf); 45 | const nl = outputBuf.lastIndexOf("\n"); 46 | if (nl != -1) { 47 | console.log(outputBuf.substr(0, nl)); 48 | outputBuf = outputBuf.substr(nl + 1); 49 | } 50 | return buf.length; 51 | }, 52 | write(fd, buf, offset, length, position, callback) { 53 | if (offset !== 0 || length !== buf.length || position !== null) { 54 | callback(enosys()); 55 | return; 56 | } 57 | const n = this.writeSync(fd, buf); 58 | callback(null, n); 59 | }, 60 | chmod(path, mode, callback) { callback(enosys()); }, 61 | chown(path, uid, gid, callback) { callback(enosys()); }, 62 | close(fd, callback) { callback(enosys()); }, 63 | fchmod(fd, mode, callback) { callback(enosys()); }, 64 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 65 | fstat(fd, callback) { callback(enosys()); }, 66 | fsync(fd, callback) { callback(null); }, 67 | ftruncate(fd, length, callback) { callback(enosys()); }, 68 | lchown(path, uid, gid, callback) { callback(enosys()); }, 69 | link(path, link, callback) { callback(enosys()); }, 70 | lstat(path, callback) { callback(enosys()); }, 71 | mkdir(path, perm, callback) { callback(enosys()); }, 72 | open(path, flags, mode, callback) { callback(enosys()); }, 73 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 74 | readdir(path, callback) { callback(enosys()); }, 75 | readlink(path, callback) { callback(enosys()); }, 76 | rename(from, to, callback) { callback(enosys()); }, 77 | rmdir(path, callback) { callback(enosys()); }, 78 | stat(path, callback) { callback(enosys()); }, 79 | symlink(path, link, callback) { callback(enosys()); }, 80 | truncate(path, length, callback) { callback(enosys()); }, 81 | unlink(path, callback) { callback(enosys()); }, 82 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 83 | }; 84 | } 85 | 86 | if (!global.process) { 87 | global.process = { 88 | getuid() { return -1; }, 89 | getgid() { return -1; }, 90 | geteuid() { return -1; }, 91 | getegid() { return -1; }, 92 | getgroups() { throw enosys(); }, 93 | pid: -1, 94 | ppid: -1, 95 | umask() { throw enosys(); }, 96 | cwd() { throw enosys(); }, 97 | chdir() { throw enosys(); }, 98 | } 99 | } 100 | 101 | if (!global.crypto) { 102 | const nodeCrypto = require("crypto"); 103 | global.crypto = { 104 | getRandomValues(b) { 105 | nodeCrypto.randomFillSync(b); 106 | }, 107 | }; 108 | } 109 | 110 | if (!global.performance) { 111 | global.performance = { 112 | now() { 113 | const [sec, nsec] = process.hrtime(); 114 | return sec * 1000 + nsec / 1000000; 115 | }, 116 | }; 117 | } 118 | 119 | if (!global.TextEncoder) { 120 | global.TextEncoder = require("util").TextEncoder; 121 | } 122 | 123 | if (!global.TextDecoder) { 124 | global.TextDecoder = require("util").TextDecoder; 125 | } 126 | 127 | // End of polyfills for common API. 128 | 129 | const encoder = new TextEncoder("utf-8"); 130 | const decoder = new TextDecoder("utf-8"); 131 | 132 | global.Go = class { 133 | constructor() { 134 | this.argv = ["js"]; 135 | this.env = {}; 136 | this.exit = (code) => { 137 | if (code !== 0) { 138 | console.warn("exit code:", code); 139 | } 140 | }; 141 | this._exitPromise = new Promise((resolve) => { 142 | this._resolveExitPromise = resolve; 143 | }); 144 | this._pendingEvent = null; 145 | this._scheduledTimeouts = new Map(); 146 | this._nextCallbackTimeoutID = 1; 147 | 148 | const setInt64 = (addr, v) => { 149 | this.mem.setUint32(addr + 0, v, true); 150 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 151 | } 152 | 153 | const getInt64 = (addr) => { 154 | const low = this.mem.getUint32(addr + 0, true); 155 | const high = this.mem.getInt32(addr + 4, true); 156 | return low + high * 4294967296; 157 | } 158 | 159 | const loadValue = (addr) => { 160 | const f = this.mem.getFloat64(addr, true); 161 | if (f === 0) { 162 | return undefined; 163 | } 164 | if (!isNaN(f)) { 165 | return f; 166 | } 167 | 168 | const id = this.mem.getUint32(addr, true); 169 | return this._values[id]; 170 | } 171 | 172 | const storeValue = (addr, v) => { 173 | const nanHead = 0x7FF80000; 174 | 175 | if (typeof v === "number") { 176 | if (isNaN(v)) { 177 | this.mem.setUint32(addr + 4, nanHead, true); 178 | this.mem.setUint32(addr, 0, true); 179 | return; 180 | } 181 | if (v === 0) { 182 | this.mem.setUint32(addr + 4, nanHead, true); 183 | this.mem.setUint32(addr, 1, true); 184 | return; 185 | } 186 | this.mem.setFloat64(addr, v, true); 187 | return; 188 | } 189 | 190 | switch (v) { 191 | case undefined: 192 | this.mem.setFloat64(addr, 0, true); 193 | return; 194 | case null: 195 | this.mem.setUint32(addr + 4, nanHead, true); 196 | this.mem.setUint32(addr, 2, true); 197 | return; 198 | case true: 199 | this.mem.setUint32(addr + 4, nanHead, true); 200 | this.mem.setUint32(addr, 3, true); 201 | return; 202 | case false: 203 | this.mem.setUint32(addr + 4, nanHead, true); 204 | this.mem.setUint32(addr, 4, true); 205 | return; 206 | } 207 | 208 | let id = this._ids.get(v); 209 | if (id === undefined) { 210 | id = this._idPool.pop(); 211 | if (id === undefined) { 212 | id = this._values.length; 213 | } 214 | this._values[id] = v; 215 | this._goRefCounts[id] = 0; 216 | this._ids.set(v, id); 217 | } 218 | this._goRefCounts[id]++; 219 | let typeFlag = 1; 220 | switch (typeof v) { 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | const code = this.mem.getInt32(sp + 8, true); 268 | this.exited = true; 269 | delete this._inst; 270 | delete this._values; 271 | delete this._goRefCounts; 272 | delete this._ids; 273 | delete this._idPool; 274 | this.exit(code); 275 | }, 276 | 277 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 278 | "runtime.wasmWrite": (sp) => { 279 | const fd = getInt64(sp + 8); 280 | const p = getInt64(sp + 16); 281 | const n = this.mem.getInt32(sp + 24, true); 282 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 283 | }, 284 | 285 | // func resetMemoryDataView() 286 | "runtime.resetMemoryDataView": (sp) => { 287 | this.mem = new DataView(this._inst.exports.mem.buffer); 288 | }, 289 | 290 | // func nanotime1() int64 291 | "runtime.nanotime1": (sp) => { 292 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 293 | }, 294 | 295 | // func walltime1() (sec int64, nsec int32) 296 | "runtime.walltime1": (sp) => { 297 | const msec = (new Date).getTime(); 298 | setInt64(sp + 8, msec / 1000); 299 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 300 | }, 301 | 302 | // func scheduleTimeoutEvent(delay int64) int32 303 | "runtime.scheduleTimeoutEvent": (sp) => { 304 | const id = this._nextCallbackTimeoutID; 305 | this._nextCallbackTimeoutID++; 306 | this._scheduledTimeouts.set(id, setTimeout( 307 | () => { 308 | this._resume(); 309 | while (this._scheduledTimeouts.has(id)) { 310 | // for some reason Go failed to register the timeout event, log and try again 311 | // (temporary workaround for https://github.com/golang/go/issues/28975) 312 | console.warn("scheduleTimeoutEvent: missed timeout event"); 313 | this._resume(); 314 | } 315 | }, 316 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 317 | )); 318 | this.mem.setInt32(sp + 16, id, true); 319 | }, 320 | 321 | // func clearTimeoutEvent(id int32) 322 | "runtime.clearTimeoutEvent": (sp) => { 323 | const id = this.mem.getInt32(sp + 8, true); 324 | clearTimeout(this._scheduledTimeouts.get(id)); 325 | this._scheduledTimeouts.delete(id); 326 | }, 327 | 328 | // func getRandomData(r []byte) 329 | "runtime.getRandomData": (sp) => { 330 | crypto.getRandomValues(loadSlice(sp + 8)); 331 | }, 332 | 333 | // func finalizeRef(v ref) 334 | "syscall/js.finalizeRef": (sp) => { 335 | const id = this.mem.getUint32(sp + 8, true); 336 | this._goRefCounts[id]--; 337 | if (this._goRefCounts[id] === 0) { 338 | const v = this._values[id]; 339 | this._values[id] = null; 340 | this._ids.delete(v); 341 | this._idPool.push(id); 342 | } 343 | }, 344 | 345 | // func stringVal(value string) ref 346 | "syscall/js.stringVal": (sp) => { 347 | storeValue(sp + 24, loadString(sp + 8)); 348 | }, 349 | 350 | // func valueGet(v ref, p string) ref 351 | "syscall/js.valueGet": (sp) => { 352 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 353 | sp = this._inst.exports.getsp(); // see comment above 354 | storeValue(sp + 32, result); 355 | }, 356 | 357 | // func valueSet(v ref, p string, x ref) 358 | "syscall/js.valueSet": (sp) => { 359 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 360 | }, 361 | 362 | // func valueDelete(v ref, p string) 363 | "syscall/js.valueDelete": (sp) => { 364 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 365 | }, 366 | 367 | // func valueIndex(v ref, i int) ref 368 | "syscall/js.valueIndex": (sp) => { 369 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 370 | }, 371 | 372 | // valueSetIndex(v ref, i int, x ref) 373 | "syscall/js.valueSetIndex": (sp) => { 374 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 375 | }, 376 | 377 | // func valueCall(v ref, m string, args []ref) (ref, bool) 378 | "syscall/js.valueCall": (sp) => { 379 | try { 380 | const v = loadValue(sp + 8); 381 | const m = Reflect.get(v, loadString(sp + 16)); 382 | const args = loadSliceOfValues(sp + 32); 383 | const result = Reflect.apply(m, v, args); 384 | sp = this._inst.exports.getsp(); // see comment above 385 | storeValue(sp + 56, result); 386 | this.mem.setUint8(sp + 64, 1); 387 | } catch (err) { 388 | storeValue(sp + 56, err); 389 | this.mem.setUint8(sp + 64, 0); 390 | } 391 | }, 392 | 393 | // func valueInvoke(v ref, args []ref) (ref, bool) 394 | "syscall/js.valueInvoke": (sp) => { 395 | try { 396 | const v = loadValue(sp + 8); 397 | const args = loadSliceOfValues(sp + 16); 398 | const result = Reflect.apply(v, undefined, args); 399 | sp = this._inst.exports.getsp(); // see comment above 400 | storeValue(sp + 40, result); 401 | this.mem.setUint8(sp + 48, 1); 402 | } catch (err) { 403 | storeValue(sp + 40, err); 404 | this.mem.setUint8(sp + 48, 0); 405 | } 406 | }, 407 | 408 | // func valueNew(v ref, args []ref) (ref, bool) 409 | "syscall/js.valueNew": (sp) => { 410 | try { 411 | const v = loadValue(sp + 8); 412 | const args = loadSliceOfValues(sp + 16); 413 | const result = Reflect.construct(v, args); 414 | sp = this._inst.exports.getsp(); // see comment above 415 | storeValue(sp + 40, result); 416 | this.mem.setUint8(sp + 48, 1); 417 | } catch (err) { 418 | storeValue(sp + 40, err); 419 | this.mem.setUint8(sp + 48, 0); 420 | } 421 | }, 422 | 423 | // func valueLength(v ref) int 424 | "syscall/js.valueLength": (sp) => { 425 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 426 | }, 427 | 428 | // valuePrepareString(v ref) (ref, int) 429 | "syscall/js.valuePrepareString": (sp) => { 430 | const str = encoder.encode(String(loadValue(sp + 8))); 431 | storeValue(sp + 16, str); 432 | setInt64(sp + 24, str.length); 433 | }, 434 | 435 | // valueLoadString(v ref, b []byte) 436 | "syscall/js.valueLoadString": (sp) => { 437 | const str = loadValue(sp + 8); 438 | loadSlice(sp + 16).set(str); 439 | }, 440 | 441 | // func valueInstanceOf(v ref, t ref) bool 442 | "syscall/js.valueInstanceOf": (sp) => { 443 | this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 444 | }, 445 | 446 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 447 | "syscall/js.copyBytesToGo": (sp) => { 448 | const dst = loadSlice(sp + 8); 449 | const src = loadValue(sp + 32); 450 | if (!(src instanceof Uint8Array)) { 451 | this.mem.setUint8(sp + 48, 0); 452 | return; 453 | } 454 | const toCopy = src.subarray(0, dst.length); 455 | dst.set(toCopy); 456 | setInt64(sp + 40, toCopy.length); 457 | this.mem.setUint8(sp + 48, 1); 458 | }, 459 | 460 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 461 | "syscall/js.copyBytesToJS": (sp) => { 462 | const dst = loadValue(sp + 8); 463 | const src = loadSlice(sp + 16); 464 | if (!(dst instanceof Uint8Array)) { 465 | this.mem.setUint8(sp + 48, 0); 466 | return; 467 | } 468 | const toCopy = src.subarray(0, dst.length); 469 | dst.set(toCopy); 470 | setInt64(sp + 40, toCopy.length); 471 | this.mem.setUint8(sp + 48, 1); 472 | }, 473 | 474 | "debug": (value) => { 475 | console.log(value); 476 | }, 477 | } 478 | }; 479 | } 480 | 481 | async run(instance) { 482 | this._inst = instance; 483 | this.mem = new DataView(this._inst.exports.mem.buffer); 484 | this._values = [ // JS values that Go currently has references to, indexed by reference id 485 | NaN, 486 | 0, 487 | null, 488 | true, 489 | false, 490 | global, 491 | this, 492 | ]; 493 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 494 | this._ids = new Map(); // mapping from JS values to reference ids 495 | this._idPool = []; // unused ids that have been garbage collected 496 | this.exited = false; // whether the Go program has exited 497 | 498 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 499 | let offset = 4096; 500 | 501 | const strPtr = (str) => { 502 | const ptr = offset; 503 | const bytes = encoder.encode(str + "\0"); 504 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 505 | offset += bytes.length; 506 | if (offset % 8 !== 0) { 507 | offset += 8 - (offset % 8); 508 | } 509 | return ptr; 510 | }; 511 | 512 | const argc = this.argv.length; 513 | 514 | const argvPtrs = []; 515 | this.argv.forEach((arg) => { 516 | argvPtrs.push(strPtr(arg)); 517 | }); 518 | argvPtrs.push(0); 519 | 520 | const keys = Object.keys(this.env).sort(); 521 | keys.forEach((key) => { 522 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 523 | }); 524 | argvPtrs.push(0); 525 | 526 | const argv = offset; 527 | argvPtrs.forEach((ptr) => { 528 | this.mem.setUint32(offset, ptr, true); 529 | this.mem.setUint32(offset + 4, 0, true); 530 | offset += 8; 531 | }); 532 | 533 | this._inst.exports.run(argc, argv); 534 | if (this.exited) { 535 | this._resolveExitPromise(); 536 | } 537 | await this._exitPromise; 538 | } 539 | 540 | _resume() { 541 | if (this.exited) { 542 | throw new Error("Go program has already exited"); 543 | } 544 | this._inst.exports.resume(); 545 | if (this.exited) { 546 | this._resolveExitPromise(); 547 | } 548 | } 549 | 550 | _makeFuncWrapper(id) { 551 | const go = this; 552 | return function () { 553 | const event = { id: id, this: this, args: arguments }; 554 | go._pendingEvent = event; 555 | go._resume(); 556 | return event.result; 557 | }; 558 | } 559 | } 560 | 561 | if ( 562 | global.require && 563 | global.require.main === module && 564 | global.process && 565 | global.process.versions && 566 | !global.process.versions.electron 567 | ) { 568 | if (process.argv.length < 3) { 569 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 570 | process.exit(1); 571 | } 572 | 573 | const go = new Go(); 574 | go.argv = process.argv.slice(2); 575 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 576 | go.exit = process.exit; 577 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 578 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 579 | if (code === 0 && !go.exited) { 580 | // deadlock, make Go print error and stack traces 581 | go._pendingEvent = { id: 0 }; 582 | go._resume(); 583 | } 584 | }); 585 | return go.run(result.instance); 586 | }).catch((err) => { 587 | console.error(err); 588 | process.exit(1); 589 | }); 590 | } 591 | })(); 592 | -------------------------------------------------------------------------------- /gowasmsum/wasm_exporter/sumof.go: -------------------------------------------------------------------------------- 1 | package wasm_exporter 2 | 3 | import ( 4 | "reflect" 5 | "syscall/js" 6 | "unsafe" 7 | ) 8 | 9 | // SumOf returns sum of uint8 numbers 10 | func SumOf(this js.Value, args []js.Value) interface{} { 11 | 12 | jsArray := args[0] 13 | jsArrayLen := jsArray.Get("byteLength").Int() // we can also use jsArray.Length() provided by syscall/js 14 | 15 | // one way of copying memory (given array content is uin8) 16 | // if array content is not uint8 but uint16 or uint32 or uint64, consider using encoding/binary package to flatten array to []byte and then encode to uint8 17 | goArray := make([]uint8, jsArrayLen) 18 | 19 | js.CopyBytesToGo(goArray, jsArray) // array value types have to be uint8 20 | 21 | var sum uint8 22 | for i := 0; i < len(goArray); i++ { 23 | sum += goArray[i] 24 | } 25 | 26 | return js.ValueOf(sum) 27 | } 28 | 29 | // SumStringOf returns concatenation of strings 30 | func SumStringOf(this js.Value, args []js.Value) interface{} { 31 | 32 | jsArray := args[0] 33 | jsArrayLen := jsArray.Get("length").Int() 34 | 35 | // one way of copying memory (given array content is string) 36 | goArray := make([]string, jsArrayLen) 37 | 38 | for i := 0; i < jsArrayLen; i++ { 39 | goArray[i] = jsArray.Index(i).String() 40 | } 41 | 42 | var sum string 43 | for i := 0; i < len(goArray); i++ { 44 | sum += goArray[i] 45 | } 46 | 47 | return js.ValueOf(sum) 48 | } 49 | 50 | // Below functions are used as a zero copy alternative to SumOf function 51 | 52 | // InitializeWasmMemory initializes wasm memory of passed length and returns a pointer 53 | func InitializeWasmMemory(this js.Value, args []js.Value) interface{} { 54 | 55 | var ptr *[]uint8 56 | goArrayLen := args[0].Int() 57 | 58 | goArray := make([]uint8, goArrayLen) 59 | ptr = &goArray 60 | 61 | boxedPtr := unsafe.Pointer(ptr) 62 | boxedPtrMap := map[string]interface{}{ 63 | "internalptr": boxedPtr, 64 | } 65 | return js.ValueOf(boxedPtrMap) 66 | } 67 | 68 | // SumOf_ZeroCopy loads the array populated at the pointer and returns sum 69 | func SumOf_ZeroCopy(this js.Value, args []js.Value) interface{} { 70 | 71 | var len = args[1].Int() 72 | 73 | sliceHeader := &reflect.SliceHeader{ 74 | Data: uintptr(args[0].Int()), 75 | Len: len, 76 | Cap: len, 77 | } 78 | 79 | var ptr = (*[]uint8)(unsafe.Pointer(sliceHeader)) 80 | 81 | var sum uint8 82 | for i := 0; i < len; i++ { 83 | sum += uint8((*ptr)[i]) 84 | } 85 | 86 | return js.ValueOf(sum) 87 | } 88 | --------------------------------------------------------------------------------