Commit e083dc63 authored by Richard Musiol's avatar Richard Musiol Committed by Austin Clements

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/114197Reviewed-by: 's avatarAustin Clements <austin@google.com>
Reviewed-by: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
parent 5fdacfa8
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
console.warn("exit code:", code); console.warn("exit code:", code);
} }
}; };
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const mem = () => { const mem = () => {
// The buffer may change when requesting more memory. // The buffer may change when requesting more memory.
...@@ -119,6 +121,7 @@ ...@@ -119,6 +121,7 @@
go: { go: {
// func wasmExit(code int32) // func wasmExit(code int32)
"runtime.wasmExit": (sp) => { "runtime.wasmExit": (sp) => {
this.exited = true;
this.exit(mem().getInt32(sp + 8, true)); this.exit(mem().getInt32(sp + 8, true));
}, },
...@@ -142,6 +145,24 @@ ...@@ -142,6 +145,24 @@
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); 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) // func getRandomData(r []byte)
"runtime.getRandomData": (sp) => { "runtime.getRandomData": (sp) => {
crypto.getRandomValues(loadSlice(sp + 8)); crypto.getRandomValues(loadSlice(sp + 8));
...@@ -269,7 +290,19 @@ ...@@ -269,7 +290,19 @@
async run(instance) { async run(instance) {
this._inst = 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) const mem = new DataView(this._inst.exports.mem.buffer)
...@@ -303,7 +336,16 @@ ...@@ -303,7 +336,16 @@
offset += 8; 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 @@ ...@@ -318,9 +360,16 @@
go.env = process.env; go.env = process.env;
go.exit = process.exit; go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 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); return go.run(result.instance);
}).catch((err) => { }).catch((err) => {
console.error(err); console.error(err);
go.exited = true;
process.exit(1); process.exit(1);
}); });
} }
......
...@@ -219,6 +219,8 @@ const ( ...@@ -219,6 +219,8 @@ const (
// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call. // However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call.
ACALLNORESUME ACALLNORESUME
ARETUNWIND
AMOVB AMOVB
AMOVH AMOVH
AMOVW AMOVW
...@@ -244,6 +246,7 @@ const ( ...@@ -244,6 +246,7 @@ const (
REG_RET1 REG_RET1
REG_RET2 REG_RET2
REG_RET3 REG_RET3
REG_RUN
// locals // locals
REG_R0 REG_R0
......
...@@ -180,6 +180,7 @@ var Anames = []string{ ...@@ -180,6 +180,7 @@ var Anames = []string{
"F64ReinterpretI64", "F64ReinterpretI64",
"RESUMEPOINT", "RESUMEPOINT",
"CALLNORESUME", "CALLNORESUME",
"RETUNWIND",
"MOVB", "MOVB",
"MOVH", "MOVH",
"MOVW", "MOVW",
......
...@@ -25,6 +25,7 @@ var Register = map[string]int16{ ...@@ -25,6 +25,7 @@ var Register = map[string]int16{
"RET1": REG_RET1, "RET1": REG_RET1,
"RET2": REG_RET2, "RET2": REG_RET2,
"RET3": REG_RET3, "RET3": REG_RET3,
"RUN": REG_RUN,
"R0": REG_R0, "R0": REG_R0,
"R1": REG_R1, "R1": REG_R1,
...@@ -487,7 +488,7 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { ...@@ -487,7 +488,7 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
p = appendp(p, AEnd) // end of Loop p = appendp(p, AEnd) // end of Loop
} }
case obj.ARET: case obj.ARET, ARETUNWIND:
ret := *p ret := *p
p.As = obj.ANOP p.As = obj.ANOP
...@@ -528,7 +529,14 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { ...@@ -528,7 +529,14 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
p = appendp(p, AI32Add) p = appendp(p, AI32Add)
p = appendp(p, ASet, regAddr(REG_SP)) 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, AI32Const, constAddr(0))
p = appendp(p, AReturn) p = appendp(p, AReturn)
} }
...@@ -726,7 +734,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { ...@@ -726,7 +734,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
} }
reg := p.From.Reg reg := p.From.Reg
switch { switch {
case reg >= REG_PC_F && reg <= REG_RET3: case reg >= REG_PC_F && reg <= REG_RUN:
w.WriteByte(0x23) // get_global w.WriteByte(0x23) // get_global
writeUleb128(w, uint64(reg-REG_PC_F)) writeUleb128(w, uint64(reg-REG_PC_F))
case reg >= REG_R0 && reg <= REG_F15: case reg >= REG_R0 && reg <= REG_F15:
...@@ -743,7 +751,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) { ...@@ -743,7 +751,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
} }
reg := p.To.Reg reg := p.To.Reg
switch { switch {
case reg >= REG_PC_F && reg <= REG_RET3: case reg >= REG_PC_F && reg <= REG_RUN:
w.WriteByte(0x24) // set_global w.WriteByte(0x24) // set_global
writeUleb128(w, uint64(reg-REG_PC_F)) writeUleb128(w, uint64(reg-REG_PC_F))
case reg >= REG_R0 && reg <= REG_F15: case reg >= REG_R0 && reg <= REG_F15:
......
...@@ -304,6 +304,7 @@ func writeGlobalSec(ctxt *ld.Link) { ...@@ -304,6 +304,7 @@ func writeGlobalSec(ctxt *ld.Link) {
I64, // 6: RET1 I64, // 6: RET1
I64, // 7: RET2 I64, // 7: RET2
I64, // 8: RET3 I64, // 8: RET3
I32, // 9: RUN
} }
writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals
......
// 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 package main
import ( import (
......
// 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 package main
import ( import (
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !js
package main package main
import ( import (
......
...@@ -139,7 +139,7 @@ var pkgDeps = map[string][]string{ ...@@ -139,7 +139,7 @@ var pkgDeps = map[string][]string{
// Operating system access. // Operating system access.
"syscall": {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"}, "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/unix": {"L0", "syscall"},
"internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"}, "internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"}, "internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},
......
...@@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool { ...@@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall() exitsyscall()
return ok return ok
} }
func pauseSchedulerUntilCallback() bool {
return false
}
func checkTimeouts() {}
...@@ -6,14 +6,22 @@ ...@@ -6,14 +6,22 @@
package runtime package runtime
import (
_ "unsafe"
)
// js/wasm has no support for threads yet. There is no preemption. // js/wasm has no support for threads yet. There is no preemption.
// Waiting for a mutex or timeout is implemented as a busy loop // Waiting for a mutex is implemented by allowing other goroutines
// while allowing other goroutines to run. // to run until the mutex gets unlocked.
const ( const (
mutex_unlocked = 0 mutex_unlocked = 0
mutex_locked = 1 mutex_locked = 1
note_cleared = 0
note_woken = 1
note_timeout = 2
active_spin = 4 active_spin = 4
active_spin_cnt = 30 active_spin_cnt = 30
passive_spin = 1 passive_spin = 1
...@@ -21,7 +29,7 @@ const ( ...@@ -21,7 +29,7 @@ const (
func lock(l *mutex) { func lock(l *mutex) {
for l.key == mutex_locked { for l.key == mutex_locked {
Gosched() mcall(gosched_m)
} }
l.key = mutex_locked l.key = mutex_locked
} }
...@@ -34,16 +42,31 @@ func unlock(l *mutex) { ...@@ -34,16 +42,31 @@ func unlock(l *mutex) {
} }
// One-time notifications. // 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) { func noteclear(n *note) {
n.key = 0 n.key = note_cleared
} }
func notewakeup(n *note) { func notewakeup(n *note) {
if n.key != 0 { // gp := getg()
print("notewakeup - double wakeup (", n.key, ")\n") if n.key == note_woken {
throw("notewakeup - double wakeup") 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) { func notesleep(n *note) {
...@@ -62,14 +85,88 @@ func notetsleepg(n *note, ns int64) bool { ...@@ -62,14 +85,88 @@ func notetsleepg(n *note, ns int64) bool {
throw("notetsleepg on g0") throw("notetsleepg on g0")
} }
deadline := nanotime() + ns if ns >= 0 {
for { deadline := nanotime() + ns
if n.key != 0 { delay := ns/1000000 + 1 // round up
return true if delay > 1<<31-1 {
delay = 1<<31 - 1 // cap to max int32
} }
Gosched()
if ns >= 0 && nanotime() >= deadline { id := scheduleCallback(delay)
return false 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)
...@@ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool { ...@@ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall() exitsyscall()
return ok return ok
} }
func pauseSchedulerUntilCallback() bool {
return false
}
func checkTimeouts() {}
...@@ -263,6 +263,7 @@ func forcegchelper() { ...@@ -263,6 +263,7 @@ func forcegchelper() {
// Gosched yields the processor, allowing other goroutines to run. It does not // Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically. // suspend the current goroutine, so execution resumes automatically.
func Gosched() { func Gosched() {
checkTimeouts()
mcall(gosched_m) mcall(gosched_m)
} }
...@@ -282,6 +283,9 @@ func goschedguarded() { ...@@ -282,6 +283,9 @@ func goschedguarded() {
// Reasons should be unique and descriptive. // Reasons should be unique and descriptive.
// Do not re-use reasons, add new ones. // 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) { 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() mp := acquirem()
gp := mp.curg gp := mp.curg
status := readgstatus(gp) status := readgstatus(gp)
...@@ -2361,6 +2365,14 @@ stop: ...@@ -2361,6 +2365,14 @@ stop:
return gp, false 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, // Before we drop our P, make a snapshot of the allp slice,
// which can change underfoot once we no longer block // which can change underfoot once we no longer block
// safe-points. We don't need to snapshot the contents because // safe-points. We don't need to snapshot the contents because
......
...@@ -5,45 +5,81 @@ ...@@ -5,45 +5,81 @@
#include "go_asm.h" #include "go_asm.h"
#include "textflag.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: // _rt0_wasm_js does NOT follow the Go ABI. It has two WebAssembly parameters:
// R0: argc (i32) // R0: argc (i32)
// R1: argv (i32) // R1: argv (i32)
TEXT _rt0_wasm_js(SB),NOSPLIT,$0 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 SP
Get R0 // argc Get R1 // argv
I64ExtendUI32 I64ExtendUI32
I64Store $0 I64Store $8
Get SP I32Const $runtime·rt0_go(SB)
Get R1 // argv I32Const $16
I64ExtendUI32 I32ShrU
I64Store $8 Set PC_F
I32Const $runtime·rt0_go(SB) I32Const $RUN_RUNNING
I32Const $16 Set RUN
I32ShrU Else
Set PC_F 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 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 // 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. // with this loop, without having to restore the full WebAssembly stack.
loop: loop:
Loop Loop
Get SP
I32Eqz
If
Return
End
Get PC_F Get PC_F
CallIndirect $0 CallIndirect $0
Drop Drop
Br loop Get RUN
I32Const $RUN_RUNNING
I32Eq
BrIf loop
End 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 TEXT _rt0_wasm_js_lib(SB),NOSPLIT,$0
UNDEF UNDEF
...@@ -149,13 +149,6 @@ TEXT runtime·wasmTruncU(SB), NOSPLIT, $0-0 ...@@ -149,13 +149,6 @@ TEXT runtime·wasmTruncU(SB), NOSPLIT, $0-0
I64TruncUF64 I64TruncUF64
Return 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 TEXT runtime·exitThread(SB), NOSPLIT, $0-0
UNDEF UNDEF
...@@ -194,6 +187,14 @@ TEXT ·walltime(SB), NOSPLIT, $0 ...@@ -194,6 +187,14 @@ TEXT ·walltime(SB), NOSPLIT, $0
CallImport CallImport
RET RET
TEXT ·scheduleCallback(SB), NOSPLIT, $0
CallImport
RET
TEXT ·clearScheduledCallback(SB), NOSPLIT, $0
CallImport
RET
TEXT ·getRandomData(SB), NOSPLIT, $0 TEXT ·getRandomData(SB), NOSPLIT, $0
CallImport CallImport
RET RET
// 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()
...@@ -39,7 +39,11 @@ var ( ...@@ -39,7 +39,11 @@ var (
// Global is the JavaScript global object, usually "window" or "global". // Global is the JavaScript global object, usually "window" or "global".
Global = Value{2} Global = Value{2}
// memory is the WebAssembly linear memory.
memory = Value{3} 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") var uint8Array = Global.Get("Uint8Array")
...@@ -49,6 +53,8 @@ func ValueOf(x interface{}) Value { ...@@ -49,6 +53,8 @@ func ValueOf(x interface{}) Value {
switch x := x.(type) { switch x := x.(type) {
case Value: case Value:
return x return x
case Callback:
return x.enqueueFn
case nil: case nil:
return Null return Null
case bool: case bool:
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
package js_test package js_test
import ( import (
"fmt"
"syscall/js" "syscall/js"
"testing" "testing"
) )
...@@ -144,3 +145,52 @@ func TestNew(t *testing.T) { ...@@ -144,3 +145,52 @@ func TestNew(t *testing.T) {
t.Errorf("got %#v, want %#v", got, 42) 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)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment