Open mengqiy opened 2 years ago
Moving the discussion from slack to GitHub for better visibility and discoverability.
I provided the code I used in case it is usefull.
It'd be great if the maintainer(s) can chime in and provide a guideline about what need to be done to make it working.
interesting idea. Any update?
Moving the discussion from slack to GitHub for better visibility and discoverability.
I provided the code I used in case it is usefull.
go runner code
```golang import ( "encoding/json" "fmt" "io/ioutil" "go.kuoruan.net/v8go-polyfills/fetch" "go.kuoruan.net/v8go-polyfills/console" "go.kuoruan.net/v8go-polyfills/timers" "rogchap.com/v8go" ) func main() { err := run() if err != nil { panic(err) } } func run() error { iso := v8go.NewIsolate() global := v8go.NewObjectTemplate(iso) err := fetch.InjectTo(iso, global) if err != nil { return err } ctx := v8go.NewContext(iso, global) err = console.InjectTo(ctx) if err != nil { return err } glbObj := ctx.Global() fetchVal, err := ctx.RunScript(`fetch("https://github.com/mengqiy/wasm-go-poc/blob/main/formatjson/formatjson.wasm").then(res => res.arrayBuffer())`, "fetch.js") if err != nil { return err } fetchProms, err := fetchVal.AsPromise() if err != nil { return err } for fetchProms.State() == v8go.Pending { continue } wasm := fetchProms.Result() err = glbObj.Set("fnwasm", wasm) if err != nil { return err } _, err = ctx.RunScript(wasmExecJs, "wasm_exec.js") if err != nil { return err } _, err = ctx.RunScript(`const go = new Go();`, "newgo.js") if err != nil { return err } val, err := ctx.RunScript(`WebAssembly.instantiate(fnwasm, go.importObject);`, "instantiatewasm.js") if err != nil { return err } proms, err := val.AsPromise() if err != nil { return err } for proms.State() == v8go.Pending { continue } wasmResult := proms.Result() glbObj.Set("wasmResult", wasmResult) val, err = ctx.RunScript(`go.run(wasmResult.instance);`, "gorun.js") if err != nil { return err } proms, err = val.AsPromise() if err != nil { return err } for proms.State() == v8go.Pending { continue } val, err = ctx.RunScript(`formatJSON('{"foo":"bar"}');`, "formatJSON.js") if err != nil { return err } fmt.Printf("formatted string: %v\n", val.String()) return nil } ```wasmExecJs
```golang const wasmExecJs = ` // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. "use strict"; (() => { const enosys = () => { const err = new Error("not implemented"); err.code = "ENOSYS"; return err; }; if (!globalThis.fs) { let outputBuf = ""; globalThis.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused writeSync(fd, buf) { outputBuf += decoder.decode(buf); const nl = outputBuf.lastIndexOf("\n"); if (nl != -1) { console.log(outputBuf.substr(0, nl)); outputBuf = outputBuf.substr(nl + 1); } return buf.length; }, write(fd, buf, offset, length, position, callback) { if (offset !== 0 || length !== buf.length || position !== null) { callback(enosys()); return; } const n = this.writeSync(fd, buf); callback(null, n); }, chmod(path, mode, callback) { callback(enosys()); }, chown(path, uid, gid, callback) { callback(enosys()); }, close(fd, callback) { callback(enosys()); }, fchmod(fd, mode, callback) { callback(enosys()); }, fchown(fd, uid, gid, callback) { callback(enosys()); }, fstat(fd, callback) { callback(enosys()); }, fsync(fd, callback) { callback(null); }, ftruncate(fd, length, callback) { callback(enosys()); }, lchown(path, uid, gid, callback) { callback(enosys()); }, link(path, link, callback) { callback(enosys()); }, lstat(path, callback) { callback(enosys()); }, mkdir(path, perm, callback) { callback(enosys()); }, open(path, flags, mode, callback) { callback(enosys()); }, read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, readdir(path, callback) { callback(enosys()); }, readlink(path, callback) { callback(enosys()); }, rename(from, to, callback) { callback(enosys()); }, rmdir(path, callback) { callback(enosys()); }, stat(path, callback) { callback(enosys()); }, symlink(path, link, callback) { callback(enosys()); }, truncate(path, length, callback) { callback(enosys()); }, unlink(path, callback) { callback(enosys()); }, utimes(path, atime, mtime, callback) { callback(enosys()); }, }; } if (!globalThis.process) { globalThis.process = { getuid() { return -1; }, getgid() { return -1; }, geteuid() { return -1; }, getegid() { return -1; }, getgroups() { throw enosys(); }, pid: -1, ppid: -1, umask() { throw enosys(); }, cwd() { throw enosys(); }, chdir() { throw enosys(); }, } } /* if (!globalThis.crypto) { throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); } if (!globalThis.performance) { throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); } if (!globalThis.TextEncoder) { throw new Error("globalThis.TextEncoder is not available, polyfill required"); } if (!globalThis.TextDecoder) { throw new Error("globalThis.TextDecoder is not available, polyfill required"); } const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); */ globalThis.Go = class { constructor() { this.argv = ["js"]; this.env = {}; this.exit = (code) => { if (code !== 0) { console.warn("exit code:", code); } }; this._exitPromise = new Promise((resolve) => { this._resolveExitPromise = resolve; }); this._pendingEvent = null; this._scheduledTimeouts = new Map(); this._nextCallbackTimeoutID = 1; const setInt64 = (addr, v) => { this.mem.setUint32(addr + 0, v, true); this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); } const getInt64 = (addr) => { const low = this.mem.getUint32(addr + 0, true); const high = this.mem.getInt32(addr + 4, true); return low + high * 4294967296; } const loadValue = (addr) => { const f = this.mem.getFloat64(addr, true); if (f === 0) { return undefined; } if (!isNaN(f)) { return f; } const id = this.mem.getUint32(addr, true); return this._values[id]; } const storeValue = (addr, v) => { const nanHead = 0x7FF80000; if (typeof v === "number" && v !== 0) { if (isNaN(v)) { this.mem.setUint32(addr + 4, nanHead, true); this.mem.setUint32(addr, 0, true); return; } this.mem.setFloat64(addr, v, true); return; } if (v === undefined) { this.mem.setFloat64(addr, 0, true); return; } let id = this._ids.get(v); if (id === undefined) { id = this._idPool.pop(); if (id === undefined) { id = this._values.length; } this._values[id] = v; this._goRefCounts[id] = 0; this._ids.set(v, id); } this._goRefCounts[id]++; let typeFlag = 0; switch (typeof v) { case "object": if (v !== null) { typeFlag = 1; } break; case "string": typeFlag = 2; break; case "symbol": typeFlag = 3; break; case "function": typeFlag = 4; break; } this.mem.setUint32(addr + 4, nanHead | typeFlag, true); this.mem.setUint32(addr, id, true); } const loadSlice = (addr) => { const array = getInt64(addr + 0); const len = getInt64(addr + 8); return new Uint8Array(this._inst.exports.mem.buffer, array, len); } const loadSliceOfValues = (addr) => { const array = getInt64(addr + 0); const len = getInt64(addr + 8); const a = new Array(len); for (let i = 0; i < len; i++) { a[i] = loadValue(array + i * 8); } return a; } const loadString = (addr) => { const saddr = getInt64(addr + 0); const len = getInt64(addr + 8); return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); } // const timeOrigin = Date.now() - performance.now(); this.importObject = { go: { // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). // This changes the SP, thus we have to update the SP used by the imported function. // func wasmExit(code int32) "runtime.wasmExit": (sp) => { sp >>>= 0; const code = this.mem.getInt32(sp + 8, true); this.exited = true; delete this._inst; delete this._values; delete this._goRefCounts; delete this._ids; delete this._idPool; this.exit(code); }, // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) "runtime.wasmWrite": (sp) => { sp >>>= 0; const fd = getInt64(sp + 8); const p = getInt64(sp + 16); const n = this.mem.getInt32(sp + 24, true); fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); }, // func resetMemoryDataView() "runtime.resetMemoryDataView": (sp) => { sp >>>= 0; this.mem = new DataView(this._inst.exports.mem.buffer); }, // // func nanotime1() int64 // "runtime.nanotime1": (sp) => { // sp >>>= 0; // setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); // }, // func walltime() (sec int64, nsec int32) "runtime.walltime": (sp) => { sp >>>= 0; const msec = (new Date).getTime(); setInt64(sp + 8, msec / 1000); this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); }, // func scheduleTimeoutEvent(delay int64) int32 "runtime.scheduleTimeoutEvent": (sp) => { sp >>>= 0; const id = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++; this._scheduledTimeouts.set(id, setTimeout( () => { this._resume(); while (this._scheduledTimeouts.has(id)) { // for some reason Go failed to register the timeout event, log and try again // (temporary workaround for https://github.com/golang/go/issues/28975) console.warn("scheduleTimeoutEvent: missed timeout event"); this._resume(); } }, getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early )); this.mem.setInt32(sp + 16, id, true); }, // func clearTimeoutEvent(id int32) "runtime.clearTimeoutEvent": (sp) => { sp >>>= 0; const id = this.mem.getInt32(sp + 8, true); clearTimeout(this._scheduledTimeouts.get(id)); this._scheduledTimeouts.delete(id); }, // func getRandomData(r []byte) "runtime.getRandomData": (sp) => { sp >>>= 0; crypto.getRandomValues(loadSlice(sp + 8)); }, // func finalizeRef(v ref) "syscall/js.finalizeRef": (sp) => { sp >>>= 0; const id = this.mem.getUint32(sp + 8, true); this._goRefCounts[id]--; if (this._goRefCounts[id] === 0) { const v = this._values[id]; this._values[id] = null; this._ids.delete(v); this._idPool.push(id); } }, // func stringVal(value string) ref "syscall/js.stringVal": (sp) => { sp >>>= 0; storeValue(sp + 24, loadString(sp + 8)); }, // func valueGet(v ref, p string) ref "syscall/js.valueGet": (sp) => { sp >>>= 0; const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 32, result); }, // func valueSet(v ref, p string, x ref) "syscall/js.valueSet": (sp) => { sp >>>= 0; Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); }, // func valueDelete(v ref, p string) "syscall/js.valueDelete": (sp) => { sp >>>= 0; Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); }, // func valueIndex(v ref, i int) ref "syscall/js.valueIndex": (sp) => { sp >>>= 0; storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); }, // valueSetIndex(v ref, i int, x ref) "syscall/js.valueSetIndex": (sp) => { sp >>>= 0; Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); }, // func valueCall(v ref, m string, args []ref) (ref, bool) "syscall/js.valueCall": (sp) => { sp >>>= 0; try { const v = loadValue(sp + 8); const m = Reflect.get(v, loadString(sp + 16)); const args = loadSliceOfValues(sp + 32); const result = Reflect.apply(m, v, args); sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 56, result); this.mem.setUint8(sp + 64, 1); } catch (err) { sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 56, err); this.mem.setUint8(sp + 64, 0); } }, // func valueInvoke(v ref, args []ref) (ref, bool) "syscall/js.valueInvoke": (sp) => { sp >>>= 0; try { const v = loadValue(sp + 8); const args = loadSliceOfValues(sp + 16); const result = Reflect.apply(v, undefined, args); sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 40, result); this.mem.setUint8(sp + 48, 1); } catch (err) { sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 40, err); this.mem.setUint8(sp + 48, 0); } }, // func valueNew(v ref, args []ref) (ref, bool) "syscall/js.valueNew": (sp) => { sp >>>= 0; try { const v = loadValue(sp + 8); const args = loadSliceOfValues(sp + 16); const result = Reflect.construct(v, args); sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 40, result); this.mem.setUint8(sp + 48, 1); } catch (err) { sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 40, err); this.mem.setUint8(sp + 48, 0); } }, // func valueLength(v ref) int "syscall/js.valueLength": (sp) => { sp >>>= 0; setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); }, // valuePrepareString(v ref) (ref, int) "syscall/js.valuePrepareString": (sp) => { sp >>>= 0; const str = encoder.encode(String(loadValue(sp + 8))); storeValue(sp + 16, str); setInt64(sp + 24, str.length); }, // valueLoadString(v ref, b []byte) "syscall/js.valueLoadString": (sp) => { sp >>>= 0; const str = loadValue(sp + 8); loadSlice(sp + 16).set(str); }, // func valueInstanceOf(v ref, t ref) bool "syscall/js.valueInstanceOf": (sp) => { sp >>>= 0; this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); }, // func copyBytesToGo(dst []byte, src ref) (int, bool) "syscall/js.copyBytesToGo": (sp) => { sp >>>= 0; const dst = loadSlice(sp + 8); const src = loadValue(sp + 32); if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { this.mem.setUint8(sp + 48, 0); return; } const toCopy = src.subarray(0, dst.length); dst.set(toCopy); setInt64(sp + 40, toCopy.length); this.mem.setUint8(sp + 48, 1); }, // func copyBytesToJS(dst ref, src []byte) (int, bool) "syscall/js.copyBytesToJS": (sp) => { sp >>>= 0; const dst = loadValue(sp + 8); const src = loadSlice(sp + 16); if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { this.mem.setUint8(sp + 48, 0); return; } const toCopy = src.subarray(0, dst.length); dst.set(toCopy); setInt64(sp + 40, toCopy.length); this.mem.setUint8(sp + 48, 1); }, "debug": (value) => { console.log(value); }, } }; } async run(instance) { if (!(instance instanceof WebAssembly.Instance)) { throw new Error("Go.run: WebAssembly.Instance expected"); } this._inst = instance; this.mem = new DataView(this._inst.exports.mem.buffer); this._values = [ // JS values that Go currently has references to, indexed by reference id NaN, 0, null, true, false, globalThis, this, ]; this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id this._ids = new Map([ // mapping from JS values to reference ids [0, 1], [null, 2], [true, 3], [false, 4], [globalThis, 5], [this, 6], ]); this._idPool = []; // unused ids that have been garbage collected this.exited = false; // whether the Go program has exited // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. let offset = 4096; const strPtr = (str) => { const ptr = offset; const bytes = encoder.encode(str + "\0"); new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); offset += bytes.length; if (offset % 8 !== 0) { offset += 8 - (offset % 8); } return ptr; }; const argc = this.argv.length; const argvPtrs = []; this.argv.forEach((arg) => { argvPtrs.push(strPtr(arg)); }); argvPtrs.push(0); const keys = Object.keys(this.env).sort(); keys.forEach((key) => { argvPtrs.push(strPtr(`+"`"+`${key}=${this.env[key]}`+"`"+`)); }); argvPtrs.push(0); const argv = offset; argvPtrs.forEach((ptr) => { this.mem.setUint32(offset, ptr, true); this.mem.setUint32(offset + 4, 0, true); offset += 8; }); // The linker guarantees global data starts from at least wasmMinDataAddr. // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. const wasmMinDataAddr = 4096 + 8192; if (offset >= wasmMinDataAddr) { throw new Error("total length of command line and environment variables exceeds limit"); } this._inst.exports.run(argc, argv); if (this.exited) { this._resolveExitPromise(); } await this._exitPromise; } _resume() { if (this.exited) { throw new Error("Go program has already exited"); } this._inst.exports.resume(); if (this.exited) { this._resolveExitPromise(); } } _makeFuncWrapper(id) { const go = this; return function () { const event = { id: id, this: this, args: arguments }; go._pendingEvent = event; go._resume(); return event.result; }; } } })(); ` ```go code to build wasm artifact
Code below mostly comes from https://github.com/golangbot/webassembly/tree/tutorial1/ ```golang import ( "encoding/json" "fmt" "syscall/js" ) func jsonWrapper() js.Func { jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { if len(args) != 1 { return "Invalid no of arguments passed" } inputJSON := args[0].String() fmt.Printf("input %s\n", inputJSON) pretty, err := prettyJson(inputJSON) if err != nil { fmt.Printf("unable to convert to json %s\n", err) return err.Error() } return pretty }) return jsonFunc } func prettyJson(input string) (string, error) { var raw interface{} if err := json.Unmarshal([]byte(input), &raw); err != nil { return "", err } pretty, err := json.MarshalIndent(raw, "", " ") if err != nil { return "", err } return string(pretty), nil } func main() { fmt.Println("Go Web Assembly") js.Global().Set("formatJSON", jsonWrapper()) <-make(chan bool) } ```It'd be great if the maintainer(s) can chime in and provide a guideline about what need to be done to make it working.