console.warn("exit code:", code);
}
};
+ this._callbackTimeouts = new Map();
+ this._nextCallbackTimeoutID = 1;
const mem = () => {
// The buffer may change when requesting more memory.
go: {
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
+ this.exited = true;
this.exit(mem().getInt32(sp + 8, 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)
"runtime.getRandomData": (sp) => {
crypto.getRandomValues(loadSlice(sp + 8));
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)
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;
+ }
}
}
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);
});
}
// However, it is not allowed to switch goroutines while inside of an ACALLNORESUME call.
ACALLNORESUME
+ ARETUNWIND
+
AMOVB
AMOVH
AMOVW
REG_RET1
REG_RET2
REG_RET3
+ REG_RUN
// locals
REG_R0
"F64ReinterpretI64",
"RESUMEPOINT",
"CALLNORESUME",
+ "RETUNWIND",
"MOVB",
"MOVH",
"MOVW",
"RET1": REG_RET1,
"RET2": REG_RET2,
"RET3": REG_RET3,
+ "RUN": REG_RUN,
"R0": REG_R0,
"R1": REG_R1,
p = appendp(p, AEnd) // end of Loop
}
- case obj.ARET:
+ case obj.ARET, ARETUNWIND:
ret := *p
p.As = obj.ANOP
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)
}
}
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:
}
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:
I64, // 6: RET1
I64, // 7: RET2
I64, // 8: RET3
+ I32, // 9: RUN
}
writeUleb128(ctxt.Out, uint64(len(globalRegs))) // number of globals
+// 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 (
+// 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 (
// 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 (
// 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"},
exitsyscall()
return ok
}
+
+func pauseSchedulerUntilCallback() bool {
+ return false
+}
+
+func checkTimeouts() {}
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
}
}
// 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) {
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)
exitsyscall()
return ok
}
+
+func pauseSchedulerUntilCallback() bool {
+ return false
+}
+
+func checkTimeouts() {}
// 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)
}
// 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)
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,
// which can change underfoot once we no longer block
// safe-points. We don't need to snapshot the contents because
#include "go_asm.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:
// R0: argc (i32)
// R1: argv (i32)
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 R0 // argc
+ I64ExtendUI32
+ I64Store $0
- Get SP
- Get R0 // argc
- I64ExtendUI32
- I64Store $0
+ Get SP
+ Get R1 // argv
+ I64ExtendUI32
+ I64Store $8
- Get SP
- Get R1 // argv
- I64ExtendUI32
- I64Store $8
+ I32Const $runtime·rt0_go(SB)
+ I32Const $16
+ I32ShrU
+ Set PC_F
- I32Const $runtime·rt0_go(SB)
- I32Const $16
- I32ShrU
- Set PC_F
+ 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 SP=0 indicates program 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 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.
loop:
Loop
- Get SP
- I32Eqz
- If
- Return
- End
-
Get PC_F
CallIndirect $0
Drop
- Br loop
+ Get RUN
+ I32Const $RUN_RUNNING
+ I32Eq
+ BrIf loop
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
UNDEF
I64TruncUF64
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
UNDEF
CallImport
RET
+TEXT ·scheduleCallback(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·clearScheduledCallback(SB), NOSPLIT, $0
+ CallImport
+ RET
+
TEXT ·getRandomData(SB), NOSPLIT, $0
CallImport
RET
--- /dev/null
+// 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()
// Global is the JavaScript global object, usually "window" or "global".
Global = Value{2}
+ // memory is the WebAssembly linear memory.
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")
switch x := x.(type) {
case Value:
return x
+ case Callback:
+ return x.enqueueFn
case nil:
return Null
case bool:
package js_test
import (
+ "fmt"
"syscall/js"
"testing"
)
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)
+}