grow-graphics / gd

Go + Godot 4.3
https://learn.grow.graphics/
MIT License
206 stars 10 forks source link

Signals from goroutine? #36

Closed TeddyDD closed 2 months ago

TeddyDD commented 2 months ago

To avoid xy problem: I was thinking about leveraging gd for writing code where Go shines and Godot is bad at (networking, HTTP requests, IPC, etc.). My idea was to do a bunch of work on the Go side and communicate the results asynchronously to the Godot via signals.

I modified the example to call signal from goroutine:

type HelloWorld struct {
    gd.Class[HelloWorld, gd.SceneTree]
    something gd.SignalAs[func()]
}

func (h *HelloWorld) DoSomething() {
    go func() {
        time.Sleep(time.Second * 5)
        h.something.Emit()
    }()
}
func _ready():
    var a = HelloWorld.new()
    a.connect("something", handle_sig)
    a.DoSomething()

func handle_sig():
    print("yay")

It results in crash though:

Hello World from Go!
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x7f06901f488f]

goroutine 52 [running]:
main.(*HelloWorld).DoSomething.func1()
        /tmp/quic-tmp.UWJNs/main.go:26 +0x2f
created by main.(*HelloWorld).DoSomething in goroutine 17
        /tmp/quic-tmp.UWJNs/main.go:24 +0x4f

I assume the bindings are not thread safe? Is it possible to use signals like this? Or perhaps should I use other method (pooling from the Godot side?)

Splizard commented 2 months ago

Thanks for reporting this use case, as you've mentioned, it makes perfect sense to use Go in this way.

There are some issues here, the Something field needs to be exported, you can tag it as gd:"something". This should fix the panic. There's still the possibility of race conditions here. On the Godot side, Emit isn't necessarily thread 'safe'. So to do this safely, the signal needs to be emitted through a deferred call that runs on the main thread.

What I'm going to do, is add a representation for this using channels. If you add a write only channel field to your class. This will be registered as a signal available in Godot. Then you can send to this from a goroutine.

I would avoid calling any gd functions in a goroutine. As you really need to know what you are doing to make it thread safe. Use the soon-to-be-implemented chan signals to communicate asynchronously so that gd can take care of emitting the signal safely.

I'll close this issue once I've implemented the following, which I will be working on today.

type HelloWorld struct {
    gd.Class[HelloWorld, gd.SceneTree]
    Something chan<- struct{} `gd:"something"`
}

func (h *HelloWorld) DoSomething() {
    go func() {
        time.Sleep(time.Second * 5)
        h.Something <- struct{}{} 
    }()
}