Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wasm: add //go:wasmexport support to js/wasm #4494

Merged
merged 2 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 62 additions & 28 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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')
Expand Down Expand Up @@ -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()

Expand Down
21 changes: 7 additions & 14 deletions src/runtime/runtime_wasm_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
// 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
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
}

var handleEvent func()

//go:linkname setEventHandler syscall/js.setEventHandler
Expand Down Expand Up @@ -50,3 +39,7 @@ func sleepTicks(d timeUnit)

//go:wasmimport gojs runtime.ticks
func ticks() timeUnit

func beforeExit() {
__stdio_exit()
}
2 changes: 1 addition & 1 deletion src/runtime/runtime_wasmentry.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build tinygo.wasm && !js
//go:build tinygo.wasm

package runtime

Expand Down
19 changes: 6 additions & 13 deletions targets/wasm_exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
await new Promise(() => {});
} else {
this._inst.exports._initialize();
}
}

Expand Down
40 changes: 40 additions & 0 deletions testdata/wasmexport.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading