veandco / go-sdl2

SDL2 binding for Go
https://godoc.org/github.com/veandco/go-sdl2
BSD 3-Clause "New" or "Revised" License
2.17k stars 219 forks source link

`sdl.PushEvent(&sdl.UserEvent)` produces "Cgo argument has Go pointer to Go pointer" for non-nil `Data1` or `Data2` #534

Open iBug opened 1 year ago

iBug commented 1 year ago

This was originally asked on Stack Overflow, but I later discovered that Data1 and Data2 are completely unusable.

The code snippet is:

package main

import (
    "unsafe"

    "github.com/veandco/go-sdl2/sdl"
)

type MyType struct {
    A int
}

func main() {
    if err := sdl.Init(sdl.INIT_EVENTS); err != nil {
        panic(err)
    }

    ty := sdl.RegisterEvents(1)
    sdl.PushEvent(&sdl.UserEvent{
        Type:      ty,
        Timestamp: sdl.GetTicks(),
        Data1:     unsafe.Pointer(&MyType{}),
    })
}

In fact, Data1 can't be anything non-nil, so even this gives the "Go pointer to Go pointer" error:

var t int
sdl.PushEvent(&sdl.UserEvent{
    Type:      sdl.USEREVENT,
    Timestamp: sdl.GetTicks(),
    Data1:     unsafe.Pointer(&t),
})

Am I missing something? What is the correct way to pass extra data using Data1 and Data2 pointers?

veeableful commented 1 year ago

Hi @iBug, thanks for submitting the question! I believe due to Go's garbage collector default behavior of disallowing having Go pointer within the object another Go pointer is pointing to, it is not possible to pass data to SDL2 which is using C runtime using Go-allocated pointers.

However if you would like to experiment with passing Go-allocated pointer to SDL2, you can disable the check by disabling the check using the command export GODEBUG=cgocheck=0 before building. After that, try building and running the program again.

Another, more long-winded way which satisfies Go's default behavior is that you can allocate data using C and pass C-allocated pointer instead like the following example:

package main

/*
#include <stdlib.h>
typedef struct MyType {
    int A;
} MyType;

static inline MyType* NewMyType(int A)
{
    MyType* myType = (MyType*) malloc(sizeof(MyType));
    myType->A = A;
    return myType;
}
*/
import "C"
import (
    "fmt"
    "unsafe"

    "github.com/veandco/go-sdl2/sdl"
)

type MyType struct {
    A int
}

func main() {
    if err := sdl.Init(sdl.INIT_EVENTS); err != nil {
        panic(err)
    }

    ty := sdl.RegisterEvents(1)

    running := true
    for running {
        sdl.PushEvent(&sdl.UserEvent{
            Type:      ty,
            Timestamp: sdl.GetTicks(),
            Data1:     unsafe.Pointer(C.NewMyType(37)),
        })
        for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
            switch ev := event.(type) {
            case *sdl.QuitEvent:
                running = false
            case *sdl.UserEvent:
                if ev.Type == ty {
                    data1 := (*C.MyType)(unsafe.Pointer(ev.Data1))
                    fmt.Println("UserEvent with type and value:", ev.Type, data1.A)
            C.free(ev.Data1)
                }
            }
        }

        sdl.Delay(1000)
    }
}

Since I allocated using C, I also needed to free using C at some point. In this case, I chose to free immediately after processing it.

iBug commented 1 year ago

Thank you for your response. I'm not familiar with Cgo so I'd rather not touch that part with my application.

I ended up make(chan MyData, 16) and use PushEvent(UserEvent) as a notification for "data ready" so I don't have to create a separate select statement, which also enabled me to incorporate my custom events into the main sdl.PollEvent loop. This workaround was satisfactory for me as only an extra chan<- MyData needs to be carried around.

veeableful commented 1 year ago

Oh that's really smart! I'm glad that you found a better solution.