aboutsummaryrefslogtreecommitdiff
path: root/src/syscall
diff options
context:
space:
mode:
authorRichard Musiol <mail@richard-musiol.de>2018-10-11 12:46:14 +0200
committerRichard Musiol <neelance@gmail.com>2018-11-10 11:57:17 +0000
commit6dd70fc5e391eb7a47be5eb6353107f38b73f161 (patch)
treee5ba2aa9f1dcaa4ab396417129b0a04c27b8d0e9 /src/syscall
parente3e043bea4d7547edf004a9e202f66a4d69b5899 (diff)
downloadgo-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.go5
-rw-r--r--src/syscall/js/callback.go116
-rw-r--r--src/syscall/js/js_test.go44
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)
}