├── 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 | })(); --------------------------------------------------------------------------------