Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime, builder: WebAssembly reactor mode #4082

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}()
}
Loading