Skip to content

Commit

Permalink
wasm: add //go:wasmexport support to js/wasm
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aykevl committed Oct 3, 2024
1 parent 107be65 commit 647bd56
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 28 deletions.
40 changes: 40 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,46 @@ func TestWasmExport(t *testing.T) {
}
}

// 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)
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
// 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
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 && !wasip2 && !js
//go:build tinygo.wasm && !wasip2

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.
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);
});

0 comments on commit 647bd56

Please sign in to comment.