From 5443a119a80edcbb3f293b4e15e768056467216d Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 30 Oct 2024 12:24:21 +0100 Subject: [PATCH] wasm: correctly return from run() in wasm_exec.js Instead of hanging forever, it should return the exit code from os.Exit. --- main_test.go | 44 ++++++++++++++++++++++ src/runtime/runtime_tinygowasm.go | 11 ++++-- src/runtime/runtime_tinygowasm_unknown.go | 4 ++ src/runtime/runtime_tinygowasmp2.go | 7 ++++ src/runtime/runtime_wasip1.go | 4 -- src/runtime/runtime_wasip2.go | 3 -- src/runtime/runtime_wasm_js.go | 4 -- src/runtime/runtime_wasm_unknown.go | 5 ++- src/runtime/runtime_wasmentry.go | 3 +- targets/wasm_exec.js | 46 ++++++++++++++++------- 10 files changed, 102 insertions(+), 29 deletions(-) diff --git a/main_test.go b/main_test.go index 136128d51c..c0e0e164cf 100644 --- a/main_test.go +++ b/main_test.go @@ -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")) diff --git a/src/runtime/runtime_tinygowasm.go b/src/runtime/runtime_tinygowasm.go index f791ffacdf..7bc65e9c44 100644 --- a/src/runtime/runtime_tinygowasm.go +++ b/src/runtime/runtime_tinygowasm.go @@ -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. diff --git a/src/runtime/runtime_tinygowasm_unknown.go b/src/runtime/runtime_tinygowasm_unknown.go index 39caa245a2..e426f36ff8 100644 --- a/src/runtime/runtime_tinygowasm_unknown.go +++ b/src/runtime/runtime_tinygowasm_unknown.go @@ -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 diff --git a/src/runtime/runtime_tinygowasmp2.go b/src/runtime/runtime_tinygowasmp2.go index eb3c507fd2..70b5a6d11e 100644 --- a/src/runtime/runtime_tinygowasmp2.go +++ b/src/runtime/runtime_tinygowasmp2.go @@ -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. diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index ad66b0d860..92adb9bef6 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -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 diff --git a/src/runtime/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index ba8f52100b..296f4a45bd 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -52,6 +52,3 @@ func sleepTicks(d timeUnit) { func ticks() timeUnit { return timeUnit(monotonicclock.Now()) } - -func beforeExit() { -} diff --git a/src/runtime/runtime_wasm_js.go b/src/runtime/runtime_wasm_js.go index b49ffd15d6..21a0bc1055 100644 --- a/src/runtime/runtime_wasm_js.go +++ b/src/runtime/runtime_wasm_js.go @@ -32,7 +32,3 @@ func sleepTicks(d timeUnit) //go:wasmimport gojs runtime.ticks func ticks() timeUnit - -func beforeExit() { - __stdio_exit() -} diff --git a/src/runtime/runtime_wasm_unknown.go b/src/runtime/runtime_wasm_unknown.go index 846b95d2a8..27e2485791 100644 --- a/src/runtime/runtime_wasm_unknown.go +++ b/src/runtime/runtime_wasm_unknown.go @@ -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. } diff --git a/src/runtime/runtime_wasmentry.go b/src/runtime/runtime_wasmentry.go index 756db50955..1d2cec6cae 100644 --- a/src/runtime/runtime_wasmentry.go +++ b/src/runtime/runtime_wasmentry.go @@ -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() } } diff --git a/targets/wasm_exec.js b/targets/wasm_exec.js index c430cc2b23..d6270adbfd 100644 --- a/targets/wasm_exec.js +++ b/targets/wasm_exec.js @@ -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() { @@ -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)); @@ -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) @@ -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(); } @@ -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(); }