Skip to content

Commit d9599ea

Browse files
committed
wasm: add //go:wasmexport support to js/wasm
This adds support for //go:wasmexport with `-target=wasm` (in the browser). This follows the //go:wasmexport proposal, meaning that blocking functions are not allowed. Both `-buildmode=default` and `-buildmode=c-shared` are supported. The latter allows calling exported functions after `go.run()` has returned.
1 parent 661d285 commit d9599ea

File tree

5 files changed

+94
-28
lines changed

5 files changed

+94
-28
lines changed

main_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,46 @@ func TestWasmExport(t *testing.T) {
690690
}
691691
}
692692

693+
// Test //go:wasmexport in JavaScript (using NodeJS).
694+
func TestWasmExportJS(t *testing.T) {
695+
type testCase struct {
696+
name string
697+
buildMode string
698+
}
699+
700+
tests := []testCase{
701+
{name: "default"},
702+
{name: "c-shared", buildMode: "c-shared"},
703+
}
704+
for _, tc := range tests {
705+
t.Run(tc.name, func(t *testing.T) {
706+
// Build the wasm binary.
707+
tmpdir := t.TempDir()
708+
options := optionsFromTarget("wasm", sema)
709+
options.BuildMode = tc.buildMode
710+
buildConfig, err := builder.NewConfig(&options)
711+
if err != nil {
712+
t.Fatal(err)
713+
}
714+
result, err := builder.Build("testdata/wasmexport-noscheduler.go", ".wasm", tmpdir, buildConfig)
715+
if err != nil {
716+
t.Fatal("failed to build binary:", err)
717+
}
718+
719+
// Test the resulting binary using NodeJS.
720+
output := &bytes.Buffer{}
721+
cmd := exec.Command("node", "testdata/wasmexport.js", result.Binary, buildConfig.BuildMode())
722+
cmd.Stdout = output
723+
cmd.Stderr = output
724+
err = cmd.Run()
725+
if err != nil {
726+
t.Error("failed to run node:", err)
727+
}
728+
checkOutput(t, "testdata/wasmexport.txt", output.Bytes())
729+
})
730+
}
731+
}
732+
693733
// Check whether the output of a test equals the expected output.
694734
func checkOutput(t *testing.T, filename string, actual []byte) {
695735
expectedOutput, err := os.ReadFile(filename)

src/runtime/runtime_wasm_js.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,15 @@
22

33
package runtime
44

5-
import "unsafe"
6-
75
type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript
86

97
// wasmNested is used to detect scheduler nesting (WASM calls into JS calls back into WASM).
108
// When this happens, we need to use a reduced version of the scheduler.
9+
//
10+
// TODO: this variable can probably be removed once //go:wasmexport is the only
11+
// allowed way to export a wasm function (currently, //export also works).
1112
var wasmNested bool
1213

13-
//export _start
14-
func _start() {
15-
// These need to be initialized early so that the heap can be initialized.
16-
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
17-
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
18-
19-
wasmNested = true
20-
run()
21-
__stdio_exit()
22-
wasmNested = false
23-
}
24-
2514
var handleEvent func()
2615

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

5140
//go:wasmimport gojs runtime.ticks
5241
func ticks() timeUnit
42+
43+
func beforeExit() {
44+
__stdio_exit()
45+
}

src/runtime/runtime_wasmentry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build tinygo.wasm && !js
1+
//go:build tinygo.wasm
22

33
package runtime
44

targets/wasm_exec.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -466,20 +466,13 @@
466466
this._idPool = []; // unused ids that have been garbage collected
467467
this.exited = false; // whether the Go program has exited
468468

469-
while (true) {
470-
const callbackPromise = new Promise((resolve) => {
471-
this._resolveCallbackPromise = () => {
472-
if (this.exited) {
473-
throw new Error("bad callback: Go program has already exited");
474-
}
475-
setTimeout(resolve, 0); // make sure it is asynchronous
476-
};
477-
});
469+
if (this._inst.exports._start) {
478470
this._inst.exports._start();
479-
if (this.exited) {
480-
break;
481-
}
482-
await callbackPromise;
471+
472+
// TODO: wait until the program exists.
473+
await new Promise(() => {});
474+
} else {
475+
this._inst.exports._initialize();
483476
}
484477
}
485478

testdata/wasmexport.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
require('../targets/wasm_exec.js');
2+
3+
function runTests() {
4+
let testCall = (name, params, expected) => {
5+
let result = go._inst.exports[name].apply(null, params);
6+
if (result !== expected) {
7+
console.error(`${name}(...${params}): expected result ${expected}, got ${result}`);
8+
}
9+
}
10+
11+
// These are the same tests as in TestWasmExport.
12+
testCall('hello', [], undefined);
13+
testCall('add', [3, 5], 8);
14+
testCall('add', [7, 9], 16);
15+
testCall('add', [6, 1], 7);
16+
testCall('reentrantCall', [2, 3], 5);
17+
testCall('reentrantCall', [1, 8], 9);
18+
}
19+
20+
let go = new Go();
21+
go.importObject.tester = {
22+
callOutside: (a, b) => {
23+
return go._inst.exports.add(a, b);
24+
},
25+
callTestMain: () => {
26+
runTests();
27+
},
28+
};
29+
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
30+
let buildMode = process.argv[3];
31+
if (buildMode === 'default') {
32+
go.run(result.instance);
33+
} else if (buildMode === 'c-shared') {
34+
go.run(result.instance);
35+
runTests();
36+
}
37+
}).catch((err) => {
38+
console.error(err);
39+
process.exit(1);
40+
});

0 commit comments

Comments
 (0)