diff options
| author | Richard Musiol <mail@richard-musiol.de> | 2018-05-20 00:56:36 +0200 |
|---|---|---|
| committer | Austin Clements <austin@google.com> | 2018-06-14 21:50:53 +0000 |
| commit | e083dc6307b6593bdd44b219ffd21699d6f17fd7 (patch) | |
| tree | 2a411d82639a778c6aa107529b1d708034c7a7f1 /src/syscall | |
| parent | 5fdacfa89f871888d6f8fde726b8f95f11e674d6 (diff) | |
| download | go-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.go | 145 | ||||
| -rw-r--r-- | src/syscall/js/js.go | 6 | ||||
| -rw-r--r-- | src/syscall/js/js_test.go | 50 |
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) +} |
