Skip to content

Commit

Permalink
runtime, sycall/js: add support for callbacks from JavaScript
Browse files Browse the repository at this point in the history
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
  • Loading branch information
neelance committed May 23, 2018
1 parent 5997f9c commit d1b0188
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 47 deletions.
37 changes: 35 additions & 2 deletions misc/wasm/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
go: {
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
this.exited = true;
this.exit(mem().getInt32(sp + 8, true));
},

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

// func scheduleCallback(delay int64)
"runtime.scheduleCallback": (sp) => {
setTimeout(() => { this._resolveCallbackPromise(); }, getInt64(sp + 8));
},

// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
crypto.getRandomValues(loadSlice(sp + 8));
Expand Down Expand Up @@ -270,7 +276,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,
() => {
if (this.exited) {
throw new Error('bad callback: Go program has already exited (hint: use "select {}")');
}
setTimeout(this._resolveCallbackPromise, 0); // make sure it is asynchronous
},
];
this.exited = false;

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

Expand Down Expand Up @@ -304,7 +322,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;
}
}
}

Expand All @@ -319,6 +346,12 @@
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);
Expand Down
3 changes: 3 additions & 0 deletions src/cmd/internal/obj/wasm/a.out.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ const (
// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call.
ACALLNORESUME

ARETUNWIND

AMOVB
AMOVH
AMOVW
Expand All @@ -244,6 +246,7 @@ const (
REG_RET1
REG_RET2
REG_RET3
REG_RUN

// locals
REG_R0
Expand Down
1 change: 1 addition & 0 deletions src/cmd/internal/obj/wasm/anames.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ var Anames = []string{
"F64ReinterpretI64",
"RESUMEPOINT",
"CALLNORESUME",
"RETUNWIND",
"MOVB",
"MOVH",
"MOVW",
Expand Down
16 changes: 12 additions & 4 deletions src/cmd/internal/obj/wasm/wasmobj.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/cmd/link/internal/wasm/asm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/lock_futex.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}

func pauseSchedulerUntilCallback() bool {
return false
}

func checkTimeouts() {}
94 changes: 81 additions & 13 deletions src/runtime/lock_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 as simple as allowing other goroutines
// to run until the mutex got 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
Expand All @@ -34,16 +42,28 @@ func unlock(l *mutex) {
}

// One-time notifications.

type noteWithTimeout struct {
gp *g
deadline int64
}

var notes = make(map[*note]*g)
var 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")
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) {
Expand All @@ -62,14 +82,62 @@ func notetsleepg(n *note, ns int64) bool {
throw("notetsleepg on g0")
}

deadline := nanotime() + ns
for {
if n.key != 0 {
return true
if ns >= 0 {
delay := ns/1000000 + 2
if delay > 1<<31-1 {
delay = 1<<31 - 1 // cap to max int32
}
Gosched()
if ns >= 0 && nanotime() >= deadline {
return false

notes[n] = gp
notesWithTimeout[n] = noteWithTimeout{gp: gp, deadline: nanotime() + ns}
scheduleCallback(delay)
gopark(nil, nil, waitReasonSleep, traceEvNone, 1)
delete(notes, n)
delete(notesWithTimeout, n)

return n.key == note_woken
}

for n.key != note_woken {
notes[n] = gp
gopark(nil, nil, waitReasonZero, traceEvNone, 1)
delete(notes, n)
}
return true
}

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

//go:linkname sleepUntilCallback syscall/js.sleepUntilCallback
func sleepUntilCallback() {
waitingForCallback = append(waitingForCallback, getg())
gopark(nil, nil, waitReasonZero, traceEvNone, 1)
}

func pauseSchedulerUntilCallback() bool {
if len(waitingForCallback) == 0 {
return false
}

pause()
checkTimeouts()
for _, gp := range waitingForCallback {
goready(gp, 1)
}
waitingForCallback = waitingForCallback[:0]
return true
}

func pause()

func scheduleCallback(delay int64)
6 changes: 6 additions & 0 deletions src/runtime/lock_sema.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}

func pauseSchedulerUntilCallback() bool {
return false
}

func checkTimeouts() {}
8 changes: 8 additions & 0 deletions src/runtime/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,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)
}

Expand All @@ -281,6 +282,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)
Expand Down Expand Up @@ -2315,6 +2319,10 @@ stop:
return gp, false
}

if pauseSchedulerUntilCallback() {
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
Expand Down
Loading

0 comments on commit d1b0188

Please sign in to comment.