Skip to content

Commit

Permalink
wasm: correctly return from run() in wasm_exec.js
Browse files Browse the repository at this point in the history
Instead of hanging forever, it should return the exit code from os.Exit.
  • Loading branch information
aykevl committed Oct 31, 2024
1 parent 88a6f4e commit 5443a11
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 29 deletions.
44 changes: 44 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,12 +766,56 @@ func TestWasmExportJS(t *testing.T) {
}
}

// Test whether Go.run() (in wasm_exec.js) normally returns and returns the
// right exit code.
func TestWasmExit(t *testing.T) {
t.Parallel()

type testCase struct {
name string
output string
}

tests := []testCase{
{name: "normal", output: "exit code: 0\n"},
{name: "exit-0", output: "exit code: 0\n"},
{name: "exit-0-sleep", output: "slept\nexit code: 0\n"},
{name: "exit-1", output: "exit code: 1\n"},
{name: "exit-1-sleep", output: "slept\nexit code: 1\n"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
options := optionsFromTarget("wasm", sema)
buildConfig, err := builder.NewConfig(&options)
if err != nil {
t.Fatal(err)
}
buildConfig.Target.Emulator = "node testdata/wasmexit.js {}"
output := &bytes.Buffer{}
_, err = buildAndRun("testdata/wasmexit.go", buildConfig, output, []string{tc.name}, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
return cmd.Run()
})
if err != nil {
t.Error(err)
}
expected := "wasmexit test: " + tc.name + "\n" + tc.output
checkOutputData(t, []byte(expected), output.Bytes())
})
}
}

// Check whether the output of a test equals the expected output.
func checkOutput(t *testing.T, filename string, actual []byte) {
expectedOutput, err := os.ReadFile(filename)
if err != nil {
t.Fatal("could not read output file:", err)
}
checkOutputData(t, expectedOutput, actual)
}

func checkOutputData(t *testing.T, expectedOutput, actual []byte) {
expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n"))
actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n"))

Expand Down
11 changes: 8 additions & 3 deletions src/runtime/runtime_tinygowasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@ func abort() {

//go:linkname syscall_Exit syscall.Exit
func syscall_Exit(code int) {
// TODO: should we call __stdio_exit here?
// It's a low-level exit (syscall.Exit) so doing any libc stuff seems
// unexpected, but then where else should stdio buffers be flushed?
// Flush stdio buffers.
__stdio_exit()

// Exit the program.
proc_exit(uint32(code))
}

func mainReturnExit() {
syscall_Exit(0)
}

// TinyGo does not yet support any form of parallelism on WebAssembly, so these
// can be left empty.

Expand Down
4 changes: 4 additions & 0 deletions src/runtime/runtime_tinygowasm_unknown.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func abort() {

//go:linkname syscall_Exit syscall.Exit
func syscall_Exit(code int) {
// Because this is the "unknown" target we can't call an exit function.
// But we also can't just return since the program will likely expect this
// function to never return. So we panic instead.
runtimePanic("unsupported: syscall.Exit")
}

// There is not yet any support for any form of parallelism on WebAssembly, so these
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/runtime_tinygowasmp2.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ func syscall_Exit(code int) {
exit.Exit(code != 0)
}

func mainReturnExit() {
// WASIp2 does not use _start, instead it uses _initialize and a custom
// WASIp2-specific main function. So this should never be called in
// practice.
runtimePanic("unreachable: _start was called")
}

// TinyGo does not yet support any form of parallelism on WebAssembly, so these
// can be left empty.

Expand Down
4 changes: 0 additions & 4 deletions src/runtime/runtime_wasip1.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,6 @@ func ticks() timeUnit {
return timeUnit(nano)
}

func beforeExit() {
__stdio_exit()
}

// Implementations of WASI APIs

//go:wasmimport wasi_snapshot_preview1 args_get
Expand Down
3 changes: 0 additions & 3 deletions src/runtime/runtime_wasip2.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,3 @@ func sleepTicks(d timeUnit) {
func ticks() timeUnit {
return timeUnit(monotonicclock.Now())
}

func beforeExit() {
}
4 changes: 0 additions & 4 deletions src/runtime/runtime_wasm_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,3 @@ func sleepTicks(d timeUnit)

//go:wasmimport gojs runtime.ticks
func ticks() timeUnit

func beforeExit() {
__stdio_exit()
}
5 changes: 4 additions & 1 deletion src/runtime/runtime_wasm_unknown.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ func ticks() timeUnit {
return timeUnit(0)
}

func beforeExit() {
func mainReturnExit() {
// Don't exit explicitly here. We can't (there is no environment with an
// exit call) but also it's not needed. We can just let _start and main.main
// return to the caller.
}
3 changes: 2 additions & 1 deletion src/runtime/runtime_wasmentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ func wasmEntryCommand() {
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
run()
if mainExited {
beforeExit()
// To make sure wasm_exec.js knows that we've exited, exit explicitly.
mainReturnExit()
}
}

Expand Down
46 changes: 33 additions & 13 deletions targets/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)

global.Go = class {
constructor() {
Expand Down Expand Up @@ -270,14 +271,11 @@
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
"proc_exit": (code) => {
if (global.process) {
// Node.js
process.exit(code);
} else {
// Can't exit in a browser.
throw 'trying to exit with code ' + code;
}
proc_exit: (code) => {
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
Expand All @@ -293,7 +291,14 @@
// func sleepTicks(timeout float64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(this._inst.exports.go_scheduler, timeout);
setTimeout(() => {
if (this.exited) return;
try {
this._inst.exports.go_scheduler();
} catch (e) {
if (e !== wasmExit) throw e;
}
}, timeout);
},

// func finalizeRef(v ref)
Expand Down Expand Up @@ -465,12 +470,23 @@
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
this.exitCode = 0;

if (this._inst.exports._start) {
this._inst.exports._start();
let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
});

// Run program, but catch the wasmExit exception that's thrown
// to return back here.
try {
this._inst.exports._start();
} catch (e) {
if (e !== wasmExit) throw e;
}

// TODO: wait until the program exists.
await new Promise(() => {});
await exitPromise;
return this.exitCode;
} else {
this._inst.exports._initialize();
}
Expand All @@ -480,7 +496,11 @@
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
try {
this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) {
this._resolveExitPromise();
}
Expand Down

0 comments on commit 5443a11

Please sign in to comment.