mirror of https://github.com/golang/go.git
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>
This commit is contained in:
parent
5fdacfa89f
commit
e083dc6307
|
|
@ -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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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 (
|
||||||
// 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 (
|
||||||
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{
|
||||||
"F64ReinterpretI64",
|
"F64ReinterpretI64",
|
||||||
"RESUMEPOINT",
|
"RESUMEPOINT",
|
||||||
"CALLNORESUME",
|
"CALLNORESUME",
|
||||||
|
"RETUNWIND",
|
||||||
"MOVB",
|
"MOVB",
|
||||||
"MOVH",
|
"MOVH",
|
||||||
"MOVW",
|
"MOVW",
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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) {
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
||||||
// 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 {
|
||||||
exitsyscall()
|
exitsyscall()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pauseSchedulerUntilCallback() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTimeouts() {}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
||||||
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
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 {
|
||||||
exitsyscall()
|
exitsyscall()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pauseSchedulerUntilCallback() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTimeouts() {}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
// 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:
|
||||||
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 @@
|
||||||
#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 SP
|
||||||
Get R0 // argc
|
Get R0 // argc
|
||||||
I64ExtendUI32
|
I64ExtendUI32
|
||||||
I64Store $0
|
I64Store $0
|
||||||
|
|
||||||
Get SP
|
Get SP
|
||||||
Get R1 // argv
|
Get R1 // argv
|
||||||
I64ExtendUI32
|
I64ExtendUI32
|
||||||
I64Store $8
|
I64Store $8
|
||||||
|
|
||||||
I32Const $runtime·rt0_go(SB)
|
I32Const $runtime·rt0_go(SB)
|
||||||
I32Const $16
|
I32Const $16
|
||||||
I32ShrU
|
I32ShrU
|
||||||
Set PC_F
|
Set PC_F
|
||||||
|
|
||||||
// Call the function for the current PC_F. Repeat until SP=0 indicates program end.
|
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 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
|
||||||
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
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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 {
|
||||||
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 @@
|
||||||
package js_test
|
package js_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue