Skip to content

Commit e97ef87

Browse files
committed
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 golang#18892 Updates golang#25506 Change-Id: Ib8701cfa0536d10d69bd541c85b0e2a754eb54fb
1 parent 53a8eb8 commit e97ef87

File tree

13 files changed

+326
-47
lines changed

13 files changed

+326
-47
lines changed

misc/wasm/wasm_exec.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
go: {
121121
// func wasmExit(code int32)
122122
"runtime.wasmExit": (sp) => {
123+
this.exited = true;
123124
this.exit(mem().getInt32(sp + 8, true));
124125
},
125126

@@ -143,6 +144,11 @@
143144
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
144145
},
145146

147+
// func scheduleCallback(delay int64)
148+
"runtime.scheduleCallback": (sp) => {
149+
setTimeout(() => { this._resolveCallbackPromise(); }, getInt64(sp + 8));
150+
},
151+
146152
// func getRandomData(r []byte)
147153
"runtime.getRandomData": (sp) => {
148154
crypto.getRandomValues(loadSlice(sp + 8));
@@ -270,7 +276,19 @@
270276

271277
async run(instance) {
272278
this._inst = instance;
273-
this._values = [undefined, null, global, this._inst.exports.mem]; // TODO: garbage collection
279+
this._values = [ // TODO: garbage collection
280+
undefined,
281+
null,
282+
global,
283+
this._inst.exports.mem,
284+
() => {
285+
if (this.exited) {
286+
throw new Error("bad callback: Go program has already exited");
287+
}
288+
setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
289+
},
290+
];
291+
this.exited = false;
274292

275293
const mem = new DataView(this._inst.exports.mem.buffer)
276294

@@ -304,7 +322,16 @@
304322
offset += 8;
305323
});
306324

307-
this._inst.exports.run(argc, argv);
325+
while (true) {
326+
const callbackPromise = new Promise((resolve) => {
327+
this._resolveCallbackPromise = resolve;
328+
});
329+
this._inst.exports.run(argc, argv);
330+
if (this.exited) {
331+
break;
332+
}
333+
await callbackPromise;
334+
}
308335
}
309336
}
310337

@@ -319,6 +346,12 @@
319346
go.env = process.env;
320347
go.exit = process.exit;
321348
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
349+
process.on("exit", () => { // Node.js exits if no callback is pending
350+
if (!go.exited) {
351+
console.error("error: all goroutines asleep and no JavaScript callback pending - deadlock!");
352+
process.exit(1);
353+
}
354+
});
322355
return go.run(result.instance);
323356
}).catch((err) => {
324357
console.error(err);

src/cmd/internal/obj/wasm/a.out.go

Lines changed: 3 additions & 0 deletions
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

src/cmd/internal/obj/wasm/anames.go

Lines changed: 1 addition & 0 deletions
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",

src/cmd/internal/obj/wasm/wasmobj.go

Lines changed: 12 additions & 4 deletions
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:

src/cmd/link/internal/wasm/asm.go

Lines changed: 1 addition & 0 deletions
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

src/runtime/lock_futex.go

Lines changed: 6 additions & 0 deletions
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() {}

src/runtime/lock_js.go

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@
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
@@ -34,16 +42,30 @@ 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+
if n.key == note_woken {
4462
throw("notewakeup - double wakeup")
4563
}
46-
n.key = 1
64+
cleared := n.key == note_cleared
65+
n.key = note_woken
66+
if cleared {
67+
goready(notes[n], 1)
68+
}
4769
}
4870

4971
func notesleep(n *note) {
@@ -62,14 +84,62 @@ func notetsleepg(n *note, ns int64) bool {
6284
throw("notetsleepg on g0")
6385
}
6486

65-
deadline := nanotime() + ns
66-
for {
67-
if n.key != 0 {
68-
return true
87+
if ns >= 0 {
88+
delay := ns/1000000 + 2
89+
if delay > 1<<31-1 {
90+
delay = 1<<31 - 1 // cap to max int32
6991
}
70-
Gosched()
71-
if ns >= 0 && nanotime() >= deadline {
72-
return false
92+
93+
notes[n] = gp
94+
notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: nanotime() + ns}
95+
scheduleCallback(delay)
96+
gopark(nil, nil, waitReasonSleep, traceEvNone, 1)
97+
delete(notes, n)
98+
delete(notesWithTimeout, n)
99+
100+
return n.key == note_woken
101+
}
102+
103+
for n.key != note_woken {
104+
notes[n] = gp
105+
gopark(nil, nil, waitReasonZero, traceEvNone, 1)
106+
delete(notes, n)
107+
}
108+
return true
109+
}
110+
111+
func checkTimeouts() {
112+
now := nanotime()
113+
for n, nt := range notesWithTimeout {
114+
if n.key == note_cleared && now > nt.deadline {
115+
n.key = note_timeout
116+
goready(nt.gp, 1)
73117
}
74118
}
75119
}
120+
121+
var waitingForCallback []*g
122+
123+
//go:linkname sleepUntilCallback syscall/js.sleepUntilCallback
124+
func sleepUntilCallback() {
125+
waitingForCallback = append(waitingForCallback, getg())
126+
gopark(nil, nil, waitReasonZero, traceEvNone, 1)
127+
}
128+
129+
func pauseSchedulerUntilCallback() bool {
130+
if len(waitingForCallback) == 0 {
131+
return false
132+
}
133+
134+
pause()
135+
checkTimeouts()
136+
for _, gp := range waitingForCallback {
137+
goready(gp, 1)
138+
}
139+
waitingForCallback = waitingForCallback[:0]
140+
return true
141+
}
142+
143+
func pause()
144+
145+
func scheduleCallback(delay int64)

src/runtime/lock_sema.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool {
282282
exitsyscall()
283283
return ok
284284
}
285+
286+
func pauseSchedulerUntilCallback() bool {
287+
return false
288+
}
289+
290+
func checkTimeouts() {}

src/runtime/proc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ func forcegchelper() {
262262
// Gosched yields the processor, allowing other goroutines to run. It does not
263263
// suspend the current goroutine, so execution resumes automatically.
264264
func Gosched() {
265+
checkTimeouts()
265266
mcall(gosched_m)
266267
}
267268

@@ -281,6 +282,9 @@ func goschedguarded() {
281282
// Reasons should be unique and descriptive.
282283
// Do not re-use reasons, add new ones.
283284
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
285+
if reason != waitReasonSleep {
286+
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
287+
}
284288
mp := acquirem()
285289
gp := mp.curg
286290
status := readgstatus(gp)
@@ -2349,6 +2353,10 @@ stop:
23492353
return gp, false
23502354
}
23512355

2356+
if pauseSchedulerUntilCallback() {
2357+
goto top
2358+
}
2359+
23522360
// Before we drop our P, make a snapshot of the allp slice,
23532361
// which can change underfoot once we no longer block
23542362
// safe-points. We don't need to snapshot the contents because

0 commit comments

Comments
 (0)