diff options
| author | Richard Musiol <mail@richard-musiol.de> | 2018-10-11 12:46:14 +0200 |
|---|---|---|
| committer | Richard Musiol <neelance@gmail.com> | 2018-11-10 11:57:17 +0000 |
| commit | 6dd70fc5e391eb7a47be5eb6353107f38b73f161 (patch) | |
| tree | e5ba2aa9f1dcaa4ab396417129b0a04c27b8d0e9 /src/syscall | |
| parent | e3e043bea4d7547edf004a9e202f66a4d69b5899 (diff) | |
| download | go-6dd70fc5e391eb7a47be5eb6353107f38b73f161.tar.xz | |
all: add support for synchronous callbacks to js/wasm
With this change, callbacks returned by syscall/js.NewCallback
get executed synchronously. This is necessary for the APIs of
many JavaScript libraries.
A callback triggered during a call from Go to JavaScript gets executed
on the same goroutine. A callback triggered by JavaScript's event loop
gets executed on an extra goroutine.
Fixes #26045
Fixes #27441
Change-Id: I591b9e85ab851cef0c746c18eba95fb02ea9e85b
Reviewed-on: https://go-review.googlesource.com/c/142004
Reviewed-by: Cherry Zhang <cherryyz@google.com>
Run-TryBot: Cherry Zhang <cherryyz@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Diffstat (limited to 'src/syscall')
| -rw-r--r-- | src/syscall/fs_js.go | 5 | ||||
| -rw-r--r-- | src/syscall/js/callback.go | 116 | ||||
| -rw-r--r-- | src/syscall/js/js_test.go | 44 |
3 files changed, 64 insertions, 101 deletions
diff --git a/src/syscall/fs_js.go b/src/syscall/fs_js.go index 22a055a040..58d8216f21 100644 --- a/src/syscall/fs_js.go +++ b/src/syscall/fs_js.go @@ -473,8 +473,8 @@ func fsCall(name string, args ...interface{}) (js.Value, error) { err error } - c := make(chan callResult) - jsFS.Call(name, append(args, js.NewCallback(func(args []js.Value) { + c := make(chan callResult, 1) + jsFS.Call(name, append(args, js.NewCallback(func(this js.Value, args []js.Value) interface{} { var res callResult if len(args) >= 1 { // on Node.js 8, fs.utimes calls the callback without any arguments @@ -489,6 +489,7 @@ func fsCall(name string, args ...interface{}) (js.Value, error) { } c <- res + return nil }))...) res := <-c return res.val, res.err diff --git a/src/syscall/js/callback.go b/src/syscall/js/callback.go index 2801e00b68..7f6540908d 100644 --- a/src/syscall/js/callback.go +++ b/src/syscall/js/callback.go @@ -9,14 +9,8 @@ package js import "sync" var ( - pendingCallbacks = Global().Get("Array").New() - makeCallbackHelper = Global().Get("Go").Get("_makeCallbackHelper") - makeEventCallbackHelper = Global().Get("Go").Get("_makeEventCallbackHelper") -) - -var ( callbacksMu sync.Mutex - callbacks = make(map[uint32]func([]Value)) + callbacks = make(map[uint32]func(Value, []Value) interface{}) nextCallbackID uint32 = 1 ) @@ -24,61 +18,32 @@ var _ Wrapper = Callback{} // Callback must implement Wrapper // Callback is a Go function that got wrapped for use as a JavaScript callback. type Callback struct { - Value // the JavaScript function that queues the callback for execution + Value // the JavaScript function that invokes the Go function id uint32 } // NewCallback returns a wrapped callback function. // -// 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. +// Invoking the callback in JavaScript will synchronously call the Go function fn with the value of JavaScript's +// "this" keyword and the arguments of the invocation. +// The return value of the invocation is the result of the Go function mapped back to JavaScript according to ValueOf. +// +// A callback triggered during a call from Go to JavaScript gets executed on the same goroutine. +// A callback triggered by JavaScript's event loop gets executed on an extra goroutine. +// Blocking operations in the callback will block the event loop. +// As a consequence, if one callback blocks, other callbacks will not be processed. // A blocking callback should therefore explicitly start a new goroutine. // // Callback.Release 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() - }) - +func NewCallback(fn func(this Value, args []Value) interface{}) Callback { callbacksMu.Lock() id := nextCallbackID nextCallbackID++ callbacks[id] = fn callbacksMu.Unlock() return Callback{ - Value: makeCallbackHelper.Invoke(id, pendingCallbacks, jsGo), id: id, - } -} - -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{ - Value: makeEventCallbackHelper.Invoke( - flags&PreventDefault != 0, - flags&StopPropagation != 0, - flags&StopImmediatePropagation != 0, - c, - ), - id: c.id, + Value: jsGo.Call("_makeCallbackHelper", id), } } @@ -90,35 +55,38 @@ func (c Callback) Release() { callbacksMu.Unlock() } -var callbackLoopOnce sync.Once +// setCallbackHandler is defined in the runtime package. +func setCallbackHandler(fn func()) -func callbackLoop() { - for !jsGo.Get("_callbackShutdown").Bool() { - sleepUntilCallback() - for { - cb := pendingCallbacks.Call("shift") - if cb == Undefined() { - break - } +func init() { + setCallbackHandler(handleCallback) +} - 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 - } +func handleCallback() { + cb := jsGo.Get("_pendingCallback") + if cb == Null() { + return + } + jsGo.Set("_pendingCallback", Null()) - argsObj := cb.Get("args") - args := make([]Value, argsObj.Length()) - for i := range args { - args[i] = argsObj.Index(i) - } - f(args) - } + id := uint32(cb.Get("id").Int()) + if id == 0 { // zero indicates deadlock + select {} + } + callbacksMu.Lock() + f, ok := callbacks[id] + callbacksMu.Unlock() + if !ok { + Global().Get("console").Call("error", "call to closed callback") + return } -} -// sleepUntilCallback is defined in the runtime package -func sleepUntilCallback() + this := cb.Get("this") + argsObj := cb.Get("args") + args := make([]Value, argsObj.Length()) + for i := range args { + args[i] = argsObj.Index(i) + } + result := f(this, args) + cb.Set("result", result) +} diff --git a/src/syscall/js/js_test.go b/src/syscall/js/js_test.go index 73d112a2e8..b4d2e66faf 100644 --- a/src/syscall/js/js_test.go +++ b/src/syscall/js/js_test.go @@ -302,49 +302,43 @@ func TestZeroValue(t *testing.T) { func TestCallback(t *testing.T) { c := make(chan struct{}) - cb := js.NewCallback(func(args []js.Value) { + cb := js.NewCallback(func(this js.Value, args []js.Value) interface{} { if got := args[0].Int(); got != 42 { t.Errorf("got %#v, want %#v", got, 42) } c <- struct{}{} + return nil }) defer cb.Release() 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{}{} +func TestInvokeCallback(t *testing.T) { + called := false + cb := js.NewCallback(func(this js.Value, args []js.Value) interface{} { + cb2 := js.NewCallback(func(this js.Value, args []js.Value) interface{} { + called = true + return 42 }) - defer cb.Release() - - event := js.Global().Call("eval", fmt.Sprintf("({ called: false, %s: function() { this.called = true; } })", name)) - cb.Invoke(event) - if !event.Get("called").Bool() { - t.Errorf("%s not called", name) - } - - <-c + defer cb2.Release() + return cb2.Invoke() + }) + defer cb.Release() + if got := cb.Invoke().Int(); got != 42 { + t.Errorf("got %#v, want %#v", got, 42) + } + if !called { + t.Error("callback not called") } } func ExampleNewCallback() { var cb js.Callback - cb = js.NewCallback(func(args []js.Value) { + cb = js.NewCallback(func(this js.Value, args []js.Value) interface{} { fmt.Println("button clicked") cb.Release() // release the callback if the button will not be clicked again + return nil }) js.Global().Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb) } |
