Skip to content

Commit ceb7891

Browse files
committed
wasm: correctly return from run() in wasm_exec.js
Instead of hanging forever, it should return the exit code from os.Exit.
1 parent 04a7bae commit ceb7891

12 files changed

+173
-30
lines changed

main_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/tetratelabs/wazero"
2626
"github.com/tetratelabs/wazero/api"
2727
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
28+
"github.com/tetratelabs/wazero/sys"
2829
"github.com/tinygo-org/tinygo/builder"
2930
"github.com/tinygo-org/tinygo/compileopts"
3031
"github.com/tinygo-org/tinygo/diagnostics"
@@ -686,7 +687,14 @@ func TestWasmExport(t *testing.T) {
686687
if tc.command {
687688
// Call _start (the entry point), which calls
688689
// tester.callTestMain, which then runs all the tests.
689-
mustCall(mod.ExportedFunction("_start").Call(ctx))
690+
_, err := mod.ExportedFunction("_start").Call(ctx)
691+
if err != nil {
692+
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() == 0 {
693+
// Exited with code 0. Nothing to worry about.
694+
} else {
695+
t.Error("failed to run _start:", err)
696+
}
697+
}
690698
} else {
691699
// Run the _initialize call, because this is reactor mode wasm.
692700
mustCall(mod.ExportedFunction("_initialize").Call(ctx))
@@ -772,12 +780,56 @@ func TestWasmExportJS(t *testing.T) {
772780
}
773781
}
774782

783+
// Test whether Go.run() (in wasm_exec.js) normally returns and returns the
784+
// right exit code.
785+
func TestWasmExit(t *testing.T) {
786+
t.Parallel()
787+
788+
type testCase struct {
789+
name string
790+
output string
791+
}
792+
793+
tests := []testCase{
794+
{name: "normal", output: "exit code: 0\n"},
795+
{name: "exit-0", output: "exit code: 0\n"},
796+
{name: "exit-0-sleep", output: "slept\nexit code: 0\n"},
797+
{name: "exit-1", output: "exit code: 1\n"},
798+
{name: "exit-1-sleep", output: "slept\nexit code: 1\n"},
799+
}
800+
for _, tc := range tests {
801+
tc := tc
802+
t.Run(tc.name, func(t *testing.T) {
803+
t.Parallel()
804+
options := optionsFromTarget("wasm", sema)
805+
buildConfig, err := builder.NewConfig(&options)
806+
if err != nil {
807+
t.Fatal(err)
808+
}
809+
buildConfig.Target.Emulator = "node testdata/wasmexit.js {}"
810+
output := &bytes.Buffer{}
811+
_, err = buildAndRun("testdata/wasmexit.go", buildConfig, output, []string{tc.name}, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
812+
return cmd.Run()
813+
})
814+
if err != nil {
815+
t.Error(err)
816+
}
817+
expected := "wasmexit test: " + tc.name + "\n" + tc.output
818+
checkOutputData(t, []byte(expected), output.Bytes())
819+
})
820+
}
821+
}
822+
775823
// Check whether the output of a test equals the expected output.
776824
func checkOutput(t *testing.T, filename string, actual []byte) {
777825
expectedOutput, err := os.ReadFile(filename)
778826
if err != nil {
779827
t.Fatal("could not read output file:", err)
780828
}
829+
checkOutputData(t, expectedOutput, actual)
830+
}
831+
832+
func checkOutputData(t *testing.T, expectedOutput, actual []byte) {
781833
expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n"))
782834
actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n"))
783835

src/runtime/runtime_tinygowasm.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,17 @@ func abort() {
8080

8181
//go:linkname syscall_Exit syscall.Exit
8282
func syscall_Exit(code int) {
83-
// TODO: should we call __stdio_exit here?
84-
// It's a low-level exit (syscall.Exit) so doing any libc stuff seems
85-
// unexpected, but then where else should stdio buffers be flushed?
83+
// Flush stdio buffers.
84+
__stdio_exit()
85+
86+
// Exit the program.
8687
proc_exit(uint32(code))
8788
}
8889

90+
func mainReturnExit() {
91+
syscall_Exit(0)
92+
}
93+
8994
// TinyGo does not yet support any form of parallelism on WebAssembly, so these
9095
// can be left empty.
9196

src/runtime/runtime_tinygowasm_unknown.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ func abort() {
3131

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

3640
// There is not yet any support for any form of parallelism on WebAssembly, so these

src/runtime/runtime_tinygowasmp2.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ func syscall_Exit(code int) {
6060
exit.Exit(code != 0)
6161
}
6262

63+
func mainReturnExit() {
64+
// WASIp2 does not use _start, instead it uses _initialize and a custom
65+
// WASIp2-specific main function. So this should never be called in
66+
// practice.
67+
runtimePanic("unreachable: _start was called")
68+
}
69+
6370
// TinyGo does not yet support any form of parallelism on WebAssembly, so these
6471
// can be left empty.
6572

src/runtime/runtime_wasip1.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,6 @@ func ticks() timeUnit {
9191
return timeUnit(nano)
9292
}
9393

94-
func beforeExit() {
95-
__stdio_exit()
96-
}
97-
9894
// Implementations of WASI APIs
9995

10096
//go:wasmimport wasi_snapshot_preview1 args_get

src/runtime/runtime_wasip2.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,3 @@ func sleepTicks(d timeUnit) {
5252
func ticks() timeUnit {
5353
return timeUnit(monotonicclock.Now())
5454
}
55-
56-
func beforeExit() {
57-
}

src/runtime/runtime_wasm_js.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,3 @@ func sleepTicks(d timeUnit)
3232

3333
//go:wasmimport gojs runtime.ticks
3434
func ticks() timeUnit
35-
36-
func beforeExit() {
37-
__stdio_exit()
38-
}

src/runtime/runtime_wasm_unknown.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ func ticks() timeUnit {
3434
return timeUnit(0)
3535
}
3636

37-
func beforeExit() {
37+
func mainReturnExit() {
38+
// Don't exit explicitly here. We can't (there is no environment with an
39+
// exit call) but also it's not needed. We can just let _start and main.main
40+
// return to the caller.
3841
}

src/runtime/runtime_wasmentry.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ func wasmEntryCommand() {
1919
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
2020
run()
2121
if mainExited {
22-
beforeExit()
22+
// To make sure wasm_exec.js knows that we've exited, exit explicitly.
23+
mainReturnExit()
2324
}
2425
}
2526

targets/wasm_exec.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
const decoder = new TextDecoder("utf-8");
133133
let reinterpretBuf = new DataView(new ArrayBuffer(8));
134134
var logLine = [];
135+
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
135136

136137
global.Go = class {
137138
constructor() {
@@ -270,14 +271,11 @@
270271
fd_close: () => 0, // dummy
271272
fd_fdstat_get: () => 0, // dummy
272273
fd_seek: () => 0, // dummy
273-
"proc_exit": (code) => {
274-
if (global.process) {
275-
// Node.js
276-
process.exit(code);
277-
} else {
278-
// Can't exit in a browser.
279-
throw 'trying to exit with code ' + code;
280-
}
274+
proc_exit: (code) => {
275+
this.exited = true;
276+
this.exitCode = code;
277+
this._resolveExitPromise();
278+
throw wasmExit;
281279
},
282280
random_get: (bufPtr, bufLen) => {
283281
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
@@ -293,7 +291,14 @@
293291
// func sleepTicks(timeout float64)
294292
"runtime.sleepTicks": (timeout) => {
295293
// Do not sleep, only reactivate scheduler after the given timeout.
296-
setTimeout(this._inst.exports.go_scheduler, timeout);
294+
setTimeout(() => {
295+
if (this.exited) return;
296+
try {
297+
this._inst.exports.go_scheduler();
298+
} catch (e) {
299+
if (e !== wasmExit) throw e;
300+
}
301+
}, timeout);
297302
},
298303

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

469475
if (this._inst.exports._start) {
470-
this._inst.exports._start();
476+
let exitPromise = new Promise((resolve, reject) => {
477+
this._resolveExitPromise = resolve;
478+
});
479+
480+
// Run program, but catch the wasmExit exception that's thrown
481+
// to return back here.
482+
try {
483+
this._inst.exports._start();
484+
} catch (e) {
485+
if (e !== wasmExit) throw e;
486+
}
471487

472-
// TODO: wait until the program exists.
473-
await new Promise(() => {});
488+
await exitPromise;
489+
return this.exitCode;
474490
} else {
475491
this._inst.exports._initialize();
476492
}
@@ -480,7 +496,11 @@
480496
if (this.exited) {
481497
throw new Error("Go program has already exited");
482498
}
483-
this._inst.exports.resume();
499+
try {
500+
this._inst.exports.resume();
501+
} catch (e) {
502+
if (e !== wasmExit) throw e;
503+
}
484504
if (this.exited) {
485505
this._resolveExitPromise();
486506
}

0 commit comments

Comments
 (0)