Skip to content

Commit bd6a3f2

Browse files
committed
builder, src/runtime, targets, tests/wasm: prototype WebAssembly reactor mode
The entrypoint for a WebAssembly reactor module is _initialize instead of _start. Assuming that the WebAssembly runtime is not reentrant (e.g. not threaded), then this works if the //go:wasmexport calls are detected and wrapped in a function that starts the Go scheduler. When the exported function ends, all other goroutines are paused. Goroutines started in a global init() function will run while the host has called into the guest. They are paused when the guest call returns and restarted on the next call. TODO: figure out how to enable reactor mode: 1. Should it be a flag to tinygo build? 2. Should it be a build tag (e.g. -tags reactor)? 3. Should the compiler detect the omission of main.main and automatically enable reactor mode? TODO: figure out where best to wrap //go:wasmexport calls. WIP
1 parent 7877500 commit bd6a3f2

File tree

10 files changed

+157
-4
lines changed

10 files changed

+157
-4
lines changed

builder/build.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
237237
// program so it's pretty fast and doesn't need to be parallelized.
238238
program := lprogram.LoadSSA()
239239

240+
// Determine if the program declares the main.main function.
241+
var hasMain bool
242+
for _, pkg := range program.AllPackages() {
243+
if pkg.Pkg.Name() != "main" {
244+
continue
245+
}
246+
if pkg.Func("main") != nil {
247+
hasMain = true
248+
break
249+
} else {
250+
// sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
251+
// fn := pkg.Prog.NewFunction("main", sig, "fake main function")
252+
// fn.Pkg = pkg
253+
// pkg.Members["main"] = fn
254+
}
255+
}
256+
println("hasMain =", hasMain)
257+
240258
// Add jobs to compile each package.
241259
// Packages that have a cache hit will not be compiled again.
242260
var packageJobs []*compileJob
@@ -523,6 +541,11 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
523541
}
524542
}
525543

544+
// Create empty main.main if not present
545+
if !hasMain {
546+
llvm.AddFunction(mod, "main.main", ctx.VoidType())
547+
}
548+
526549
// Create runtime.initAll function that calls the runtime
527550
// initializer of each package.
528551
llvmInitFn := mod.NamedFunction("runtime.initAll")
@@ -598,6 +621,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
598621
if err != nil {
599622
return result, err
600623
}
624+
601625
// Generate output.
602626
switch outext {
603627
case ".o":
@@ -645,6 +669,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
645669
result.Binary = result.Executable // final file
646670
ldflags := append(config.LDFlags(), "-o", result.Executable)
647671

672+
// Enable WebAssembly reactor mode if main.main is not declared.
673+
// This sets the entrypoint to _initialize instead of _start.
674+
if !hasMain {
675+
ldflags = append(ldflags, "--entry=_initialize")
676+
fmt.Println("☢️ REACTOR MODE ☢️")
677+
}
678+
648679
// Add compiler-rt dependency if needed. Usually this is a simple load from
649680
// a cache.
650681
if config.Target.RTLib == "compiler-rt" {

reactor.wasm

78.5 KB
Binary file not shown.

src/runtime/runtime_wasm_wasi.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ type timeUnit int64
1313
//export __wasm_call_ctors
1414
func __wasm_call_ctors()
1515

16+
// _initialize is the entrypoint for reactor programs
17+
//
18+
//export _initialize
19+
func _initialize() {
20+
// These need to be initialized early so that the heap can be initialized.
21+
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
22+
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
23+
runReactor() // does NOT call main
24+
}
25+
1626
//export _start
1727
func _start() {
1828
// These need to be initialized early so that the heap can be initialized.

src/runtime/scheduler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const asyncScheduler = GOOS == "js"
2323

2424
var schedulerDone bool
2525

26+
func setSchedulerDone(done bool) {
27+
schedulerDone = done
28+
}
29+
2630
// Queues used by the scheduler.
2731
var (
2832
runqueue task.Queue

src/runtime/scheduler_any_reactor.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//go:build !scheduler.none
2+
3+
package runtime
4+
5+
// runReactor is the program entry point for a WebAssembly reactor program, instead of run().
6+
// With a scheduler, init (but not main) functions are invoked in a goroutine before starting the scheduler.
7+
func runReactor() {
8+
initHeap()
9+
go func() {
10+
initAll()
11+
// main is NOT called
12+
schedulerDone = true
13+
}()
14+
scheduler()
15+
}

src/runtime/scheduler_none_reactor.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build scheduler.none
2+
3+
package runtime
4+
5+
// runReactor is the program entry point for a WebAssembly reactor program, instead of run().
6+
// With the "none" scheduler, init (but not main) functions are invoked directly.
7+
func runReactor() {
8+
initHeap()
9+
initAll()
10+
// main is NOT called
11+
}

targets/wasi.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
],
1818
"ldflags": [
1919
"--stack-first",
20-
"--no-demangle"
20+
"--no-demangle",
21+
"--entry=_initialize"
2122
],
2223
"extra-files": [
2324
"src/runtime/asm_tinygowasm.S"

tests/wasm/reactor_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package wasm
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestReactor(t *testing.T) {
9+
tmpDir := t.TempDir()
10+
11+
err := run(t, "tinygo build -x -o "+tmpDir+"/reactor.wasm -target wasi testdata/reactor.go")
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
16+
out, err := runout(t, "wasmtime run --invoke tinygo_test "+tmpDir+"/reactor.wasm")
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
21+
got := string(out)
22+
want := "1337\n"
23+
if !strings.Contains(got, want) {
24+
t.Errorf("reactor: expected %s, got %s", want, got)
25+
}
26+
}

tests/wasm/setup_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@ import (
1818
)
1919

2020
func run(t *testing.T, cmdline string) error {
21+
args := strings.Fields(cmdline)
22+
_, err := runargs(t, args...)
23+
return err
24+
}
25+
26+
func runout(t *testing.T, cmdline string) ([]byte, error) {
2127
args := strings.Fields(cmdline)
2228
return runargs(t, args...)
2329
}
2430

25-
func runargs(t *testing.T, args ...string) error {
31+
func runargs(t *testing.T, args ...string) ([]byte, error) {
2632
cmd := exec.Command(args[0], args[1:]...)
2733
b, err := cmd.CombinedOutput()
2834
t.Logf("Command: %s; err=%v; full output:\n%s", strings.Join(args, " "), err, b)
2935
if err != nil {
30-
return err
36+
return b, err
3137
}
32-
return nil
38+
return b, nil
3339
}
3440

3541
func chromectx(t *testing.T) context.Context {

tests/wasm/testdata/reactor.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"time"
5+
_ "unsafe"
6+
)
7+
8+
//go:linkname scheduler runtime.scheduler
9+
func scheduler()
10+
11+
//go:linkname setSchedulerDone runtime.setSchedulerDone
12+
func setSchedulerDone(bool)
13+
14+
// __go_wasm_export_tinygo_test is a wrapper function around tinygo_test
15+
// that runs the exported function in a goroutine and starts the scheduler.
16+
// Goroutines started by this or other functions will persist, are paused
17+
// when this function returns, and restarted when the host calls back into
18+
// another exported function.
19+
//
20+
//export tinygo_test
21+
func __go_wasm_export_tinygo_test() int32 {
22+
setSchedulerDone(false)
23+
var ret int32
24+
go func() {
25+
ret = tinygo_test()
26+
setSchedulerDone(true)
27+
}()
28+
scheduler()
29+
return ret
30+
}
31+
32+
func tinygo_test() int32 {
33+
for ticks != 1337 {
34+
time.Sleep(time.Nanosecond)
35+
}
36+
return ticks
37+
}
38+
39+
var ticks int32
40+
41+
func init() {
42+
// Start infinite ticker
43+
go func() {
44+
for {
45+
ticks++
46+
time.Sleep(time.Nanosecond)
47+
}
48+
}()
49+
}

0 commit comments

Comments
 (0)