From e083dc6307b6593bdd44b219ffd21699d6f17fd7 Mon Sep 17 00:00:00 2001 From: Richard Musiol Date: Sun, 20 May 2018 00:56:36 +0200 Subject: [PATCH] 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 Reviewed-by: Brad Fitzpatrick --- misc/wasm/wasm_exec.js | 53 +++++++++- src/cmd/internal/obj/wasm/a.out.go | 3 + src/cmd/internal/obj/wasm/anames.go | 1 + src/cmd/internal/obj/wasm/wasmobj.go | 16 ++- src/cmd/link/internal/wasm/asm.go | 1 + src/cmd/trace/annotations.go | 4 + src/cmd/trace/annotations_test.go | 6 ++ src/cmd/trace/trace_test.go | 2 + src/go/build/deps_test.go | 2 +- src/runtime/lock_futex.go | 6 ++ src/runtime/lock_js.go | 125 ++++++++++++++++++++--- src/runtime/lock_sema.go | 6 ++ src/runtime/proc.go | 12 +++ src/runtime/rt0_js_wasm.s | 78 ++++++++++---- src/runtime/sys_wasm.s | 15 +-- src/syscall/js/callback.go | 145 +++++++++++++++++++++++++++ src/syscall/js/js.go | 6 ++ src/syscall/js/js_test.go | 50 +++++++++ 18 files changed, 482 insertions(+), 49 deletions(-) create mode 100644 src/syscall/js/callback.go diff --git a/misc/wasm/wasm_exec.js b/misc/wasm/wasm_exec.js index de4cff7d2c..ada6f0cd92 100755 --- a/misc/wasm/wasm_exec.js +++ b/misc/wasm/wasm_exec.js @@ -56,6 +56,8 @@ console.warn("exit code:", code); } }; + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; const mem = () => { // The buffer may change when requesting more memory. @@ -119,6 +121,7 @@ go: { // func wasmExit(code int32) "runtime.wasmExit": (sp) => { + this.exited = true; this.exit(mem().getInt32(sp + 8, true)); }, @@ -142,6 +145,24 @@ mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); }, + // func scheduleCallback(delay int64) int32 + "runtime.scheduleCallback": (sp) => { + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._callbackTimeouts.set(id, setTimeout( + () => { this._resolveCallbackPromise(); }, + getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early + )); + mem().setInt32(sp + 16, id, true); + }, + + // func clearScheduledCallback(id int32) + "runtime.clearScheduledCallback": (sp) => { + const id = mem().getInt32(sp + 8, true); + clearTimeout(this._callbackTimeouts.get(id)); + this._callbackTimeouts.delete(id); + }, + // func getRandomData(r []byte) "runtime.getRandomData": (sp) => { crypto.getRandomValues(loadSlice(sp + 8)); @@ -269,7 +290,19 @@ async run(instance) { this._inst = instance; - this._values = [undefined, null, global, this._inst.exports.mem]; // TODO: garbage collection + this._values = [ // TODO: garbage collection + undefined, + null, + global, + this._inst.exports.mem, + () => { // resolveCallbackPromise + if (this.exited) { + throw new Error("bad callback: Go program has already exited"); + } + setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous + }, + ]; + this.exited = false; const mem = new DataView(this._inst.exports.mem.buffer) @@ -303,7 +336,16 @@ offset += 8; }); - this._inst.exports.run(argc, argv); + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = resolve; + }); + this._inst.exports.run(argc, argv); + if (this.exited) { + break; + } + await callbackPromise; + } } } @@ -318,9 +360,16 @@ go.env = process.env; go.exit = process.exit; WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + process.on("exit", () => { // Node.js exits if no callback is pending + if (!go.exited) { + console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!"); + process.exit(1); + } + }); return go.run(result.instance); }).catch((err) => { console.error(err); + go.exited = true; process.exit(1); }); } 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) +} -- 2.50.0