Skip to content

Commit e083dc6

Browse files
neelanceaclements
authored andcommitted
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>
1 parent 5fdacfa commit e083dc6

File tree

18 files changed

+482
-49
lines changed

18 files changed

+482
-49
lines changed

Diff for: misc/wasm/wasm_exec.js

+51-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
console.warn("exit code:", code);
5757
}
5858
};
59+
this._callbackTimeouts = new Map();
60+
this._nextCallbackTimeoutID = 1;
5961

6062
const mem = () => {
6163
// The buffer may change when requesting more memory.
@@ -119,6 +121,7 @@
119121
go: {
120122
// func wasmExit(code int32)
121123
"runtime.wasmExit": (sp) => {
124+
this.exited = true;
122125
this.exit(mem().getInt32(sp + 8, true));
123126
},
124127

@@ -142,6 +145,24 @@
142145
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
143146
},
144147

148+
// func scheduleCallback(delay int64) int32
149+
"runtime.scheduleCallback": (sp) => {
150+
const id = this._nextCallbackTimeoutID;
151+
this._nextCallbackTimeoutID++;
152+
this._callbackTimeouts.set(id, setTimeout(
153+
() => { this._resolveCallbackPromise(); },
154+
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
155+
));
156+
mem().setInt32(sp + 16, id, true);
157+
},
158+
159+
// func clearScheduledCallback(id int32)
160+
"runtime.clearScheduledCallback": (sp) => {
161+
const id = mem().getInt32(sp + 8, true);
162+
clearTimeout(this._callbackTimeouts.get(id));
163+
this._callbackTimeouts.delete(id);
164+
},
165+
145166
// func getRandomData(r []byte)
146167
"runtime.getRandomData": (sp) => {
147168
crypto.getRandomValues(loadSlice(sp + 8));
@@ -269,7 +290,19 @@
269290

270291
async run(instance) {
271292
this._inst = instance;
272-
this._values = [undefined, null, global, this._inst.exports.mem]; // TODO: garbage collection
293+
this._values = [ // TODO: garbage collection
294+
undefined,
295+
null,
296+
global,
297+
this._inst.exports.mem,
298+
() => { // resolveCallbackPromise
299+
if (this.exited) {
300+
throw new Error("bad callback: Go program has already exited");
301+
}
302+
setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
303+
},
304+
];
305+
this.exited = false;
273306

274307
const mem = new DataView(this._inst.exports.mem.buffer)
275308

@@ -303,7 +336,16 @@
303336
offset += 8;
304337
});
305338

306-
this._inst.exports.run(argc, argv);
339+
while (true) {
340+
const callbackPromise = new Promise((resolve) => {
341+
this._resolveCallbackPromise = resolve;
342+
});
343+
this._inst.exports.run(argc, argv);
344+
if (this.exited) {
345+
break;
346+
}
347+
await callbackPromise;
348+
}
307349
}
308350
}
309351

@@ -318,9 +360,16 @@
318360
go.env = process.env;
319361
go.exit = process.exit;
320362
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
363+
process.on("exit", () => { // Node.js exits if no callback is pending
364+
if (!go.exited) {
365+
console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!");
366+
process.exit(1);
367+
}
368+
});
321369
return go.run(result.instance);
322370
}).catch((err) => {
323371
console.error(err);
372+
go.exited = true;
324373
process.exit(1);
325374
});
326375
}

Diff for: src/cmd/internal/obj/wasm/a.out.go

