tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
14.84k stars 875 forks source link

Allow goroutines from exported wasm functions #3095

Open codefromthecrypt opened 1 year ago

codefromthecrypt commented 1 year ago

Currently, the following will work as expected:

package main

import "fmt"

func main() {
    msg := make(chan int)
    finished := make(chan int)
    go func() {
        <-msg
        fmt.Println("consumer")
        finished <- 1
    }()
    go func() {
        fmt.Println("producer")
        msg <- 1
    }()
    <-finished
}

However, doing the same from an exported function besides main panics at the line the goroutine is created.

package main

import "fmt"

func main() {}

//export notMain
func notMain() {
    msg := make(chan int)
    finished := make(chan int)
    go func() { // <- panic
        <-msg
        fmt.Println("consumer")
        finished <- 1
    }()
    go func() {
        fmt.Println("producer")
        msg <- 1
    }()
    <-finished
}

Since Wasm isn't parallel-safe, we rely on keeping main alive and calling the exported function at the same time. It would seem helpful to have a compiler flag or an annotation like //enable-scheduler

Note: jimmysl lee noticed that if you manipulate the %.wasm and insert a call opcode to call the _start function prior to actually invoking the desired export, it doesn't crash. This hint might help narrow down a path to a solution.

aykevl commented 1 year ago

If you want to call an exported function without calling _start first: that is not possible. _start must be called first to initialize various things (the runtime, global variables, etc).

Or do you mean something else?

It might be possible to allow calling exported functions after main has returned, if that is what you want.

codefromthecrypt commented 1 year ago

_start is already called first (at least in the test I was doing as wazero calls it by default, so before you can call any exports). Perhaps it is due to _start completing?

aykevl commented 1 year ago

I don't think this can work, at least not in a JS environment. The issue is that JavaScript can't block, and you are doing a blocking operation with <-finished.

Apart from that, running goroutines after main has returned isn't specified in the Go specification:

Program execution begins by initializing the main package and then invoking the function main. When that function invocation returns, the program exits. It does not wait for other (non-main) goroutines to complete.

In other words, goroutines that were running when main exits will be terminated. That doesn't necessarily imply that you can't start new goroutines, but it would be odd if that were possible.

codefromthecrypt commented 1 year ago

ok I should rephrase this as non-js. ex wasi and freestanding cc @hunjixin

The nature of entry points in wasm are different than go, and the impedance mismatch is a reality. what we do about it however is our choice. IOTW saying something about how main works in normal go, is historical context, but in the context of wasm (non js.. remember wasm core spec has no JS in it), it isn't necessarily painting a clear decision of what to do.

dgryski commented 1 year ago

Does this boil down to https://github.com/tinygo-org/tinygo/issues/2735 ?

codefromthecrypt commented 1 year ago

I'm not really sure the future of the reactor thing in WASI, even normal command is being completely redone. It feels uncomfortable to move it to blocked on something we don't implement, and might be dead by the time we do..

twharmon commented 6 months ago

I encountered a very similar problem with wasm ran in the browser. Rather than panic, the goroutine would just not execute. I was able to "fix" it by wrapping it in a js func:

var work = js.FuncOf(somethingThatUsesGoroutines)

//export notMain
func notMain() {
    work.Invoke()
}