Skip to content

Commit

Permalink
builder, src/runtime, targets, tests/wasm: prototype WebAssembly reac…
Browse files Browse the repository at this point in the history
…tor 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
  • Loading branch information
ydnar committed Mar 20, 2024
1 parent 7877500 commit bd6a3f2
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 4 deletions.
31 changes: 31 additions & 0 deletions builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,24 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
// program so it's pretty fast and doesn't need to be parallelized.
program := lprogram.LoadSSA()

// Determine if the program declares the main.main function.
var hasMain bool
for _, pkg := range program.AllPackages() {
if pkg.Pkg.Name() != "main" {
continue
}
if pkg.Func("main") != nil {
hasMain = true
break
} else {
// sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
// fn := pkg.Prog.NewFunction("main", sig, "fake main function")
// fn.Pkg = pkg
// pkg.Members["main"] = fn
}
}
println("hasMain =", hasMain)

// Add jobs to compile each package.
// Packages that have a cache hit will not be compiled again.
var packageJobs []*compileJob
Expand Down Expand Up @@ -523,6 +541,11 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
}
}

// Create empty main.main if not present
if !hasMain {
llvm.AddFunction(mod, "main.main", ctx.VoidType())
}

// Create runtime.initAll function that calls the runtime
// initializer of each package.
llvmInitFn := mod.NamedFunction("runtime.initAll")
Expand Down Expand Up @@ -598,6 +621,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
if err != nil {
return result, err
}

// Generate output.
switch outext {
case ".o":
Expand Down Expand Up @@ -645,6 +669,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
result.Binary = result.Executable // final file
ldflags := append(config.LDFlags(), "-o", result.Executable)

// Enable WebAssembly reactor mode if main.main is not declared.
// This sets the entrypoint to _initialize instead of _start.
if !hasMain {
ldflags = append(ldflags, "--entry=_initialize")
fmt.Println("☢️ REACTOR MODE ☢️")
}

// Add compiler-rt dependency if needed. Usually this is a simple load from
// a cache.
if config.Target.RTLib == "compiler-rt" {
Expand Down
Binary file added reactor.wasm
Binary file not shown.
10 changes: 10 additions & 0 deletions src/runtime/runtime_wasm_wasi.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ type timeUnit int64
//export __wasm_call_ctors
func __wasm_call_ctors()

// _initialize is the entrypoint for reactor programs
//
//export _initialize
func _initialize() {
// 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)
runReactor() // does NOT call main
}

//export _start
func _start() {
// These need to be initialized early so that the heap can be initialized.
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const asyncScheduler = GOOS == "js"

var schedulerDone bool

func setSchedulerDone(done bool) {
schedulerDone = done
}

// Queues used by the scheduler.
var (
runqueue task.Queue
Expand Down
15 changes: 15 additions & 0 deletions src/runtime/scheduler_any_reactor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build !scheduler.none

package runtime

// runReactor is the program entry point for a WebAssembly reactor program, instead of run().
// With a scheduler, init (but not main) functions are invoked in a goroutine before starting the scheduler.
func runReactor() {
initHeap()
go func() {
initAll()
// main is NOT called
schedulerDone = true
}()
scheduler()
}
11 changes: 11 additions & 0 deletions src/runtime/scheduler_none_reactor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build scheduler.none

package runtime

// runReactor is the program entry point for a WebAssembly reactor program, instead of run().
// With the "none" scheduler, init (but not main) functions are invoked directly.
func runReactor() {
initHeap()
initAll()
// main is NOT called
}
3 changes: 2 additions & 1 deletion targets/wasi.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
],
"ldflags": [
"--stack-first",
"--no-demangle"
"--no-demangle",
"--entry=_initialize"
],
"extra-files": [
"src/runtime/asm_tinygowasm.S"
Expand Down
26 changes: 26 additions & 0 deletions tests/wasm/reactor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package wasm

import (
"strings"
"testing"
)

func TestReactor(t *testing.T) {
tmpDir := t.TempDir()

err := run(t, "tinygo build -x -o "+tmpDir+"/reactor.wasm -target wasi testdata/reactor.go")
if err != nil {
t.Fatal(err)
}

out, err := runout(t, "wasmtime run --invoke tinygo_test "+tmpDir+"/reactor.wasm")
if err != nil {
t.Fatal(err)
}

got := string(out)
want := "1337\n"
if !strings.Contains(got, want) {
t.Errorf("reactor: expected %s, got %s", want, got)
}
}
12 changes: 9 additions & 3 deletions tests/wasm/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,24 @@ import (
)

func run(t *testing.T, cmdline string) error {
args := strings.Fields(cmdline)
_, err := runargs(t, args...)
return err
}

func runout(t *testing.T, cmdline string) ([]byte, error) {
args := strings.Fields(cmdline)
return runargs(t, args...)
}

func runargs(t *testing.T, args ...string) error {
func runargs(t *testing.T, args ...string) ([]byte, error) {
cmd := exec.Command(args[0], args[1:]...)
b, err := cmd.CombinedOutput()
t.Logf("Command: %s; err=%v; full output:\n%s", strings.Join(args, " "), err, b)
if err != nil {
return err
return b, err
}
return nil
return b, nil
}

func chromectx(t *testing.T) context.Context {
Expand Down
49 changes: 49 additions & 0 deletions tests/wasm/testdata/reactor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"time"
_ "unsafe"
)

//go:linkname scheduler runtime.scheduler
func scheduler()

//go:linkname setSchedulerDone runtime.setSchedulerDone
func setSchedulerDone(bool)

// __go_wasm_export_tinygo_test is a wrapper function around tinygo_test
// that runs the exported function in a goroutine and starts the scheduler.
// Goroutines started by this or other functions will persist, are paused
// when this function returns, and restarted when the host calls back into
// another exported function.
//
//export tinygo_test
func __go_wasm_export_tinygo_test() int32 {
setSchedulerDone(false)
var ret int32
go func() {
ret = tinygo_test()
setSchedulerDone(true)
}()
scheduler()
return ret
}

func tinygo_test() int32 {
for ticks != 1337 {
time.Sleep(time.Nanosecond)
}
return ticks
}

var ticks int32

func init() {
// Start infinite ticker
go func() {
for {
ticks++
time.Sleep(time.Nanosecond)
}
}()
}

0 comments on commit bd6a3f2

Please sign in to comment.