aboutsummaryrefslogtreecommitdiff
path: root/src/syscall
diff options
context:
space:
mode:
authorRichard Musiol <mail@richard-musiol.de>2018-05-20 00:56:36 +0200
committerAustin Clements <austin@google.com>2018-06-14 21:50:53 +0000
commite083dc6307b6593bdd44b219ffd21699d6f17fd7 (patch)
tree2a411d82639a778c6aa107529b1d708034c7a7f1 /src/syscall
parent5fdacfa89f871888d6f8fde726b8f95f11e674d6 (diff)
downloadgo-e083dc6307b6593bdd44b219ffd21699d6f17fd7.tar.xz
runtime, sycall/js: add support for callbacks from JavaScript
This commit adds support for JavaScript callbacks back into WebAssembly. This is experimental API, just like the rest of the syscall/js package. The time package now also uses this mechanism to properly support timers without resorting to a busy loop. JavaScript code can call into the same entry point multiple times. The new RUN register is used to keep track of the program's run state. Possible values are: starting, running, paused and exited. If no goroutine is ready any more, the scheduler can put the program into the "paused" state and the WebAssembly code will stop running. When a callback occurs, the JavaScript code puts the callback data into a queue and then calls into WebAssembly to allow the Go code to continue running. Updates #18892 Updates #25506 Change-Id: Ib8701cfa0536d10d69bd541c85b0e2a754eb54fb Reviewed-on: https://go-review.googlesource.com/114197 Reviewed-by: Austin Clements <austin@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Diffstat (limited to 'src/syscall')
-rw-r--r--src/syscall/js/callback.go145
-rw-r--r--src/syscall/js/js.go6
-rw-r--r--src/syscall/js/js_test.go50
3 files changed, 201 insertions, 0 deletions
diff --git a/src/syscall/js/callback.go b/src/syscall/js/callback.go
new file mode 100644
index 0000000000..2c693240fa
--- /dev/null
+++ b/src/syscall/js/callback.go
@@ -0,0 +1,145 @@
+// 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.
+
+// +build js,wasm
+
+package js
+
+import "sync"
+
+var pendingCallbacks = Global.Get("Array").New()
+
+var makeCallbackHelper = Global.Call("eval", `
+ (function(id, pendingCallbacks, resolveCallbackPromise) {
+ return function() {
+ pendingCallbacks.push({ id: id, args: arguments });
+ resolveCallbackPromise();
+ };
+ })
+`)
+
+var makeEventCallbackHelper = Global.Call("eval", `
+ (function(preventDefault, stopPropagation, stopImmediatePropagation, fn) {
+ return function(event) {
+ if (preventDefault) {
+ event.preventDefault();
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ }
+ if (stopImmediatePropagation) {
+ event.stopImmediatePropagation();
+ }
+ fn(event);
+ };
+ })
+`)
+
+var (
+ callbacksMu sync.Mutex
+ callbacks = make(map[uint32]func([]Value))
+ nextCallbackID uint32 = 1
+)
+
+// Callback is a Go function that got wrapped for use as a JavaScript callback.
+// A Callback can be passed to functions of this package that accept interface{},
+// for example Value.Set and Value.Call.
+type Callback struct {
+ id uint32
+ enqueueFn Value // the JavaScript function that queues the callback for execution
+}
+
+// NewCallback returns a wrapped callback function. It can be passed to functions of this package
+// that accept interface{}, for example Value.Set and Value.Call.
+//
+// Invoking the callback in JavaScript will queue the Go function fn for execution.
+// This execution happens asynchronously on a special goroutine that handles all callbacks and preserves
+// the order in which the callbacks got called.
+// As a consequence, if one callback blocks this goroutine, other callbacks will not be processed.
+// A blocking callback should therefore explicitly start a new goroutine.
+//
+// Callback.Close must be called to free up resources when the callback will not be used any more.
+func NewCallback(fn func(args []Value)) Callback {
+ callbackLoopOnce.Do(func() {
+ go callbackLoop()
+ })
+
+ callbacksMu.Lock()
+ id := nextCallbackID
+ nextCallbackID++
+ callbacks[id] = fn
+ callbacksMu.Unlock()
+ return Callback{
+ id: id,
+ enqueueFn: makeCallbackHelper.Invoke(id, pendingCallbacks, resolveCallbackPromise),
+ }
+}
+
+type EventCallbackFlag int
+
+const (
+ // PreventDefault can be used with NewEventCallback to call event.preventDefault synchronously.
+ PreventDefault EventCallbackFlag = 1 << iota
+ // StopPropagation can be used with NewEventCallback to call event.stopPropagation synchronously.
+ StopPropagation
+ // StopImmediatePropagation can be used with NewEventCallback to call event.stopImmediatePropagation synchronously.
+ StopImmediatePropagation
+)
+
+// NewEventCallback returns a wrapped callback function, just like NewCallback, but the callback expects to have
+// exactly one argument, the event. Depending on flags, it will synchronously call event.preventDefault,
+// event.stopPropagation and/or event.stopImmediatePropagation before queuing the Go function fn for execution.
+func NewEventCallback(flags EventCallbackFlag, fn func(event Value)) Callback {
+ c := NewCallback(func(args []Value) {
+ fn(args[0])
+ })
+ return Callback{
+ id: c.id,
+ enqueueFn: makeEventCallbackHelper.Invoke(
+ flags&PreventDefault != 0,
+ flags&StopPropagation != 0,
+ flags&StopImmediatePropagation != 0,
+ c,
+ ),
+ }
+}
+
+func (c Callback) Close() {
+ callbacksMu.Lock()
+ delete(callbacks, c.id)
+ callbacksMu.Unlock()
+}
+
+var callbackLoopOnce sync.Once
+
+func callbackLoop() {
+ for {
+ sleepUntilCallback()
+ for {
+ cb := pendingCallbacks.Call("shift")
+ if cb == Undefined {
+ break
+ }
+
+ id := uint32(cb.Get("id").Int())
+ callbacksMu.Lock()
+ f, ok := callbacks[id]
+ callbacksMu.Unlock()
+ if !ok {
+ Global.Get("console").Call("error", "call to closed callback")
+ continue
+ }
+
+ argsObj := cb.Get("args")
+ args := make([]Value, argsObj.Length())
+ for i := range args {
+ args[i] = argsObj.Index(i)
+ }
+ f(args)
+ }
+ }
+}
+
+// sleepUntilCallback is defined in the runtime package
+func sleepUntilCallback()
diff --git a/src/syscall/js/js.go b/src/syscall/js/js.go
index 9332a26254..fdb58b2efa 100644
--- a/src/syscall/js/js.go
+++ b/src/syscall/js/js.go
@@ -39,7 +39,11 @@ var (
// Global is the JavaScript global object, usually "window" or "global".
Global = Value{2}
+ // memory is the WebAssembly linear memory.
memory = Value{3}
+
+ // resolveCallbackPromise is a function that the callback helper uses to resume the execution of Go's WebAssembly code.
+ resolveCallbackPromise = Value{4}
)
var uint8Array = Global.Get("Uint8Array")
@@ -49,6 +53,8 @@ func ValueOf(x interface{}) Value {
switch x := x.(type) {
case Value:
return x
+ case Callback:
+ return x.enqueueFn
case nil:
return Null
case bool:
diff --git a/src/syscall/js/js_test.go b/src/syscall/js/js_test.go
index ca065e321d..7d5b1a238a 100644
--- a/src/syscall/js/js_test.go
+++ b/src/syscall/js/js_test.go
@@ -7,6 +7,7 @@
package js_test
import (
+ "fmt"
"syscall/js"
"testing"
)
@@ -144,3 +145,52 @@ func TestNew(t *testing.T) {
t.Errorf("got %#v, want %#v", got, 42)
}
}
+
+func TestCallback(t *testing.T) {
+ c := make(chan struct{})
+ cb := js.NewCallback(func(args []js.Value) {
+ if got := args[0].Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+ c <- struct{}{}
+ })
+ defer cb.Close()
+ js.Global.Call("setTimeout", cb, 0, 42)
+ <-c
+}
+
+func TestEventCallback(t *testing.T) {
+ for _, name := range []string{"preventDefault", "stopPropagation", "stopImmediatePropagation"} {
+ c := make(chan struct{})
+ var flags js.EventCallbackFlag
+ switch name {
+ case "preventDefault":
+ flags = js.PreventDefault
+ case "stopPropagation":
+ flags = js.StopPropagation
+ case "stopImmediatePropagation":
+ flags = js.StopImmediatePropagation
+ }
+ cb := js.NewEventCallback(flags, func(event js.Value) {
+ c <- struct{}{}
+ })
+ defer cb.Close()
+
+ event := js.Global.Call("eval", fmt.Sprintf("({ called: false, %s: function() { this.called = true; } })", name))
+ js.ValueOf(cb).Invoke(event)
+ if !event.Get("called").Bool() {
+ t.Errorf("%s not called", name)
+ }
+
+ <-c
+ }
+}
+
+func ExampleNewCallback() {
+ var cb js.Callback
+ cb = js.NewCallback(func(args []js.Value) {
+ fmt.Println("button clicked")
+ cb.Close() // close the callback if the button will not be clicked again
+ })
+ js.Global.Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb)
+}