├── Dockerfile
├── Makefile
├── README.md
├── _headers
├── app.wasm
├── index.html
├── mime.types
├── otto.go
└── wasm_exec.js
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx
2 | COPY mime.types /etc/nginx/
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PWD = $(shell pwd)
2 |
3 | build:
4 | docker run -v "$(PWD)":/go/src/app nlepage/golang_wasm bash -c "go get -d -v ./... && go build -o app.wasm app && cp /go/app.wasm /go/src/app"
5 |
6 | serve:
7 | docker build -t nginx .
8 | docker run -p 3000:80 -v $(PWD):/usr/share/nginx/html nginx
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # otto-web
2 | This is a website that runs javascript to execute webassembly written in golang that runs a javascript interpreter that calculates fibonacci.
3 |
4 | # load time
5 | On firefox this is almost instant.
6 | On chrome its a good 20 seconds before it loads.
7 |
8 | # demo
9 | [Go see the magic](https://agitated-tesla-9f4163.netlify.com/)
--------------------------------------------------------------------------------
/_headers:
--------------------------------------------------------------------------------
1 | /app.wasm
2 | Content-Type: application/wasm
--------------------------------------------------------------------------------
/app.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trashhalo/otto-web/37db78f8c7a023a3f1a0555c15de6bd8ab2ccde6/app.wasm
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 | Go wasm
12 |
13 |
14 |
15 |
16 |
33 |
34 |
loading...
35 |
Your browser is about to run javascript to execute webassembly written in golang that runs a javascript interpreter.
36 | Yep.
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/mime.types:
--------------------------------------------------------------------------------
1 | types {
2 | text/html html htm shtml;
3 | text/css css;
4 | text/xml xml;
5 | image/gif gif;
6 | image/jpeg jpeg jpg;
7 | application/javascript js;
8 | application/atom+xml atom;
9 | application/rss+xml rss;
10 |
11 | text/mathml mml;
12 | text/plain txt;
13 | text/vnd.sun.j2me.app-descriptor jad;
14 | text/vnd.wap.wml wml;
15 | text/x-component htc;
16 |
17 | image/png png;
18 | image/svg+xml svg svgz;
19 | image/tiff tif tiff;
20 | image/vnd.wap.wbmp wbmp;
21 | image/webp webp;
22 | image/x-icon ico;
23 | image/x-jng jng;
24 | image/x-ms-bmp bmp;
25 |
26 | application/font-woff woff;
27 | application/java-archive jar war ear;
28 | application/json json;
29 | application/mac-binhex40 hqx;
30 | application/msword doc;
31 | application/pdf pdf;
32 | application/postscript ps eps ai;
33 | application/rtf rtf;
34 | application/vnd.apple.mpegurl m3u8;
35 | application/vnd.google-earth.kml+xml kml;
36 | application/vnd.google-earth.kmz kmz;
37 | application/vnd.ms-excel xls;
38 | application/vnd.ms-fontobject eot;
39 | application/vnd.ms-powerpoint ppt;
40 | application/vnd.oasis.opendocument.graphics odg;
41 | application/vnd.oasis.opendocument.presentation odp;
42 | application/vnd.oasis.opendocument.spreadsheet ods;
43 | application/vnd.oasis.opendocument.text odt;
44 | application/vnd.openxmlformats-officedocument.presentationml.presentation
45 | pptx;
46 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
47 | xlsx;
48 | application/vnd.openxmlformats-officedocument.wordprocessingml.document
49 | docx;
50 | application/vnd.wap.wmlc wmlc;
51 | application/x-7z-compressed 7z;
52 | application/x-cocoa cco;
53 | application/x-java-archive-diff jardiff;
54 | application/x-java-jnlp-file jnlp;
55 | application/x-makeself run;
56 | application/x-perl pl pm;
57 | application/x-pilot prc pdb;
58 | application/x-rar-compressed rar;
59 | application/x-redhat-package-manager rpm;
60 | application/x-sea sea;
61 | application/x-shockwave-flash swf;
62 | application/x-stuffit sit;
63 | application/x-tcl tcl tk;
64 | application/x-x509-ca-cert der pem crt;
65 | application/x-xpinstall xpi;
66 | application/xhtml+xml xhtml;
67 | application/xspf+xml xspf;
68 | application/zip zip;
69 |
70 | application/octet-stream bin exe dll;
71 | application/octet-stream deb;
72 | application/octet-stream dmg;
73 | application/octet-stream iso img;
74 | application/octet-stream msi msp msm;
75 |
76 | audio/midi mid midi kar;
77 | audio/mpeg mp3;
78 | audio/ogg ogg;
79 | audio/x-m4a m4a;
80 | audio/x-realaudio ra;
81 |
82 | video/3gpp 3gpp 3gp;
83 | video/mp2t ts;
84 | video/mp4 mp4;
85 | video/mpeg mpeg mpg;
86 | video/quicktime mov;
87 | video/webm webm;
88 | video/x-flv flv;
89 | video/x-m4v m4v;
90 | video/x-mng mng;
91 | video/x-ms-asf asx asf;
92 | video/x-ms-wmv wmv;
93 | video/x-msvideo avi;
94 |
95 | application/wasm wasm;
96 | }
--------------------------------------------------------------------------------
/otto.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/robertkrimen/otto"
6 | "syscall/js"
7 | )
8 |
9 | func main() {
10 | document := js.Global().Get("document")
11 | body := document.Get("body")
12 | textarea := document.Call("createElement", "textarea")
13 | textarea.Call("setAttribute", "style", "width:100%;height:100px")
14 | textarea.Set("value", "function fib(num) {\n if (num <= 1) return 1;\n return fib(num - 1) + fib(num - 2);\n}\nfib(20);")
15 | run := document.Call("createElement", "input")
16 | run.Call("setAttribute", "type", "submit")
17 | run.Call("setAttribute", "value", "Run")
18 | run.Call("setAttribute", "onclick", "runOtto()")
19 |
20 | result := document.Call("createElement", "pre")
21 | js.Global().Set("runOtto", js.NewCallback(func(args []js.Value) {
22 | vm := otto.New()
23 | code := textarea.Get("value").String()
24 | value, err := vm.Run(code)
25 | if err != nil {
26 | result.Set("innerText", fmt.Sprintf("error running javascript\n%v", err))
27 | }
28 | result.Set("innerText", fmt.Sprintf("returned\n%v", value))
29 | }))
30 |
31 | body.Call("appendChild", textarea)
32 | body.Call("appendChild", document.Call("createElement", "br"))
33 | body.Call("appendChild", run)
34 | body.Call("appendChild", document.Call("createElement", "br"))
35 | body.Call("appendChild", result)
36 | select {}
37 | }
38 |
--------------------------------------------------------------------------------
/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 web browser API and Node.js API to a single common API (preferring web standards over Node.js API).
7 | const isNodeJS = typeof process !== "undefined";
8 | if (isNodeJS) {
9 | global.require = require;
10 | global.fs = require("fs");
11 |
12 | const nodeCrypto = require("crypto");
13 | global.crypto = {
14 | getRandomValues(b) {
15 | nodeCrypto.randomFillSync(b);
16 | },
17 | };
18 |
19 | global.performance = {
20 | now() {
21 | const [sec, nsec] = process.hrtime();
22 | return sec * 1000 + nsec / 1000000;
23 | },
24 | };
25 |
26 | const util = require("util");
27 | global.TextEncoder = util.TextEncoder;
28 | global.TextDecoder = util.TextDecoder;
29 | } else {
30 | if (typeof window !== "undefined") {
31 | window.global = window;
32 | } else if (typeof self !== "undefined") {
33 | self.global = self;
34 | } else {
35 | throw new Error("cannot export Go (neither window nor self is defined)");
36 | }
37 |
38 | let outputBuf = "";
39 | global.fs = {
40 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_NONBLOCK: -1, O_SYNC: -1 }, // unused
41 | writeSync(fd, buf) {
42 | outputBuf += decoder.decode(buf);
43 | const nl = outputBuf.lastIndexOf("\n");
44 | if (nl != -1) {
45 | console.log(outputBuf.substr(0, nl));
46 | outputBuf = outputBuf.substr(nl + 1);
47 | }
48 | return buf.length;
49 | },
50 | openSync(path, flags, mode) {
51 | const err = new Error("not implemented");
52 | err.code = "ENOSYS";
53 | throw err;
54 | },
55 | };
56 | }
57 |
58 | const encoder = new TextEncoder("utf-8");
59 | const decoder = new TextDecoder("utf-8");
60 |
61 | global.Go = class {
62 | constructor() {
63 | this.argv = ["js"];
64 | this.env = {};
65 | this.exit = (code) => {
66 | if (code !== 0) {
67 | console.warn("exit code:", code);
68 | }
69 | };
70 | this._callbackTimeouts = new Map();
71 | this._nextCallbackTimeoutID = 1;
72 |
73 | const mem = () => {
74 | // The buffer may change when requesting more memory.
75 | return new DataView(this._inst.exports.mem.buffer);
76 | }
77 |
78 | const setInt64 = (addr, v) => {
79 | mem().setUint32(addr + 0, v, true);
80 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
81 | }
82 |
83 | const getInt64 = (addr) => {
84 | const low = mem().getUint32(addr + 0, true);
85 | const high = mem().getInt32(addr + 4, true);
86 | return low + high * 4294967296;
87 | }
88 |
89 | const loadValue = (addr) => {
90 | const f = mem().getFloat64(addr, true);
91 | if (!isNaN(f)) {
92 | return f;
93 | }
94 |
95 | const id = mem().getUint32(addr, true);
96 | return this._values[id];
97 | }
98 |
99 | const storeValue = (addr, v) => {
100 | const nanHead = 0x7FF80000;
101 |
102 | if (typeof v === "number") {
103 | if (isNaN(v)) {
104 | mem().setUint32(addr + 4, nanHead, true);
105 | mem().setUint32(addr, 0, true);
106 | return;
107 | }
108 | mem().setFloat64(addr, v, true);
109 | return;
110 | }
111 |
112 | switch (v) {
113 | case undefined:
114 | mem().setUint32(addr + 4, nanHead, true);
115 | mem().setUint32(addr, 1, true);
116 | return;
117 | case null:
118 | mem().setUint32(addr + 4, nanHead, true);
119 | mem().setUint32(addr, 2, true);
120 | return;
121 | case true:
122 | mem().setUint32(addr + 4, nanHead, true);
123 | mem().setUint32(addr, 3, true);
124 | return;
125 | case false:
126 | mem().setUint32(addr + 4, nanHead, true);
127 | mem().setUint32(addr, 4, true);
128 | return;
129 | }
130 |
131 | let ref = this._refs.get(v);
132 | if (ref === undefined) {
133 | ref = this._values.length;
134 | this._values.push(v);
135 | this._refs.set(v, ref);
136 | }
137 | let typeFlag = 0;
138 | switch (typeof v) {
139 | case "string":
140 | typeFlag = 1;
141 | break;
142 | case "symbol":
143 | typeFlag = 2;
144 | break;
145 | case "function":
146 | typeFlag = 3;
147 | break;
148 | }
149 | mem().setUint32(addr + 4, nanHead | typeFlag, true);
150 | mem().setUint32(addr, ref, true);
151 | }
152 |
153 | const loadSlice = (addr) => {
154 | const array = getInt64(addr + 0);
155 | const len = getInt64(addr + 8);
156 | return new Uint8Array(this._inst.exports.mem.buffer, array, len);
157 | }
158 |
159 | const loadSliceOfValues = (addr) => {
160 | const array = getInt64(addr + 0);
161 | const len = getInt64(addr + 8);
162 | const a = new Array(len);
163 | for (let i = 0; i < len; i++) {
164 | a[i] = loadValue(array + i * 8);
165 | }
166 | return a;
167 | }
168 |
169 | const loadString = (addr) => {
170 | const saddr = getInt64(addr + 0);
171 | const len = getInt64(addr + 8);
172 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
173 | }
174 |
175 | const timeOrigin = Date.now() - performance.now();
176 | this.importObject = {
177 | go: {
178 | // func wasmExit(code int32)
179 | "runtime.wasmExit": (sp) => {
180 | this.exited = true;
181 | this.exit(mem().getInt32(sp + 8, true));
182 | },
183 |
184 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
185 | "runtime.wasmWrite": (sp) => {
186 | const fd = getInt64(sp + 8);
187 | const p = getInt64(sp + 16);
188 | const n = mem().getInt32(sp + 24, true);
189 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
190 | },
191 |
192 | // func nanotime() int64
193 | "runtime.nanotime": (sp) => {
194 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
195 | },
196 |
197 | // func walltime() (sec int64, nsec int32)
198 | "runtime.walltime": (sp) => {
199 | const msec = (new Date).getTime();
200 | setInt64(sp + 8, msec / 1000);
201 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
202 | },
203 |
204 | // func scheduleCallback(delay int64) int32
205 | "runtime.scheduleCallback": (sp) => {
206 | const id = this._nextCallbackTimeoutID;
207 | this._nextCallbackTimeoutID++;
208 | this._callbackTimeouts.set(id, setTimeout(
209 | () => { this._resolveCallbackPromise(); },
210 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
211 | ));
212 | mem().setInt32(sp + 16, id, true);
213 | },
214 |
215 | // func clearScheduledCallback(id int32)
216 | "runtime.clearScheduledCallback": (sp) => {
217 | const id = mem().getInt32(sp + 8, true);
218 | clearTimeout(this._callbackTimeouts.get(id));
219 | this._callbackTimeouts.delete(id);
220 | },
221 |
222 | // func getRandomData(r []byte)
223 | "runtime.getRandomData": (sp) => {
224 | crypto.getRandomValues(loadSlice(sp + 8));
225 | },
226 |
227 | // func stringVal(value string) ref
228 | "syscall/js.stringVal": (sp) => {
229 | storeValue(sp + 24, loadString(sp + 8));
230 | },
231 |
232 | // func valueGet(v ref, p string) ref
233 | "syscall/js.valueGet": (sp) => {
234 | storeValue(sp + 32, Reflect.get(loadValue(sp + 8), loadString(sp + 16)));
235 | },
236 |
237 | // func valueSet(v ref, p string, x ref)
238 | "syscall/js.valueSet": (sp) => {
239 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
240 | },
241 |
242 | // func valueIndex(v ref, i int) ref
243 | "syscall/js.valueIndex": (sp) => {
244 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
245 | },
246 |
247 | // valueSetIndex(v ref, i int, x ref)
248 | "syscall/js.valueSetIndex": (sp) => {
249 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
250 | },
251 |
252 | // func valueCall(v ref, m string, args []ref) (ref, bool)
253 | "syscall/js.valueCall": (sp) => {
254 | try {
255 | const v = loadValue(sp + 8);
256 | const m = Reflect.get(v, loadString(sp + 16));
257 | const args = loadSliceOfValues(sp + 32);
258 | storeValue(sp + 56, Reflect.apply(m, v, args));
259 | mem().setUint8(sp + 64, 1);
260 | } catch (err) {
261 | storeValue(sp + 56, err);
262 | mem().setUint8(sp + 64, 0);
263 | }
264 | },
265 |
266 | // func valueInvoke(v ref, args []ref) (ref, bool)
267 | "syscall/js.valueInvoke": (sp) => {
268 | try {
269 | const v = loadValue(sp + 8);
270 | const args = loadSliceOfValues(sp + 16);
271 | storeValue(sp + 40, Reflect.apply(v, undefined, args));
272 | mem().setUint8(sp + 48, 1);
273 | } catch (err) {
274 | storeValue(sp + 40, err);
275 | mem().setUint8(sp + 48, 0);
276 | }
277 | },
278 |
279 | // func valueNew(v ref, args []ref) (ref, bool)
280 | "syscall/js.valueNew": (sp) => {
281 | try {
282 | const v = loadValue(sp + 8);
283 | const args = loadSliceOfValues(sp + 16);
284 | storeValue(sp + 40, Reflect.construct(v, args));
285 | mem().setUint8(sp + 48, 1);
286 | } catch (err) {
287 | storeValue(sp + 40, err);
288 | mem().setUint8(sp + 48, 0);
289 | }
290 | },
291 |
292 | // func valueLength(v ref) int
293 | "syscall/js.valueLength": (sp) => {
294 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
295 | },
296 |
297 | // valuePrepareString(v ref) (ref, int)
298 | "syscall/js.valuePrepareString": (sp) => {
299 | const str = encoder.encode(String(loadValue(sp + 8)));
300 | storeValue(sp + 16, str);
301 | setInt64(sp + 24, str.length);
302 | },
303 |
304 | // valueLoadString(v ref, b []byte)
305 | "syscall/js.valueLoadString": (sp) => {
306 | const str = loadValue(sp + 8);
307 | loadSlice(sp + 16).set(str);
308 | },
309 |
310 | // func valueInstanceOf(v ref, t ref) bool
311 | "syscall/js.valueInstanceOf": (sp) => {
312 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
313 | },
314 |
315 | "debug": (value) => {
316 | console.log(value);
317 | },
318 | }
319 | };
320 | }
321 |
322 | async run(instance) {
323 | this._inst = instance;
324 | this._values = [ // TODO: garbage collection
325 | NaN,
326 | undefined,
327 | null,
328 | true,
329 | false,
330 | global,
331 | this._inst.exports.mem,
332 | () => { // resolveCallbackPromise
333 | if (this.exited) {
334 | throw new Error("bad callback: Go program has already exited");
335 | }
336 | setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
337 | },
338 | ];
339 | this._refs = new Map();
340 | this.exited = false;
341 |
342 | const mem = new DataView(this._inst.exports.mem.buffer)
343 |
344 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
345 | let offset = 4096;
346 |
347 | const strPtr = (str) => {
348 | let ptr = offset;
349 | new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0"));
350 | offset += str.length + (8 - (str.length % 8));
351 | return ptr;
352 | };
353 |
354 | const argc = this.argv.length;
355 |
356 | const argvPtrs = [];
357 | this.argv.forEach((arg) => {
358 | argvPtrs.push(strPtr(arg));
359 | });
360 |
361 | const keys = Object.keys(this.env).sort();
362 | argvPtrs.push(keys.length);
363 | keys.forEach((key) => {
364 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
365 | });
366 |
367 | const argv = offset;
368 | argvPtrs.forEach((ptr) => {
369 | mem.setUint32(offset, ptr, true);
370 | mem.setUint32(offset + 4, 0, true);
371 | offset += 8;
372 | });
373 |
374 | while (true) {
375 | const callbackPromise = new Promise((resolve) => {
376 | this._resolveCallbackPromise = resolve;
377 | });
378 | this._inst.exports.run(argc, argv);
379 | if (this.exited) {
380 | break;
381 | }
382 | await callbackPromise;
383 | }
384 | }
385 | }
386 |
387 | if (isNodeJS) {
388 | if (process.argv.length < 3) {
389 | process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n");
390 | process.exit(1);
391 | }
392 |
393 | const go = new Go();
394 | go.argv = process.argv.slice(2);
395 | go.env = process.env;
396 | go.exit = process.exit;
397 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
398 | process.on("exit", () => { // Node.js exits if no callback is pending
399 | if (!go.exited) {
400 | console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!");
401 | process.exit(1);
402 | }
403 | });
404 | return go.run(result.instance);
405 | }).catch((err) => {
406 | console.error(err);
407 | go.exited = true;
408 | process.exit(1);
409 | });
410 | }
411 | })();
--------------------------------------------------------------------------------