+3
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ const (
219219
// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call.
220220
ACALLNORESUME
221221

222+
ARETUNWIND
223+
222224
AMOVB
223225
AMOVH
224226
AMOVW
@@ -244,6 +246,7 @@ const (
244246
REG_RET1
245247
REG_RET2
246248
REG_RET3
249+
REG_RUN
247250

248251
// locals
249252
REG_R0

Diff for: src/cmd/internal/obj/wasm/anames.go

+1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ var Anames = []string{
180180
"F64ReinterpretI64",
181181
"RESUMEPOINT",
182182
"CALLNORESUME",
183+
"RETUNWIND",
183184
"MOVB",
184185
"MOVH",
185186
"MOVW",

Diff for: src/cmd/internal/obj/wasm/wasmobj.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var Register = map[string]int16{
2525
"RET1": REG_RET1,
2626
"RET2": REG_RET2,
2727
"RET3": REG_RET3,
28+
"RUN": REG_RUN,
2829

2930
"R0": REG_R0,
3031
"R1": REG_R1,
@@ -487,7 +488,7 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
487488
p = appendp(p, AEnd) // end of Loop
488489
}
489490

490-
case obj.ARET:
491+
case obj.ARET, ARETUNWIND:
491492
ret := *p
492493
p.As = obj.ANOP
493494

@@ -528,7 +529,14 @@ func preprocess(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
528529
p = appendp(p, AI32Add)
529530
p = appendp(p, ASet, regAddr(REG_SP))
530531

531-
// not switching goroutine, return 0
532+
if ret.As == ARETUNWIND {
533+
// function needs to unwind the WebAssembly stack, return 1
534+
p = appendp(p, AI32Const, constAddr(1))
535+
p = appendp(p, AReturn)
536+
break
537+
}
538+
539+
// not unwinding the WebAssembly stack, return 0
532540
p = appendp(p, AI32Const, constAddr(0))
533541
p = appendp(p, AReturn)
534542
}
@@ -726,7 +734,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
726734
}
727735
reg := p.From.Reg
728736
switch {
729-
case reg >= REG_PC_F && reg <= REG_RET3:
737+
case reg >= REG_PC_F && reg <= REG_RUN:
730738
w.WriteByte(0x23) // get_global
731739
writeUleb128(w, uint64(reg-REG_PC_F))
732740
case reg >= REG_R0 && reg <= REG_F15:
@@ -743,7 +751,7 @@ func assemble(ctxt *obj.Link, s *obj.LSym, newprog obj.ProgAlloc) {
743751
}
744752
reg := p.To.Reg
745753
switch {
746-
case reg >= REG_PC_F && reg <= REG_RET3:
754+
case reg >= REG_PC_F && reg <= REG_RUN:
747755
w.WriteByte(0x24) // set_global
748756
writeUleb128(w, uint64(reg-REG_PC_F))
749757
case reg >= REG_R0 && reg <= REG_F15:

Diff for: src/cmd/link/internal/wasm/asm.go

+1
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ func writeGlobalSec(ctxt *ld.Link) {
304304
I64, // 6: RET1
305305
I64, // 7: RET2
306306
I64, // 8: RET3
307+
I32, // 9: RUN
307308
}
308309

309310
writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals

Diff for: src/cmd/trace/annotations.go

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
15
package main
26

37
import (

Diff for: src/cmd/trace/annotations_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build !js
6+
17
package main
28

39
import (

Diff for: src/cmd/trace/trace_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
// +build !js
6+
57
package main
68

79
import (

Diff for: src/go/build/deps_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ var pkgDeps = map[string][]string{
139139

140140
// Operating system access.
141141
"syscall": {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"},
142-
"syscall/js": {"unsafe"},
142+
"syscall/js": {"L0"},
143143
"internal/syscall/unix": {"L0", "syscall"},
144144
"internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
145145
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},

Diff for: src/runtime/lock_futex.go

+6
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool {
229229
exitsyscall()
230230
return ok
231231
}
232+
233+
func pauseSchedulerUntilCallback() bool {
234+
return false
235+
}
236+
237+
func checkTimeouts() {}

Diff for: src/runtime/lock_js.go

+111-14
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,30 @@
66

77
package runtime
88

9+
import (
10+
_ "unsafe"
11+
)
12+
913
// js/wasm has no support for threads yet. There is no preemption.
10-
// Waiting for a mutex or timeout is implemented as a busy loop
11-
// while allowing other goroutines to run.
14+
// Waiting for a mutex is implemented by allowing other goroutines
15+
// to run until the mutex gets unlocked.
1216

1317
const (
1418
mutex_unlocked = 0
1519
mutex_locked = 1
1620

21+
note_cleared = 0
22+
note_woken = 1
23+
note_timeout = 2
24+
1725
active_spin = 4
1826
active_spin_cnt = 30
1927
passive_spin = 1
2028
)
2129

2230
func lock(l *mutex) {
2331
for l.key == mutex_locked {
24-
Gosched()
32+
mcall(gosched_m)
2533
}
2634
l.key = mutex_locked
2735
}
@@ -34,16 +42,31 @@ func unlock(l *mutex) {
3442
}
3543

3644
// One-time notifications.
45+
46+
type noteWithTimeout struct {
47+
gp *g
48+
deadline int64
49+
}
50+
51+
var (
52+
notes = make(map[*note]*g)
53+
notesWithTimeout = make(map[*note]noteWithTimeout)
54+
)
55+
3756
func noteclear(n *note) {
38-
n.key = 0
57+
n.key = note_cleared
3958
}
4059

4160
func notewakeup(n *note) {
42-
if n.key != 0 {
43-
print("notewakeup - double wakeup (", n.key, ")\n")
61+
// gp := getg()
62+
if n.key == note_woken {
4463
throw("notewakeup - double wakeup")
4564
}
46-
n.key = 1
65+
cleared := n.key == note_cleared
66+
n.key = note_woken
67+
if cleared {
68+
goready(notes[n], 1)
69+
}
4770
}
4871

4972
func notesleep(n *note) {
@@ -62,14 +85,88 @@ func notetsleepg(n *note, ns int64) bool {
6285
throw("notetsleepg on g0")
6386
}
6487

65-
deadline := nanotime() + ns
66-
for {
67-
if n.key != 0 {
68-
return true
88+
if ns >= 0 {
89+
deadline := nanotime() + ns
90+
delay := ns/1000000 + 1 // round up
91+
if delay > 1<<31-1 {
92+
delay = 1<<31 - 1 // cap to max int32
6993
}
70-
Gosched()
71-
if ns >= 0 && nanotime() >= deadline {
72-
return false
94+
95+
id := scheduleCallback(delay)
96+
mp := acquirem()
97+
notes[n] = gp
98+
notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: deadline}
99+
releasem(mp)
100+
101+
gopark(nil, nil, waitReasonSleep, traceEvNone, 1)
102+
103+
clearScheduledCallback(id) // note might have woken early, clear timeout
104+
mp = acquirem()
105+
delete(notes, n)
106+
delete(notesWithTimeout, n)
107+
releasem(mp)
108+
109+
return n.key == note_woken
110+
}
111+
112+
for n.key != note_woken {
113+
mp := acquirem()
114+
notes[n] = gp
115+
releasem(mp)
116+
117+
gopark(nil, nil, waitReasonZero, traceEvNone, 1)
118+
119+
mp = acquirem()
120+
delete(notes, n)
121+
releasem(mp)
122+
}
123+
return true
124+
}
125+
126+
// checkTimeouts resumes goroutines that are waiting on a note which has reached its deadline.
127+
func checkTimeouts() {
128+
now := nanotime()
129+
for n, nt := range notesWithTimeout {
130+
if n.key == note_cleared && now > nt.deadline {
131+
n.key = note_timeout
132+
goready(nt.gp, 1)
73133
}
74134
}
75135
}
136+
137+
var waitingForCallback *g
138+
139+
// sleepUntilCallback puts the current goroutine to sleep until a callback is triggered.
140+
// It is currently only used by the callback routine of the syscall/js package.
141+
//go:linkname sleepUntilCallback syscall/js.sleepUntilCallback
142+
func sleepUntilCallback() {
143+
waitingForCallback = getg()
144+
gopark(nil, nil, waitReasonZero, traceEvNone, 1)
145+
waitingForCallback = nil
146+
}
147+
148+
// pauseSchedulerUntilCallback gets called from the scheduler and pauses the execution
149+
// of Go's WebAssembly code until a callback is triggered. Then it checks for note timeouts
150+
// and resumes goroutines that are waiting for a callback.
151+
func pauseSchedulerUntilCallback() bool {
152+
if waitingForCallback == nil && len(notesWithTimeout) == 0 {
153+
return false
154+
}
155+
156+
pause()
157+
checkTimeouts()
158+
if waitingForCallback != nil {
159+
goready(waitingForCallback, 1)
160+
}
161+
return true
162+
}
163+
164+
// pause pauses the execution of Go's WebAssembly code until a callback is triggered.
165+
func pause()
166+
167+
// scheduleCallback tells the WebAssembly environment to trigger a callback after ms milliseconds.
168+
// It returns a timer id that can be used with clearScheduledCallback.
169+
func scheduleCallback(ms int64) int32
170+
171+
// clearScheduledCallback clears a callback scheduled by scheduleCallback.
172+
func clearScheduledCallback(id int32)

0 commit comments

Comments
 (0)