wasm: correctly return from run() in wasm_exec.js

Instead of hanging forever, it should return the exit code from os.Exit.
This commit is contained in:
Ayke van Laethem
2024-10-30 12:24:21 +01:00
committed by Ayke
parent 04a7baec3e
commit ceb7891986
12 changed files with 173 additions and 30 deletions
+53 -1
View File
@@ -25,6 +25,7 @@ import (
"github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/sys"
"github.com/tinygo-org/tinygo/builder" "github.com/tinygo-org/tinygo/builder"
"github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/diagnostics" "github.com/tinygo-org/tinygo/diagnostics"
@@ -686,7 +687,14 @@ func TestWasmExport(t *testing.T) {
if tc.command { if tc.command {
// Call _start (the entry point), which calls // Call _start (the entry point), which calls
// tester.callTestMain, which then runs all the tests. // tester.callTestMain, which then runs all the tests.
mustCall(mod.ExportedFunction("_start").Call(ctx)) _, err := mod.ExportedFunction("_start").Call(ctx)
if err != nil {
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() == 0 {
// Exited with code 0. Nothing to worry about.
} else {
t.Error("failed to run _start:", err)
}
}
} else { } else {
// Run the _initialize call, because this is reactor mode wasm. // Run the _initialize call, because this is reactor mode wasm.
mustCall(mod.ExportedFunction("_initialize").Call(ctx)) mustCall(mod.ExportedFunction("_initialize").Call(ctx))
@@ -772,12 +780,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. // Check whether the output of a test equals the expected output.
func checkOutput(t *testing.T, filename string, actual []byte) { func checkOutput(t *testing.T, filename string, actual []byte) {
expectedOutput, err := os.ReadFile(filename) expectedOutput, err := os.ReadFile(filename)
if err != nil { if err != nil {
t.Fatal("could not read output file:", err) 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")) expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n"))
actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n")) actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n"))
+8 -3
View File
@@ -80,12 +80,17 @@ func abort() {
//go:linkname syscall_Exit syscall.Exit //go:linkname syscall_Exit syscall.Exit
func syscall_Exit(code int) { func syscall_Exit(code int) {
// TODO: should we call __stdio_exit here? // Flush stdio buffers.
// It's a low-level exit (syscall.Exit) so doing any libc stuff seems __stdio_exit()
// unexpected, but then where else should stdio buffers be flushed?
// Exit the program.
proc_exit(uint32(code)) proc_exit(uint32(code))
} }
func mainReturnExit() {
syscall_Exit(0)
}
// TinyGo does not yet support any form of parallelism on WebAssembly, so these // TinyGo does not yet support any form of parallelism on WebAssembly, so these
// can be left empty. // can be left empty.
@@ -31,6 +31,10 @@ func abort() {
//go:linkname syscall_Exit syscall.Exit //go:linkname syscall_Exit syscall.Exit
func syscall_Exit(code int) { 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 // There is not yet any support for any form of parallelism on WebAssembly, so these
+7
View File
@@ -60,6 +60,13 @@ func syscall_Exit(code int) {
exit.Exit(code != 0) 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 // TinyGo does not yet support any form of parallelism on WebAssembly, so these
// can be left empty. // can be left empty.
-4
View File
@@ -91,10 +91,6 @@ func ticks() timeUnit {
return timeUnit(nano) return timeUnit(nano)
} }
func beforeExit() {
__stdio_exit()
}
// Implementations of WASI APIs // Implementations of WASI APIs
//go:wasmimport wasi_snapshot_preview1 args_get //go:wasmimport wasi_snapshot_preview1 args_get
-3
View File
@@ -52,6 +52,3 @@ func sleepTicks(d timeUnit) {
func ticks() timeUnit { func ticks() timeUnit {
return timeUnit(monotonicclock.Now()) return timeUnit(monotonicclock.Now())
} }
func beforeExit() {
}
-4
View File
@@ -32,7 +32,3 @@ func sleepTicks(d timeUnit)
//go:wasmimport gojs runtime.ticks //go:wasmimport gojs runtime.ticks
func ticks() timeUnit func ticks() timeUnit
func beforeExit() {
__stdio_exit()
}
+4 -1
View File
@@ -34,5 +34,8 @@ func ticks() timeUnit {
return timeUnit(0) 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.
} }
+2 -1
View File
@@ -19,7 +19,8 @@ func wasmEntryCommand() {
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
run() run()
if mainExited { if mainExited {
beforeExit() // To make sure wasm_exec.js knows that we've exited, exit explicitly.
mainReturnExit()
} }
} }
+33 -13
View File
@@ -132,6 +132,7 @@
const decoder = new TextDecoder("utf-8"); const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8)); let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = []; var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
global.Go = class { global.Go = class {
constructor() { constructor() {
@@ -270,14 +271,11 @@
fd_close: () => 0, // dummy fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy fd_seek: () => 0, // dummy
"proc_exit": (code) => { proc_exit: (code) => {
if (global.process) { this.exited = true;
// Node.js this.exitCode = code;
process.exit(code); this._resolveExitPromise();
} else { throw wasmExit;
// Can't exit in a browser.
throw 'trying to exit with code ' + code;
}
}, },
random_get: (bufPtr, bufLen) => { random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen)); crypto.getRandomValues(loadSlice(bufPtr, bufLen));
@@ -293,7 +291,14 @@
// func sleepTicks(timeout float64) // func sleepTicks(timeout float64)
"runtime.sleepTicks": (timeout) => { "runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given 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) // func finalizeRef(v ref)
@@ -465,12 +470,23 @@
this._ids = new Map(); // mapping from JS values to reference ids this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited this.exited = false; // whether the Go program has exited
this.exitCode = 0;
if (this._inst.exports._start) { if (this._inst.exports._start) {
this._inst.exports._start(); let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
});
// TODO: wait until the program exists. // Run program, but catch the wasmExit exception that's thrown
await new Promise(() => {}); // to return back here.
try {
this._inst.exports._start();
} catch (e) {
if (e !== wasmExit) throw e;
}
await exitPromise;
return this.exitCode;
} else { } else {
this._inst.exports._initialize(); this._inst.exports._initialize();
} }
@@ -480,7 +496,11 @@
if (this.exited) { if (this.exited) {
throw new Error("Go program has already 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) { if (this.exited) {
this._resolveExitPromise(); this._resolveExitPromise();
} }
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"os"
"time"
)
func main() {
println("wasmexit test:", os.Args[1])
switch os.Args[1] {
case "normal":
return
case "exit-0":
os.Exit(0)
case "exit-0-sleep":
time.Sleep(time.Millisecond)
println("slept")
os.Exit(0)
case "exit-1":
os.Exit(1)
case "exit-1-sleep":
time.Sleep(time.Millisecond)
println("slept")
os.Exit(1)
}
println("unknown wasmexit test")
}
+35
View File
@@ -0,0 +1,35 @@
require('../targets/wasm_exec.js');
function runTests() {
let testCall = (name, params, expected) => {
let result = go._inst.exports[name].apply(null, params);
if (result !== expected) {
console.error(`${name}(...${params}): expected result ${expected}, got ${result}`);
}
}
// These are the same tests as in TestWasmExport.
testCall('hello', [], undefined);
testCall('add', [3, 5], 8);
testCall('add', [7, 9], 16);
testCall('add', [6, 1], 7);
testCall('reentrantCall', [2, 3], 5);
testCall('reentrantCall', [1, 8], 9);
}
let go = new Go();
go.importObject.tester = {
callOutside: (a, b) => {
return go._inst.exports.add(a, b);
},
callTestMain: () => {
runTests();
},
};
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
let value = await go.run(result.instance);
console.log('exit code:', value);
}).catch((err) => {
console.error(err);
process.exit(1);
});