diff --git a/main_test.go b/main_test.go index 3bbb31da3d..95fad9442b 100644 --- a/main_test.go +++ b/main_test.go @@ -396,17 +396,13 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c // of the path. path := TESTDATA + "/" + name // Get the expected output for this test. - txtpath := path[:len(path)-3] + ".txt" + expectedOutputPath := path[:len(path)-3] + ".txt" pkgName := "./" + path if path[len(path)-1] == '/' { - txtpath = path + "out.txt" + expectedOutputPath = path + "out.txt" options.Directory = path pkgName = "." } - expected, err := os.ReadFile(txtpath) - if err != nil { - t.Fatal("could not read expected output file:", err) - } config, err := builder.NewConfig(&options) if err != nil { @@ -428,10 +424,7 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c return } - // putchar() prints CRLF, convert it to LF. - actual := bytes.Replace(stdout.Bytes(), []byte{'\r', '\n'}, []byte{'\n'}, -1) - expected = bytes.Replace(expected, []byte{'\r', '\n'}, []byte{'\n'}, -1) // for Windows - + actual := stdout.Bytes() if config.EmulatorName() == "simavr" { // Strip simavr log formatting. actual = bytes.Replace(actual, []byte{0x1b, '[', '3', '2', 'm'}, nil, -1) @@ -446,17 +439,12 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c } // Check whether the command ran successfully. - fail := false if err != nil { - t.Log("failed to run:", err) - fail = true - } else if !bytes.Equal(expected, actual) { - t.Logf("output did not match (expected %d bytes, got %d bytes):", len(expected), len(actual)) - t.Logf(string(Diff("expected", expected, "actual", actual))) - fail = true + t.Error("failed to run:", err) } + checkOutput(t, expectedOutputPath, actual) - if fail { + if t.Failed() { r := bufio.NewReader(bytes.NewReader(actual)) for { line, err := r.ReadString('\n') @@ -696,21 +684,67 @@ func TestWasmExport(t *testing.T) { // Check that the output matches the expected output. // (Skip this for wasm-unknown because it can't produce output). if !tc.noOutput { - expectedOutput, err := os.ReadFile("testdata/wasmexport.txt") - if err != nil { - t.Fatal("could not read output file:", err) - } - actual := output.Bytes() - expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n")) - actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n")) - if !bytes.Equal(actual, expectedOutput) { - t.Error(string(Diff("expected", expectedOutput, "actual", actual))) - } + checkOutput(t, "testdata/wasmexport.txt", output.Bytes()) } }) } } +// Test //go:wasmexport in JavaScript (using NodeJS). +func TestWasmExportJS(t *testing.T) { + type testCase struct { + name string + buildMode string + } + + tests := []testCase{ + {name: "default"}, + {name: "c-shared", buildMode: "c-shared"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Build the wasm binary. + tmpdir := t.TempDir() + options := optionsFromTarget("wasm", sema) + options.BuildMode = tc.buildMode + buildConfig, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + result, err := builder.Build("testdata/wasmexport-noscheduler.go", ".wasm", tmpdir, buildConfig) + if err != nil { + t.Fatal("failed to build binary:", err) + } + + // Test the resulting binary using NodeJS. + output := &bytes.Buffer{} + cmd := exec.Command("node", "testdata/wasmexport.js", result.Binary, buildConfig.BuildMode()) + cmd.Stdout = output + cmd.Stderr = output + err = cmd.Run() + if err != nil { + t.Error("failed to run node:", err) + } + checkOutput(t, "testdata/wasmexport.txt", 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) + } + expectedOutput = bytes.ReplaceAll(expectedOutput, []byte("\r\n"), []byte("\n")) + actual = bytes.ReplaceAll(actual, []byte("\r\n"), []byte("\n")) + + if !bytes.Equal(actual, expectedOutput) { + t.Errorf("output did not match (expected %d bytes, got %d bytes):", len(expectedOutput), len(actual)) + t.Error(string(Diff("expected", expectedOutput, "actual", actual))) + } +} + func TestTest(t *testing.T) { t.Parallel() diff --git a/src/runtime/runtime_wasm_js.go b/src/runtime/runtime_wasm_js.go index 0b1aa5bc41..89898b554e 100644 --- a/src/runtime/runtime_wasm_js.go +++ b/src/runtime/runtime_wasm_js.go @@ -2,26 +2,15 @@ package runtime -import "unsafe" - type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript // wasmNested is used to detect scheduler nesting (WASM calls into JS calls back into WASM). // When this happens, we need to use a reduced version of the scheduler. +// +// TODO: this variable can probably be removed once //go:wasmexport is the only +// allowed way to export a wasm function (currently, //export also works). var wasmNested bool -//export _start -func _start() { - // These need to be initialized early so that the heap can be initialized. - heapStart = uintptr(unsafe.Pointer(&heapStartSymbol)) - heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) - - wasmNested = true - run() - __stdio_exit() - wasmNested = false -} - var handleEvent func() //go:linkname setEventHandler syscall/js.setEventHandler @@ -50,3 +39,7 @@ func sleepTicks(d timeUnit) //go:wasmimport gojs runtime.ticks func ticks() timeUnit + +func beforeExit() { + __stdio_exit() +} diff --git a/src/runtime/runtime_wasmentry.go b/src/runtime/runtime_wasmentry.go index 7bb1e1b44e..ff7b0c1198 100644 --- a/src/runtime/runtime_wasmentry.go +++ b/src/runtime/runtime_wasmentry.go @@ -1,4 +1,4 @@ -//go:build tinygo.wasm && !js +//go:build tinygo.wasm package runtime diff --git a/targets/wasm_exec.js b/targets/wasm_exec.js index 6902454409..c430cc2b23 100644 --- a/targets/wasm_exec.js +++ b/targets/wasm_exec.js @@ -466,20 +466,13 @@ this._idPool = []; // unused ids that have been garbage collected this.exited = false; // whether the Go program has exited - while (true) { - const callbackPromise = new Promise((resolve) => { - this._resolveCallbackPromise = () => { - if (this.exited) { - throw new Error("bad callback: Go program has already exited"); - } - setTimeout(resolve, 0); // make sure it is asynchronous - }; - }); + if (this._inst.exports._start) { this._inst.exports._start(); - if (this.exited) { - break; - } - await callbackPromise; + + // TODO: wait until the program exists. + await new Promise(() => {}); + } else { + this._inst.exports._initialize(); } } diff --git a/testdata/wasmexport.js b/testdata/wasmexport.js new file mode 100644 index 0000000000..c4a065125a --- /dev/null +++ b/testdata/wasmexport.js @@ -0,0 +1,40 @@ +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((result) => { + let buildMode = process.argv[3]; + if (buildMode === 'default') { + go.run(result.instance); + } else if (buildMode === 'c-shared') { + go.run(result.instance); + runTests(); + } +}).catch((err) => { + console.error(err); + process.exit(1); +});