mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-20 12:29:03 +00:00
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.
This commit is contained in:
committed by
Ron Evans
parent
6016d0c739
commit
4ef5109a07
@@ -690,6 +690,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.
|
// Check whether the output of a test equals the expected output.
|
||||||
func checkOutput(t *testing.T, filename string, actual []byte) {
|
func checkOutput(t *testing.T, filename string, actual []byte) {
|
||||||
expectedOutput, err := os.ReadFile(filename)
|
expectedOutput, err := os.ReadFile(filename)
|
||||||
|
|||||||
@@ -2,26 +2,15 @@
|
|||||||
|
|
||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import "unsafe"
|
|
||||||
|
|
||||||
type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript
|
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).
|
// 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.
|
// 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
|
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()
|
var handleEvent func()
|
||||||
|
|
||||||
//go:linkname setEventHandler syscall/js.setEventHandler
|
//go:linkname setEventHandler syscall/js.setEventHandler
|
||||||
@@ -50,3 +39,7 @@ func sleepTicks(d timeUnit)
|
|||||||
|
|
||||||
//go:wasmimport gojs runtime.ticks
|
//go:wasmimport gojs runtime.ticks
|
||||||
func ticks() timeUnit
|
func ticks() timeUnit
|
||||||
|
|
||||||
|
func beforeExit() {
|
||||||
|
__stdio_exit()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build tinygo.wasm && !js
|
//go:build tinygo.wasm
|
||||||
|
|
||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
|
|||||||
+6
-13
@@ -466,20 +466,13 @@
|
|||||||
this._idPool = []; // unused ids that have been garbage collected
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
this.exited = false; // whether the Go program has exited
|
this.exited = false; // whether the Go program has exited
|
||||||
|
|
||||||
while (true) {
|
if (this._inst.exports._start) {
|
||||||
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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this._inst.exports._start();
|
this._inst.exports._start();
|
||||||
if (this.exited) {
|
|
||||||
break;
|
// TODO: wait until the program exists.
|
||||||
}
|
await new Promise(() => {});
|
||||||
await callbackPromise;
|
} else {
|
||||||
|
this._inst.exports._initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+40
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user