aboutsummaryrefslogtreecommitdiff
path: root/src
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
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')
-rw-r--r--src/cmd/internal/obj/wasm/a.out.go3
-rw-r--r--src/cmd/internal/obj/wasm/anames.go1
-rw-r--r--src/cmd/internal/obj/wasm/wasmobj.go16
-rw-r--r--src/cmd/link/internal/wasm/asm.go1
-rw-r--r--src/cmd/trace/annotations.go4
-rw-r--r--src/cmd/trace/annotations_test.go6
-rw-r--r--src/cmd/trace/trace_test.go2
-rw-r--r--src/go/build/deps_test.go2
-rw-r--r--src/runtime/lock_futex.go6
-rw-r--r--src/runtime/lock_js.go125
-rw-r--r--src/runtime/lock_sema.go6
-rw-r--r--src/runtime/proc.go12
-rw-r--r--src/runtime/rt0_js_wasm.s78
-rw-r--r--src/runtime/sys_wasm.s15
-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
17 files changed, 431 insertions, 47 deletions
diff --git a/src/cmd/internal/obj/wasm/a.out.go b/src/cmd/internal/obj/wasm/a.out.go
index 9c04be2609..6f882215ff 100644
--- a/src/cmd/internal/obj/wasm/a.out.go
+++ b/src/cmd/internal/obj/wasm/a.out.go
@@ -219,6 +219,8 @@ const (
// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call.
ACALLNORESUME
+ ARETUNWIND
+
AMOVB
AMOVH
AMOVW
@@ -244,6 +246,7 @@ const (
REG_RET1
REG_RET2
REG_RET3
+ REG_RUN
// locals
REG_R0
diff --git a/src/cmd/internal/obj/wasm/anames.go b/src/cmd/internal/obj/wasm/anames.go
index 20d04446d0..745f0d773a 100644
--- a/src/cmd/internal/obj/wasm/anames.go
+++ b/src/cmd/internal/obj/wasm/anames.go
@@ -180,6 +180,7 @@ var Anames = []string{
"F64ReinterpretI64",
"RESUMEPOINT",
"CALLNORESUME",
+ "RETUNWIND",
"MOVB",
"MOVH",
"MOVW",
diff --git a/src/cmd/internal/obj/wasm/wasmobj.go b/src/cmd/internal/obj/wasm/wasmobj.go
index ca09b3fa0b..8498b40724 100644
--- a/src/cmd/internal/obj/wasm/wasmobj.go
+++ b/src/cmd/internal/obj/wasm/wasmobj.go
@@ -25,6 +25,7 @@ var Register = map[string]int16{
"RET1": REG_RET1,
"RET2": REG_RET2,
"RET3": REG_RET3,
+ "RUN": REG_RUN,
"R0": REG_R0,
"R1": REG_R1,
@@ -487,7 +488,7 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
p = appendp(p, AEnd) // end of Loop
}
- case obj.ARET:
+ case obj.ARET, ARETUNWIND:
ret := *p
p.As = obj.ANOP
@@ -528,7 +529,14 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
p = appendp(p, AI32Add)
p = appendp(p, ASet, regAddr(REG_SP))
- // not switching goroutine, return 0
+ if ret.As == ARETUNWIND {
+ // function needs to unwind the WebAssembly stack, return 1
+ p = appendp(p, AI32Const, constAddr(1))
+ p = appendp(p, AReturn)
+ break
+ }
+
+ // not unwinding the WebAssembly stack, return 0
p = appendp(p, AI32Const, constAddr(0))
p = appendp(p, AReturn)
}
@@ -726,7 +734,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
}
reg := p.From.Reg
switch {
- case reg >= REG_PC_F && reg <= REG_RET3:
+ case reg >= REG_PC_F && reg <= REG_RUN:
w.WriteByte(0x23) // get_global
writeUleb128(w, uint64(reg-REG_PC_F))
case reg >= REG_R0 && reg <= REG_F15:
@@ -743,7 +751,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
}
reg := p.To.Reg
switch {
- case reg >= REG_PC_F && reg <= REG_RET3:
+ case reg >= REG_PC_F && reg <= REG_RUN:
w.WriteByte(0x24) // set_global
writeUleb128(w, uint64(reg-REG_PC_F))
case reg >= REG_R0 && reg <= REG_F15:
diff --git a/src/cmd/link/internal/wasm/asm.go b/src/cmd/link/internal/wasm/asm.go
index aadb0c3b6e..b7beaa5d2f 100644
--- a/src/cmd/link/internal/wasm/asm.go
+++ b/src/cmd/link/internal/wasm/asm.go
@@ -304,6 +304,7 @@ func writeGlobalSec(ctxt *ld.Link) {
I64, // 6: RET1
I64, // 7: RET2
I64, // 8: RET3
+ I32, // 9: RUN
}
writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals
diff --git a/src/cmd/trace/annotations.go b/src/cmd/trace/annotations.go
index c91f18ef6f..96c109e0f2 100644
--- a/src/cmd/trace/annotations.go
+++ b/src/cmd/trace/annotations.go
@@ -1,3 +1,7 @@
+// 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.
+
package main
import (
diff --git a/src/cmd/trace/annotations_test.go b/src/cmd/trace/annotations_test.go
index 5d2b226b35..a9068d53c1 100644
--- a/src/cmd/trace/annotations_test.go
+++ b/src/cmd/trace/annotations_test.go
@@ -1,3 +1,9 @@
+// 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
+
package main
import (
diff --git a/src/cmd/trace/trace_test.go b/src/cmd/trace/trace_test.go
index 852d745b29..9e90f50d4b 100644
--- a/src/cmd/trace/trace_test.go
+++ b/src/cmd/trace/trace_test.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// +build !js
+
package main
import (
diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go
index 9d667b6107..663d5246f8 100644
--- a/src/go/build/deps_test.go
+++ b/src/go/build/deps_test.go
@@ -139,7 +139,7 @@ var pkgDeps = map[string][]string{
// Operating system access.
"syscall": {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"},
- "syscall/js": {"unsafe"},
+ "syscall/js": {"L0"},
"internal/syscall/unix": {"L0", "syscall"},
"internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},
diff --git a/src/runtime/lock_futex.go b/src/runtime/lock_futex.go
index 18dd4629a0..b590c4b92b 100644
--- a/src/runtime/lock_futex.go
+++ b/src/runtime/lock_futex.go
@@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}
+
+func pauseSchedulerUntilCallback() bool {
+ return false
+}
+
+func checkTimeouts() {}
diff --git a/src/runtime/lock_js.go b/src/runtime/lock_js.go
index 21e53d075e..df321e5196 100644
--- a/src/runtime/lock_js.go
+++ b/src/runtime/lock_js.go
@@ -6,14 +6,22 @@
package runtime
+import (
+ _ "unsafe"
+)
+
// js/wasm has no support for threads yet. There is no preemption.
-// Waiting for a mutex or timeout is implemented as a busy loop
-// while allowing other goroutines to run.
+// Waiting for a mutex is implemented by allowing other goroutines
+// to run until the mutex gets unlocked.
const (
mutex_unlocked = 0
mutex_locked = 1
+ note_cleared = 0
+ note_woken = 1
+ note_timeout = 2
+
active_spin = 4
active_spin_cnt = 30
passive_spin = 1
@@ -21,7 +29,7 @@ const (
func lock(l *mutex) {
for l.key == mutex_locked {
- Gosched()
+ mcall(gosched_m)
}
l.key = mutex_locked
}
@@ -34,16 +42,31 @@ func unlock(l *mutex) {
}
// One-time notifications.
+
+type noteWithTimeout struct {
+ gp *g
+ deadline int64
+}
+
+var (
+ notes = make(map[*note]*g)
+ notesWithTimeout = make(map[*note]noteWithTimeout)
+)
+
func noteclear(n *note) {
- n.key = 0
+ n.key = note_cleared
}
func notewakeup(n *note) {
- if n.key != 0 {
- print("notewakeup - double wakeup (", n.key, ")\n")
+ // gp := getg()
+ if n.key == note_woken {
throw("notewakeup - double wakeup")
}
- n.key = 1
+ cleared := n.key == note_cleared
+ n.key = note_woken
+ if cleared {
+ goready(notes[n], 1)
+ }
}
func notesleep(n *note) {
@@ -62,14 +85,88 @@ func notetsleepg(n *note, ns int64) bool {
throw("notetsleepg on g0")
}
- deadline := nanotime() + ns
- for {
- if n.key != 0 {
- return true
+ if ns >= 0 {
+ deadline := nanotime() + ns
+ delay := ns/1000000 + 1 // round up
+ if delay > 1<<31-1 {
+ delay = 1<<31 - 1 // cap to max int32
}
- Gosched()
- if ns >= 0 && nanotime() >= deadline {
- return false
+
+ id := scheduleCallback(delay)
+ mp := acquirem()
+ notes[n] = gp
+ notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline}
+ releasem(mp)
+
+ gopark(nil, nil, waitReasonSleep, traceEvNone, 1)
+
+ clearScheduledCallback(id) // note might have woken early, clear timeout
+ mp = acquirem()
+ delete(notes, n)
+ delete(notesWithTimeout, n)
+ releasem(mp)
+
+ return n.key == note_woken
+ }
+
+ for n.key != note_woken {
+ mp := acquirem()
+ notes[n] = gp
+ releasem(mp)
+
+ gopark(nil, nil, waitReasonZero, traceEvNone, 1)
+
+ mp = acquirem()
+ delete(notes, n)
+ releasem(mp)
+ }
+ return true
+}
+
+// checkTimeouts resumes goroutines that are waiting on a note which has reached its deadline.
+func checkTimeouts() {
+ now := nanotime()
+ for n, nt := range notesWithTimeout {
+ if n.key == note_cleared && now > nt.deadline {
+ n.key = note_timeout
+ goready(nt.gp, 1)
}
}
}
+
+var waitingForCallback *g
+
+// sleepUntilCallback puts the current goroutine to sleep until a callback is triggered.
+// It is currently only used by the callback routine of the syscall/js package.
+//go:linkname sleepUntilCallback syscall/js.sleepUntilCallback
+func sleepUntilCallback() {
+ waitingForCallback = getg()
+ gopark(nil, nil, waitReasonZero, traceEvNone, 1)
+ waitingForCallback = nil
+}
+
+// pauseSchedulerUntilCallback gets called from the scheduler and pauses the execution
+// of Go's WebAssembly code until a callback is triggered. Then it checks for note timeouts
+// and resumes goroutines that are waiting for a callback.
+func pauseSchedulerUntilCallback() bool {
+ if waitingForCallback == nil && len(notesWithTimeout) == 0 {
+ return false
+ }
+
+ pause()
+ checkTimeouts()
+ if waitingForCallback != nil {
+ goready(waitingForCallback, 1)
+ }
+ return true
+}
+
+// pause pauses the execution of Go's WebAssembly code until a callback is triggered.
+func pause()
+
+// scheduleCallback tells the WebAssembly environment to trigger a callback after ms milliseconds.
+// It returns a timer id that can be used with clearScheduledCallback.
+func scheduleCallback(ms int64) int32
+
+// clearScheduledCallback clears a callback scheduled by scheduleCallback.
+func clearScheduledCallback(id int32)
diff --git a/src/runtime/lock_sema.go b/src/runtime/lock_sema.go
index 4cb0e84db3..6e01d70f75 100644
--- a/src/runtime/lock_sema.go
+++ b/src/runtime/lock_sema.go
@@ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}
+
+func pauseSchedulerUntilCallback() bool {
+ return false
+}
+
+func checkTimeouts() {}
diff --git a/src/runtime/proc.go b/src/runtime/proc.go
index e3549d367a..36c74a1e8c 100644
--- a/src/runtime/proc.go
+++ b/src/runtime/proc.go
@@ -263,6 +263,7 @@ func forcegchelper() {
// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
+ checkTimeouts()
mcall(gosched_m)
}
@@ -282,6 +283,9 @@ func goschedguarded() {
// Reasons should be unique and descriptive.
// Do not re-use reasons, add new ones.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
+ if reason != waitReasonSleep {
+ checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
+ }
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
@@ -2361,6 +2365,14 @@ stop:
return gp, false
}
+ // wasm only:
+ // Check if a goroutine is waiting for a callback from the WebAssembly host.
+ // If yes, pause the execution until a callback was triggered.
+ if pauseSchedulerUntilCallback() {
+ // A callback was triggered and caused at least one goroutine to wake up.
+ goto top
+ }
+
// Before we drop our P, make a snapshot of the allp slice,
// which can change underfoot once we no longer block
// safe-points. We don't need to snapshot the contents because
diff --git a/src/runtime/rt0_js_wasm.s b/src/runtime/rt0_js_wasm.s
index 2a878d990c..e20f623610 100644
--- a/src/runtime/rt0_js_wasm.s
+++ b/src/runtime/rt0_js_wasm.s
@@ -5,45 +5,81 @@
#include "go_asm.h"
#include "textflag.h"
+// The register RUN indicates the current run state of the program.
+// Possible values are:
+#define RUN_STARTING 0
+#define RUN_RUNNING 1
+#define RUN_PAUSED 2
+#define RUN_EXITED 3
+
// _rt0_wasm_js does NOT follow the Go ABI. It has two WebAssembly parameters:
// R0: argc (i32)
// R1: argv (i32)
TEXT _rt0_wasm_js(SB),NOSPLIT,$0
- MOVD $runtime·wasmStack+m0Stack__size(SB), SP
+ Get RUN
+ I32Const $RUN_STARTING
+ I32Eq
+ If
+ MOVD $runtime·wasmStack+m0Stack__size(SB), SP
+
+ Get SP
+ Get R0 // argc
+ I64ExtendUI32
+ I64Store $0
- Get SP
- Get R0 // argc
- I64ExtendUI32
- I64Store $0
+ Get SP
+ Get R1 // argv
+ I64ExtendUI32
+ I64Store $8
- Get SP
- Get R1 // argv
- I64ExtendUI32
- I64Store $8
+ I32Const $runtime·rt0_go(SB)
+ I32Const $16
+ I32ShrU
+ Set PC_F
- I32Const $runtime·rt0_go(SB)
- I32Const $16
- I32ShrU
- Set PC_F
+ I32Const $RUN_RUNNING
+ Set RUN
+ Else
+ Get RUN
+ I32Const $RUN_PAUSED
+ I32Eq
+ If
+ I32Const $RUN_RUNNING
+ Set RUN
+ Else
+ Unreachable
+ End
+ End
-// Call the function for the current PC_F. Repeat until SP=0 indicates program end.
+// Call the function for the current PC_F. Repeat until RUN != 0 indicates pause or exit.
// The WebAssembly stack may unwind, e.g. when switching goroutines.
// The Go stack on the linear memory is then used to jump to the correct functions
// with this loop, without having to restore the full WebAssembly stack.
loop:
Loop
- Get SP
- I32Eqz
- If
- Return
- End
-
Get PC_F
CallIndirect $0
Drop
- Br loop
+ Get RUN
+ I32Const $RUN_RUNNING
+ I32Eq
+ BrIf loop
End
+ Return
+
+TEXT runtime·pause(SB), NOSPLIT, $0
+ I32Const $RUN_PAUSED
+ Set RUN
+ RETUNWIND
+
+TEXT runtime·exit(SB), NOSPLIT, $0-8
+ Call runtime·wasmExit(SB)
+ Drop
+ I32Const $RUN_EXITED
+ Set RUN
+ RETUNWIND
+
TEXT _rt0_wasm_js_lib(SB),NOSPLIT,$0
UNDEF
diff --git a/src/runtime/sys_wasm.s b/src/runtime/sys_wasm.s
index 9a67ceec63..3ca844a4c7 100644
--- a/src/runtime/sys_wasm.s
+++ b/src/runtime/sys_wasm.s
@@ -149,13 +149,6 @@ TEXT runtime·wasmTruncU(SB), NOSPLIT, $0-0
I64TruncUF64
Return
-TEXT runtime·exit(SB), NOSPLIT, $0-8
- Call runtime·wasmExit(SB)
- Drop
- I32Const $0
- Set SP
- I32Const $1
-
TEXT runtime·exitThread(SB), NOSPLIT, $0-0
UNDEF
@@ -194,6 +187,14 @@ TEXT ·walltime(SB), NOSPLIT, $0
CallImport
RET
+TEXT ·scheduleCallback(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·clearScheduledCallback(SB), NOSPLIT, $0
+ CallImport
+ RET
+
TEXT ·getRandomData(SB), NOSPLIT, $0
CallImport
RET
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)
+}