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 #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>
  • Loading branch information
neelance authored and aclements committed Jun 14, 2018
1 parent 5fdacfa commit e083dc6
Show file tree
Hide file tree
Showing 18 changed files with 482 additions and 49 deletions.
53 changes: 51 additions & 2 deletions misc/wasm/wasm_exec.js
Expand Up @@ -56,6 +56,8 @@
console.warn("exit code:", code);
}
};
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;

const mem = () => {
// The buffer may change when requesting more memory.
Expand Down Expand Up @@ -119,6 +121,7 @@
go: {
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
this.exited = true;
this.exit(mem().getInt32(sp + 8, true));
},

Expand All @@ -142,6 +145,24 @@
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)
"runtime.getRandomData": (sp) => {
crypto.getRandomValues(loadSlice(sp + 8));
Expand Down Expand Up @@ -269,7 +290,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,
() => { // 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)

Expand Down Expand Up @@ -303,7 +336,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 @@ -318,9 +360,16 @@
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);
go.exited = true;
process.exit(1);
});
}
Expand Down
3 changes: 3 additions & 0 deletions src/cmd/internal/obj/wasm/a.out.go
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
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
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
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
4 changes: 4 additions & 0 deletions src/cmd/trace/annotations.go
@@ -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

import (
Expand Down
6 changes: 6 additions & 0 deletions src/cmd/trace/annotations_test.go
@@ -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

import (
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/trace/trace_test.go
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build !js

package main

import (
Expand Down
2 changes: 1 addition & 1 deletion src/go/build/deps_test.go
Expand Up @@ -139,7 +139,7 @@ var pkgDeps = map[string][]string{

// Operating system access.
"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/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/lock_futex.go
Expand Up @@ -229,3 +229,9 @@ func notetsleepg(n *note, ns int64) bool {
exitsyscall()
return ok
}

func pauseSchedulerUntilCallback() bool {
return false
}

func checkTimeouts() {}
125 changes: 111 additions & 14 deletions src/runtime/lock_js.go
Expand Up @@ -6,22 +6,30 @@

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 implemented by allowing other goroutines
// to run until the mutex gets 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
)

func lock(l *mutex) {
for l.key == mutex_locked {
Gosched()
mcall(gosched_m)
}
l.key = mutex_locked
}
Expand All @@ -34,16 +42,31 @@ func unlock(l *mutex) {
}

// 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) {
n.key = 0
n.key = note_cleared
}

func notewakeup(n *note) {
if n.key != 0 {
print("notewakeup - double wakeup (", n.key, ")\n")
// gp := getg()
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 +85,88 @@ func notetsleepg(n *note, ns int64) bool {
throw("notetsleepg on g0")
}

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

id := scheduleCallback(delay)
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)

0 comments on commit e083dc6

Please sign in to comment.