mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-21 21:09:02 +00:00
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:
+53
-1
@@ -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"))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -52,6 +52,3 @@ func sleepTicks(d timeUnit) {
|
|||||||
func ticks() timeUnit {
|
func ticks() timeUnit {
|
||||||
return timeUnit(monotonicclock.Now())
|
return timeUnit(monotonicclock.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
func beforeExit() {
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+27
@@ -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")
|
||||||
|
}
|
||||||
Vendored
+35
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user