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