From eb1d834dd45e274e935064f5ab363556ee91fd5a Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 24 May 2019 15:30:09 +0200 Subject: [PATCH] wasm: add support for js.FuncOf --- compiler/goroutine-lowering.go | 19 ++++++++++-- src/examples/wasm/Makefile | 5 +++ src/examples/wasm/callback/index.html | 19 ++++++++++++ src/examples/wasm/callback/wasm.go | 27 ++++++++++++++++ src/examples/wasm/callback/wasm.js | 26 ++++++++++++++++ src/runtime/panic.go | 4 +++ src/runtime/runtime_wasm.go | 9 +++++- src/runtime/scheduler.go | 3 ++ targets/wasm_exec.js | 44 +++++++++++++-------------- 9 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 src/examples/wasm/callback/index.html create mode 100644 src/examples/wasm/callback/wasm.go create mode 100644 src/examples/wasm/callback/wasm.js diff --git a/compiler/goroutine-lowering.go b/compiler/goroutine-lowering.go index 2a9276df..c493bf48 100644 --- a/compiler/goroutine-lowering.go +++ b/compiler/goroutine-lowering.go @@ -389,7 +389,20 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { c.builder.SetInsertPointBefore(inst) - parentHandle := f.LastParam() + var parentHandle llvm.Value + if f.Linkage() == llvm.ExternalLinkage { + // Exported function. + // Note that getTaskPromisePtr will panic if it is called with + // a nil pointer, so blocking exported functions that try to + // return anything will not work. + parentHandle = llvm.ConstPointerNull(c.i8ptrType) + } else { + parentHandle = f.LastParam() + if parentHandle.IsNil() || parentHandle.Name() != "parentHandle" { + // sanity check + panic("trying to make exported function async") + } + } // Store return values. switch inst.OperandsCount() { @@ -417,7 +430,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { // behavior somehow (with the unreachable instruction). continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{ llvm.ConstNull(c.ctx.TokenType()), - llvm.ConstInt(c.ctx.Int1Type(), 1, false), + llvm.ConstInt(c.ctx.Int1Type(), 0, false), }, "ret") sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2) sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), frame.unreachableBlock) @@ -488,7 +501,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { c.builder.SetInsertPointBefore(deadlockCall) continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{ llvm.ConstNull(c.ctx.TokenType()), - llvm.ConstInt(c.ctx.Int1Type(), 1, false), // final suspend + llvm.ConstInt(c.ctx.Int1Type(), 0, false), }, "") c.splitBasicBlock(deadlockCall, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.wakeup.dead") c.builder.SetInsertPointBefore(deadlockCall) diff --git a/src/examples/wasm/Makefile b/src/examples/wasm/Makefile index a12bfbb3..78cdaea5 100644 --- a/src/examples/wasm/Makefile +++ b/src/examples/wasm/Makefile @@ -3,6 +3,11 @@ export: clean wasm_exec cp ./export/wasm.js ./html/ cp ./export/index.html ./html/ +callback: clean wasm_exec + tinygo build -o ./html/wasm.wasm -target wasm ./callback/wasm.go + cp ./callback/wasm.js ./html/ + cp ./callback/index.html ./html/ + main: clean wasm_exec tinygo build -o ./html/wasm.wasm -target wasm -no-debug ./main/main.go cp ./main/index.html ./html/ diff --git a/src/examples/wasm/callback/index.html b/src/examples/wasm/callback/index.html new file mode 100644 index 00000000..4cca1962 --- /dev/null +++ b/src/examples/wasm/callback/index.html @@ -0,0 +1,19 @@ + + + + + + + Go WebAssembly + + + + + + +

WebAssembly

+

Add two numbers, using WebAssembly:

+ + = + + + diff --git a/src/examples/wasm/callback/wasm.go b/src/examples/wasm/callback/wasm.go new file mode 100644 index 00000000..da86550b --- /dev/null +++ b/src/examples/wasm/callback/wasm.go @@ -0,0 +1,27 @@ +package main + +import ( + "strconv" + "syscall/js" +) + +var a, b int + +func main() { + document := js.Global().Get("document") + document.Call("getElementById", "a").Set("oninput", updater(&a)) + document.Call("getElementById", "b").Set("oninput", updater(&b)) + update() +} + +func updater(n *int) js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) interface{} { + *n, _ = strconv.Atoi(this.Get("value").String()) + update() + return nil + }) +} + +func update() { + js.Global().Get("document").Call("getElementById", "result").Set("value", a+b) +} diff --git a/src/examples/wasm/callback/wasm.js b/src/examples/wasm/callback/wasm.js new file mode 100644 index 00000000..378cf726 --- /dev/null +++ b/src/examples/wasm/callback/wasm.js @@ -0,0 +1,26 @@ +'use strict'; + +const WASM_URL = 'wasm.wasm'; + +var wasm; + +function init() { + const go = new Go(); + if ('instantiateStreaming' in WebAssembly) { + WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) { + wasm = obj.instance; + go.run(wasm); + }) + } else { + fetch(WASM_URL).then(resp => + resp.arrayBuffer() + ).then(bytes => + WebAssembly.instantiate(bytes, go.importObject).then(function (obj) { + wasm = obj.instance; + go.run(wasm); + }) + ) + } +} + +init(); diff --git a/src/runtime/panic.go b/src/runtime/panic.go index a297795e..e8aafa74 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -49,3 +49,7 @@ func lookupPanic() { func slicePanic() { runtimePanic("slice out of range") } + +func blockingPanic() { + runtimePanic("trying to do blocking operation in exported function") +} diff --git a/src/runtime/runtime_wasm.go b/src/runtime/runtime_wasm.go index e01845a5..bab786cb 100644 --- a/src/runtime/runtime_wasm.go +++ b/src/runtime/runtime_wasm.go @@ -37,9 +37,16 @@ func putchar(c byte) { resource_write(stdout, &c, 1) } +var handleEvent func() + //go:linkname setEventHandler syscall/js.setEventHandler func setEventHandler(fn func()) { - // TODO + handleEvent = fn +} + +//go:export resume +func resume() { + handleEvent() } //go:export go_scheduler diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index b4af2360..9d7bc1de 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -120,6 +120,9 @@ func setTaskPromisePtr(task *coroutine, value unsafe.Pointer) { // getTaskPromisePtr is a helper function to get the current .ptr field from a // coroutine promise. func getTaskPromisePtr(task *coroutine) unsafe.Pointer { + if task == nil { + blockingPanic() + } return task.promise().ptr } diff --git a/targets/wasm_exec.js b/targets/wasm_exec.js index 87522fa9..951b085c 100644 --- a/targets/wasm_exec.js +++ b/targets/wasm_exec.js @@ -240,9 +240,9 @@ }, // func valueIndex(v ref, i int) ref - //"syscall/js.valueIndex": (sp) => { - // storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); - //}, + "syscall/js.valueIndex": (ret_addr, v_addr, i) => { + storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); + }, // valueSetIndex(v ref, i int, x ref) //"syscall/js.valueSetIndex": (sp) => { @@ -291,9 +291,9 @@ }, // func valueLength(v ref) int - //"syscall/js.valueLength": (sp) => { - // setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); - //}, + "syscall/js.valueLength": (v_addr) => { + return loadValue(v_addr).length; + }, // valuePrepareString(v ref) (ref, int) "syscall/js.valuePrepareString": (ret_addr, v_addr) => { @@ -352,25 +352,23 @@ } } - static _makeCallbackHelper(id, pendingCallbacks, go) { - return function () { - pendingCallbacks.push({ id: id, args: arguments }); - go._resolveCallbackPromise(); - }; + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } } - static _makeEventCallbackHelper(preventDefault, stopPropagation, stopImmediatePropagation, fn) { - return function (event) { - if (preventDefault) { - event.preventDefault(); - } - if (stopPropagation) { - event.stopPropagation(); - } - if (stopImmediatePropagation) { - event.stopImmediatePropagation(); - } - fn(event); + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; }; } }