diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go index 8d567c73..cde24da2 100644 --- a/cmd/wazero/wazero_test.go +++ b/cmd/wazero/wazero_test.go @@ -459,7 +459,7 @@ func TestRun(t *testing.T) { name: "timeout: a binary that exceeds the deadline should print an error", wazeroOpts: []string{"-timeout=1ms"}, wasm: wasmInfiniteLoop, - expectedStderr: "error: module \"\" closed with context deadline exceeded (timeout 1ms)\n", + expectedStderr: "error: module closed with context deadline exceeded (timeout 1ms)\n", expectedExitCode: int(sys.ExitCodeDeadlineExceeded), test: func(t *testing.T) { require.NoError(t, err) diff --git a/context_done_example_test.go b/context_done_example_test.go index 9f57c084..76a33847 100644 --- a/context_done_example_test.go +++ b/context_done_example_test.go @@ -45,7 +45,7 @@ func ExampleRuntimeConfig_WithCloseOnContextDone_context_timeout() { fmt.Println(err) // Output: - // module "malicious_wasm" closed with context deadline exceeded + // module closed with context deadline exceeded } // ExampleRuntimeConfig_WithCloseOnContextDone_context_cancel demonstrates how to ensure the termination @@ -82,7 +82,7 @@ func ExampleRuntimeConfig_WithCloseOnContextDone_context_cancel() { fmt.Println(err) // Output: - // module "malicious_wasm" closed with context canceled + // module closed with context canceled } // ExampleRuntimeConfig_WithCloseOnContextDone_moduleClose demonstrates how to ensure the termination @@ -120,5 +120,5 @@ func ExampleRuntimeConfig_WithCloseOnContextDone_moduleClose() { fmt.Println(err) // Output: - // module "malicious_wasm" closed with exit_code(1) + // module closed with exit_code(1) } diff --git a/imports/assemblyscript/assemblyscript.go b/imports/assemblyscript/assemblyscript.go index b7eaa536..78103f67 100644 --- a/imports/assemblyscript/assemblyscript.go +++ b/imports/assemblyscript/assemblyscript.go @@ -177,7 +177,7 @@ func abort(ctx context.Context, mod api.Module, _ []uint64) { _ = mod.CloseWithExitCode(ctx, exitCode) // Prevent any code from executing after this function. - panic(sys.NewExitError(mod.Name(), exitCode)) + panic(sys.NewExitError(exitCode)) } // traceDisabled ignores the input. diff --git a/imports/emscripten/emscripten_test.go b/imports/emscripten/emscripten_test.go index 78abb017..cf62e9d0 100644 --- a/imports/emscripten/emscripten_test.go +++ b/imports/emscripten/emscripten_test.go @@ -11,7 +11,6 @@ import ( "github.com/tetratelabs/wazero/experimental/logging" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/sys" ) // growWasm was compiled from testdata/grow.cc @@ -45,10 +44,9 @@ func TestGrow(t *testing.T) { _, err := Instantiate(ctx, r) require.NoError(t, err) - // Emscripten exits main with zero by default + // Emscripten exits main with zero by default, which coerces to nul. _, err = r.Instantiate(ctx, growWasm) - require.Error(t, err) - require.Zero(t, err.(*sys.ExitError).ExitCode()) + require.Nil(t, err) // We expect the memory no-op memory growth hook to be invoked as wasm. require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)") diff --git a/imports/wasi_snapshot_preview1/proc.go b/imports/wasi_snapshot_preview1/proc.go index 20b161d2..07c963ef 100644 --- a/imports/wasi_snapshot_preview1/proc.go +++ b/imports/wasi_snapshot_preview1/proc.go @@ -37,7 +37,7 @@ func procExitFn(ctx context.Context, mod api.Module, params []uint64) { // Prevent any code from executing after this function. For example, LLVM // inserts unreachable instructions after calls to exit. // See: https://github.com/emscripten-core/emscripten/issues/12322 - panic(sys.NewExitError(mod.Name(), exitCode)) + panic(sys.NewExitError(exitCode)) } // procRaise is stubbed and will never be supported, as it was removed. diff --git a/imports/wasi_snapshot_preview1/wasi_test.go b/imports/wasi_snapshot_preview1/wasi_test.go index a4e30d1a..63705c67 100644 --- a/imports/wasi_snapshot_preview1/wasi_test.go +++ b/imports/wasi_snapshot_preview1/wasi_test.go @@ -72,7 +72,7 @@ func TestNewFunctionExporter(t *testing.T) { _, err = r.Instantiate(testCtx, exitOnStartWasm) // Ensure the modified function was used! - require.Zero(t, err.(*sys.ExitError).ExitCode()) + require.Nil(t, err) }) } diff --git a/internal/gojs/argsenv_test.go b/internal/gojs/argsenv_test.go index 615837c1..d0ea2a0c 100644 --- a/internal/gojs/argsenv_test.go +++ b/internal/gojs/argsenv_test.go @@ -15,7 +15,7 @@ func Test_argsAndEnv(t *testing.T) { return moduleConfig.WithEnv("c", "d").WithEnv("a", "b"), config.NewConfig() }) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) require.Equal(t, ` args 0 = test diff --git a/internal/gojs/crypto_test.go b/internal/gojs/crypto_test.go index c4c58194..8e1d46c8 100644 --- a/internal/gojs/crypto_test.go +++ b/internal/gojs/crypto_test.go @@ -20,7 +20,7 @@ func Test_crypto(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "crypto", defaultConfig) require.Zero(t, stderr) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Equal(t, `7a0c9f9f0d `, stdout) require.Equal(t, `==> go.runtime.getRandomData(r_len=32) diff --git a/internal/gojs/fs_test.go b/internal/gojs/fs_test.go index 81def6d3..5fc3674f 100644 --- a/internal/gojs/fs_test.go +++ b/internal/gojs/fs_test.go @@ -20,7 +20,7 @@ func Test_fs(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Equal(t, `sub mode drwxr-xr-x /animals.txt mode -rw-r--r-- animals.txt mode -rw-r--r-- @@ -50,7 +50,7 @@ func Test_testfs(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stdout) } @@ -67,7 +67,7 @@ func Test_writefs(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) if platform.CompilerSupported() { // Note: as of Go 1.19, only the Sec field is set on update in fs_js.go. diff --git a/internal/gojs/http_test.go b/internal/gojs/http_test.go index 90ae0cf6..5ba1e261 100644 --- a/internal/gojs/http_test.go +++ b/internal/gojs/http_test.go @@ -46,7 +46,7 @@ func Test_http(t *testing.T) { return moduleConfig.WithEnv("BASE_URL", "http://host"), config }) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) require.Equal(t, `Get "http://host/error": net/http: fetch() failed: error 1 diff --git a/internal/gojs/misc_test.go b/internal/gojs/misc_test.go index e3ca36a6..6de70c6b 100644 --- a/internal/gojs/misc_test.go +++ b/internal/gojs/misc_test.go @@ -23,7 +23,7 @@ func Test_exit(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "exit", defaultConfig) - require.EqualError(t, err, `module "" closed with exit_code(255)`) + require.EqualError(t, err, `module closed with exit_code(255)`) require.Zero(t, stderr) require.Zero(t, stdout) require.Equal(t, `==> go.runtime.wasmExit(code=255) @@ -36,7 +36,7 @@ func Test_goroutine(t *testing.T) { stdout, stderr, err := compileAndRun(testCtx, "goroutine", defaultConfig) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) require.Equal(t, `producer consumer @@ -52,7 +52,7 @@ func Test_mem(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "mem", defaultConfig) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) require.Zero(t, stdout) @@ -71,7 +71,7 @@ func Test_stdio(t *testing.T) { }) require.Equal(t, "stderr 6\n", stderr) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Equal(t, "stdout 6\n", stdout) } @@ -89,7 +89,7 @@ func Test_stdio_large(t *testing.T) { return defaultConfig(moduleConfig.WithStdin(bytes.NewReader(input))) }) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Equal(t, fmt.Sprintf("stderr %d\n", size), stderr) require.Equal(t, fmt.Sprintf("stdout %d\n", size), stdout) @@ -107,7 +107,7 @@ func Test_gc(t *testing.T) { stdout, stderr, err := compileAndRun(testCtx, "gc", defaultConfig) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Equal(t, "", stderr) require.Equal(t, "before gc\nafter gc\n", stdout) } diff --git a/internal/gojs/process_test.go b/internal/gojs/process_test.go index 1e2a9c1c..efa6cb49 100644 --- a/internal/gojs/process_test.go +++ b/internal/gojs/process_test.go @@ -18,7 +18,7 @@ func Test_process(t *testing.T) { }) require.Zero(t, stderr) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Equal(t, `syscall.Getpid()=1 syscall.Getppid()=0 syscall.Getuid()=0 diff --git a/internal/gojs/time_test.go b/internal/gojs/time_test.go index 3e83fbca..5784ec89 100644 --- a/internal/gojs/time_test.go +++ b/internal/gojs/time_test.go @@ -19,7 +19,7 @@ func Test_time(t *testing.T) { stdout, stderr, err := compileAndRun(loggingCtx, "time", defaultConfig) - require.EqualError(t, err, `module "" closed with exit_code(0)`) + require.EqualError(t, err, `module closed with exit_code(0)`) require.Zero(t, stderr) require.Equal(t, `Local 1ms diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go index bd6a8ebf..ab3c19a6 100644 --- a/internal/integration_test/engine/adhoc_test.go +++ b/internal/integration_test/engine/adhoc_test.go @@ -3,7 +3,6 @@ package adhoc import ( "context" _ "embed" - "fmt" "math" "strconv" "testing" @@ -115,7 +114,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) { }() _, err = infinite.Call(ctx) require.Error(t, err) - require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context canceled", t.Name())) + require.Contains(t, err.Error(), "module closed with context canceled") }) t.Run("context cancel in advance", func(t *testing.T) { @@ -124,7 +123,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) { cancel() _, err = infinite.Call(ctx) require.Error(t, err) - require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context canceled", t.Name())) + require.Contains(t, err.Error(), "module closed with context canceled") }) t.Run("context timeout", func(t *testing.T) { @@ -133,7 +132,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) { defer cancel() _, err = infinite.Call(ctx) require.Error(t, err) - require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with context deadline exceeded", t.Name())) + require.Contains(t, err.Error(), "module closed with context deadline exceeded") }) t.Run("explicit close of module", func(t *testing.T) { @@ -144,7 +143,7 @@ func testEnsureTerminationOnClose(t *testing.T, r wazero.Runtime) { }() _, err = infinite.Call(context.Background()) require.Error(t, err) - require.Contains(t, err.Error(), fmt.Sprintf("module \"%s\" closed with exit_code(2)", t.Name())) + require.Contains(t, err.Error(), "module closed with exit_code(2)") }) } @@ -688,11 +687,11 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) { var expectedErr error if tc.closeImported != 0 && tc.closeImporting != 0 { // When both modules are closed, importing is the better one to choose in the error message. - expectedErr = sys.NewExitError(importing.Name(), tc.closeImporting) + expectedErr = sys.NewExitError(tc.closeImporting) } else if tc.closeImported != 0 { - expectedErr = sys.NewExitError(imported.Name(), tc.closeImported) + expectedErr = sys.NewExitError(tc.closeImported) } else if tc.closeImporting != 0 { - expectedErr = sys.NewExitError(importing.Name(), tc.closeImporting) + expectedErr = sys.NewExitError(tc.closeImporting) } else { t.Fatal("invalid test case") } diff --git a/internal/integration_test/engine/hammer_test.go b/internal/integration_test/engine/hammer_test.go index 6ccfcff4..a6b2deba 100644 --- a/internal/integration_test/engine/hammer_test.go +++ b/internal/integration_test/engine/hammer_test.go @@ -99,7 +99,7 @@ func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported i := importing // pin the module used inside goroutines hammer.NewHammer(t, P, 1).Run(func(name string) { // In all cases, the importing module is closed, so the error should have that as its module name. - requireFunctionCallExits(t, i.Name(), i.ExportedFunction("call_return_input")) + requireFunctionCallExits(t, i.ExportedFunction("call_return_input")) }, func() { // When all functions are in-flight, re-assign the modules. imported, importing = closeFn(imported, importing) // Unblock all the calls @@ -122,7 +122,7 @@ func requireFunctionCall(t *testing.T, fn api.Function) { require.Equal(t, uint64(3), res[0]) } -func requireFunctionCallExits(t *testing.T, moduleName string, fn api.Function) { +func requireFunctionCallExits(t *testing.T, fn api.Function) { _, err := fn.Call(testCtx, 3) - require.Equal(t, sys.NewExitError(moduleName, 0), err) + require.Equal(t, sys.NewExitError(0), err) } diff --git a/internal/testing/require/require.go b/internal/testing/require/require.go index b926c11c..909ac704 100644 --- a/internal/testing/require/require.go +++ b/internal/testing/require/require.go @@ -42,6 +42,10 @@ func Contains(t TestingT, s, substr string, formatWithArgs ...interface{}) { // // - formatWithArgs are optional. When the first is a string that contains '%', it is treated like fmt.Sprintf. func Equal(t TestingT, expected, actual interface{}, formatWithArgs ...interface{}) { + if expected == nil { + Nil(t, actual) + return + } if equal(expected, actual) { return } diff --git a/internal/wasm/call_context.go b/internal/wasm/call_context.go index 84e4f388..e868dd39 100644 --- a/internal/wasm/call_context.go +++ b/internal/wasm/call_context.go @@ -61,7 +61,7 @@ type CallContext struct { // FailIfClosed returns a sys.ExitError if CloseWithExitCode was called. func (m *CallContext) FailIfClosed() (err error) { if closed := atomic.LoadUint64(&m.Closed); closed != 0 { - return sys.NewExitError(m.module.Name, uint32(closed>>32)) // Unpack the high order bits as the exit code. + return sys.NewExitError(uint32(closed >> 32)) // Unpack the high order bits as the exit code. } return nil } diff --git a/internal/wasm/call_context_test.go b/internal/wasm/call_context_test.go index 829a992d..79c9f23d 100644 --- a/internal/wasm/call_context_test.go +++ b/internal/wasm/call_context_test.go @@ -271,7 +271,7 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) { defer done() err := cc.FailIfClosed() - require.EqualError(t, err, "module \"test\" closed with context deadline exceeded") + require.EqualError(t, err, "module closed with context deadline exceeded") }) t.Run("cancel", func(t *testing.T) { @@ -286,7 +286,7 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) { time.Sleep(time.Second) err := cc.FailIfClosed() - require.EqualError(t, err, "module \"test\" closed with context canceled") + require.EqualError(t, err, "module closed with context canceled") }) t.Run("timeout over cancel", func(t *testing.T) { @@ -316,7 +316,7 @@ func TestCallContext_CloseModuleOnCanceledOrTimeout(t *testing.T) { time.Sleep(time.Second) err := cc.FailIfClosed() - require.EqualError(t, err, "module \"test\" closed with context canceled") + require.EqualError(t, err, "module closed with context canceled") }) t.Run("cancel works", func(t *testing.T) { @@ -359,7 +359,7 @@ func TestCallContext_CloseWithCtxErr(t *testing.T) { cc.CloseWithCtxErr(ctx) err := cc.FailIfClosed() - require.EqualError(t, err, "module \"test\" closed with context canceled") + require.EqualError(t, err, "module closed with context canceled") }) t.Run("context timeout", func(t *testing.T) { @@ -373,7 +373,7 @@ func TestCallContext_CloseWithCtxErr(t *testing.T) { cc.CloseWithCtxErr(ctx) err := cc.FailIfClosed() - require.EqualError(t, err, "module \"test\" closed with context deadline exceeded") + require.EqualError(t, err, "module closed with context deadline exceeded") }) t.Run("no error", func(t *testing.T) { diff --git a/runtime.go b/runtime.go index 2d8f9461..ba34a324 100644 --- a/runtime.go +++ b/runtime.go @@ -34,11 +34,14 @@ type Runtime interface { // // mod, _ := r.Instantiate(ctx, wasm) // - // See InstantiateWithConfig for configuration overrides. + // # Notes + // + // - See notes on InstantiateModule for error scenarios. + // - See InstantiateWithConfig for configuration overrides. Instantiate(ctx context.Context, source []byte) (api.Module, error) // InstantiateWithConfig instantiates a module from the WebAssembly binary - // (%.wasm) or errs if invalid. + // (%.wasm) or errs for reasons including exit or validation. // // Here's an example: // ctx := context.Background() @@ -50,11 +53,12 @@ type Runtime interface { // // # Notes // + // - See notes on InstantiateModule for error scenarios. + // - If you aren't overriding defaults, use Instantiate. // - This is a convenience utility that chains CompileModule with // InstantiateModule. To instantiate the same source multiple times, // use CompileModule as InstantiateModule avoids redundant decoding // and/or compilation. - // - If you aren't overriding defaults, use Instantiate. InstantiateWithConfig(ctx context.Context, source []byte, config ModuleConfig) (api.Module, error) // NewHostModuleBuilder lets you create modules out of functions defined in Go. @@ -87,15 +91,25 @@ type Runtime interface { // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) - // InstantiateModule instantiates the module or errs if the configuration was invalid. + // InstantiateModule instantiates the module or errs for reasons including + // exit or validation. // // Here's an example: - // mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("prod")) + // mod, _ := n.InstantiateModule(ctx, compiled, wazero.NewModuleConfig(). + // WithName("prod")) // - // While CompiledModule is pre-validated, there are a few situations which can cause an error: + // # Errors + // + // While CompiledModule is pre-validated, there are a few situations which + // can cause an error: // - The module name is already in use. - // - The module has a table element initializer that resolves to an index outside the Table minimum size. + // - The module has a table element initializer that resolves to an index + // outside the Table minimum size. // - The module has a start function, and it failed to execute. + // - The module was compiled to WASI and exited with a non-zero exit + // code, you'll receive a sys.ExitError. + // - RuntimeConfig.WithCloseOnContextDone was enabled and a context + // cancellation or deadline triggered before a start function returned. InstantiateModule(ctx context.Context, compiled CompiledModule, config ModuleConfig) (api.Module, error) // CloseWithExitCode closes all the modules that have been initialized in this Runtime with the provided exit code. @@ -294,6 +308,11 @@ func (r *runtime) InstantiateModule( if code.closeWithModule { _ = code.Close(ctx) // don't overwrite the error } + if se, ok := err.(*sys.ExitError); ok { + if se.ExitCode() == 0 { // Don't err on success. + err = nil + } + } return } @@ -310,7 +329,11 @@ func (r *runtime) InstantiateModule( } if _, err = start.Call(ctx); err != nil { _ = mod.Close(ctx) // Don't leak the module on error. - if _, ok := err.(*sys.ExitError); ok { + + if se, ok := err.(*sys.ExitError); ok { + if se.ExitCode() == 0 { // Don't err on success. + err = nil + } return // Don't wrap an exit error } err = fmt.Errorf("module[%s] function[%s] failed: %w", name, fn, err) diff --git a/runtime_test.go b/runtime_test.go index 1b5dfe92..331d6026 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -471,32 +471,73 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) { r := NewRuntime(testCtx) defer r.Close(testCtx) - start := func(ctx context.Context, m api.Module) { - require.NoError(t, m.CloseWithExitCode(ctx, 2)) + tests := []struct { + name string + exitCode uint32 + export bool + expectedErr error + }{ + { + name: "start: exit code 0", + exitCode: 0, + }, + { + name: "start: exit code 2", + exitCode: 2, + expectedErr: sys.NewExitError(2), + }, + { + name: "_start: exit code 0", + exitCode: 0, + export: true, + }, + { + name: "_start: exit code 2", + exitCode: 2, + export: true, + expectedErr: sys.NewExitError(2), + }, } - _, err := r.NewHostModuleBuilder("env"). - NewFunctionBuilder().WithFunc(start).Export("exit"). - Instantiate(testCtx) - require.NoError(t, err) + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + start := func(ctx context.Context, m api.Module) { + require.NoError(t, m.CloseWithExitCode(ctx, tc.exitCode)) + } - one := uint32(1) - binary := binaryencoding.EncodeModule(&wasm.Module{ - TypeSection: []wasm.FunctionType{{}}, - ImportSection: []wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}}, - FunctionSection: []wasm.Index{0}, - CodeSection: []wasm.Code{ - {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start. - }, - StartSection: &one, - }) + env, err := r.NewHostModuleBuilder("env"). + NewFunctionBuilder().WithFunc(start).Export("exit"). + Instantiate(testCtx) + require.NoError(t, err) + defer env.Close(testCtx) - // Instantiate the module, which calls the start function. - _, err = r.InstantiateWithConfig(testCtx, binary, - NewModuleConfig().WithName("call-exit")) + mod := &wasm.Module{ + TypeSection: []wasm.FunctionType{{}}, + ImportSection: []wasm.Import{{Module: "env", Name: "exit", Type: wasm.ExternTypeFunc, DescFunc: 0}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []wasm.Code{ + {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Call the imported env.start. + }, + } + if tc.export { + mod.ExportSection = []wasm.Export{ + {Name: "_start", Type: wasm.ExternTypeFunc, Index: 1}, + } + } else { + one := uint32(1) + mod.StartSection = &one + } + binary := binaryencoding.EncodeModule(mod) - // Ensure the exit error propagated and didn't wrap. - require.Equal(t, err, sys.NewExitError("call-exit", 2)) + // Instantiate the module, which calls the start function. + _, err = r.InstantiateWithConfig(testCtx, binary, + NewModuleConfig().WithName("call-exit")) + + // Ensure the exit error propagated and didn't wrap. + require.Equal(t, tc.expectedErr, err) + }) + } } func TestRuntime_CloseWithExitCode(t *testing.T) { @@ -559,10 +600,10 @@ func TestRuntime_CloseWithExitCode(t *testing.T) { // Modules closed so calls fail _, err = func1.Call(testCtx) - require.ErrorIs(t, err, sys.NewExitError("mod1", tc.exitCode)) + require.ErrorIs(t, err, sys.NewExitError(tc.exitCode)) _, err = func2.Call(testCtx) - require.ErrorIs(t, err, sys.NewExitError("mod2", tc.exitCode)) + require.ErrorIs(t, err, sys.NewExitError(tc.exitCode)) }) } } diff --git a/sys/error.go b/sys/error.go index 5c5ecd14..9aea47b1 100644 --- a/sys/error.go +++ b/sys/error.go @@ -25,7 +25,7 @@ const ( // main := module.ExportedFunction("main") // if err := main(ctx); err != nil { // if exitErr, ok := err.(*sys.ExitError); ok { -// // If your main function expects to exit, this could be ok if Code == 0 +// // This means your module exited with non-zero code! // } // --snip-- // @@ -37,17 +37,18 @@ const ( // // Note: In the case of context cancellation or timeout, the api.Module from which the api.Function created is closed. type ExitError struct { - moduleName string - exitCode uint32 + // Note: this is a struct not a uint32 type as it was originally one and + // we don't want to break call-sites that cast into it. + exitCode uint32 } -func NewExitError(moduleName string, exitCode uint32) *ExitError { - return &ExitError{moduleName: moduleName, exitCode: exitCode} -} +var exitZero = &ExitError{} -// ModuleName is the api.Module that was closed. -func (e *ExitError) ModuleName() string { - return e.moduleName +func NewExitError(exitCode uint32) *ExitError { + if exitCode == 0 { + return exitZero + } + return &ExitError{exitCode: exitCode} } // ExitCode returns zero on success, and an arbitrary value otherwise. @@ -59,18 +60,18 @@ func (e *ExitError) ExitCode() uint32 { func (e *ExitError) Error() string { switch e.exitCode { case ExitCodeContextCanceled: - return fmt.Sprintf("module %q closed with %s", e.moduleName, context.Canceled) + return fmt.Sprintf("module closed with %s", context.Canceled) case ExitCodeDeadlineExceeded: - return fmt.Sprintf("module %q closed with %s", e.moduleName, context.DeadlineExceeded) + return fmt.Sprintf("module closed with %s", context.DeadlineExceeded) default: - return fmt.Sprintf("module %q closed with exit_code(%d)", e.moduleName, e.exitCode) + return fmt.Sprintf("module closed with exit_code(%d)", e.exitCode) } } // Is allows use via errors.Is func (e *ExitError) Is(err error) bool { if target, ok := err.(*ExitError); ok { - return e.moduleName == target.moduleName && e.exitCode == target.exitCode + return e.exitCode == target.exitCode } return false } diff --git a/sys/error_test.go b/sys/error_test.go index ae601b54..b1b278c6 100644 --- a/sys/error_test.go +++ b/sys/error_test.go @@ -8,8 +8,7 @@ import ( ) type notExitError struct { - moduleName string - exitCode uint32 + exitCode uint32 } func (e *notExitError) Error() string { @@ -17,7 +16,7 @@ func (e *notExitError) Error() string { } func TestIs(t *testing.T) { - err := NewExitError("some module", 2) + err := NewExitError(2) tests := []struct { name string target error @@ -28,26 +27,15 @@ func TestIs(t *testing.T) { target: err, matches: true, }, - { - name: "same content", - target: NewExitError("some module", 2), - matches: true, - }, - { - name: "different module name", - target: NewExitError("not some module", 2), - matches: false, - }, { name: "different exit code", - target: NewExitError("some module", 0), + target: NewExitError(1), matches: false, }, { name: "different type", target: ¬ExitError{ - moduleName: "some module", - exitCode: 2, + exitCode: 2, }, matches: false, }, @@ -64,18 +52,18 @@ func TestIs(t *testing.T) { func TestExitError_Error(t *testing.T) { t.Run("timeout", func(t *testing.T) { - err := NewExitError("foo", ExitCodeDeadlineExceeded) + err := NewExitError(ExitCodeDeadlineExceeded) require.Equal(t, ExitCodeDeadlineExceeded, err.ExitCode()) - require.EqualError(t, err, "module \"foo\" closed with context deadline exceeded") + require.EqualError(t, err, "module closed with context deadline exceeded") }) t.Run("cancel", func(t *testing.T) { - err := NewExitError("foo", ExitCodeContextCanceled) + err := NewExitError(ExitCodeContextCanceled) require.Equal(t, ExitCodeContextCanceled, err.ExitCode()) - require.EqualError(t, err, "module \"foo\" closed with context canceled") + require.EqualError(t, err, "module closed with context canceled") }) t.Run("normal", func(t *testing.T) { - err := NewExitError("foo", 123) + err := NewExitError(123) require.Equal(t, uint32(123), err.ExitCode()) - require.EqualError(t, err, "module \"foo\" closed with exit_code(123)") + require.EqualError(t, err, "module closed with exit_code(123)") }) }