golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.28k stars 17.47k forks source link

x/exp/shiny: driver.Main not returning (and also can be made to panic unexpectedly) #21796

Open williamhanisch opened 6 years ago

williamhanisch commented 6 years ago

Please answer these questions before submitting your issue. Thanks! See below.

What version of Go are you using (go version)?

go version go1.9 darwin/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

GOARCH="amd64" GOBIN="" GOEXE="" GOHOSTARCH="amd64" GOHOSTOS="darwin" GOOS="darwin" GOPATH="/Users/william" GORACE="" GOROOT="/usr/local/go" GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64" GCCGO="gccgo" CC="clang" GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/60/zldw5qqn401b20vjmzxx1_140000gn/T/go-build661525149=/tmp/go-build -gno-record-gcc-switches -fno-common" CXX="clang++" CGO_ENABLED="1" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config"

What did you do?

Below are simplified fully runnable programs that illustrate the issue. I'm trying to add cleanup code after the function passed to driver.Main returns. The documentation for driver.Main sates that "It returns when f returns." However, when returning from the passed-in function (closure), the program exits. In lieu of cleanup code, I'm using print statements in the example code below. Although I could achieve what I need using a defer call in the passed-in function (closure), there still seems to be a bug (or at any rate, a documentation bug) worth reporting.

For additional information, see this thread on go-nuts between Dave MacFarlane (who encouraged me to file this report) and me: https://groups.google.com/forum/#!topic/golang-nuts/DmEV4K3xIZY

If possible, provide a recipe for reproducing the error. A complete runnable program is good. A link on play.golang.org is best. Here is the first illustration program (since this is a shiny based program, using the go playground won't work, I don't think):

package main

import ( "fmt"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"

)

func main() { fmt.Println("Starting.") driver.Main(func(s screen.Screen) { fmt.Println("I'm printed; you can see me.") return }) fmt.Println("I'm not printed; alas, I can't be seen.") }

What did you expect to see?

All three print statements being called.

What did you see instead?

Only the first two were called.

After adding code to create a window and running it, it panics. That is, this code (note there's also one more print statement than the code above):

package main

import ( "fmt" "log"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"

)

func main() { fmt.Println("Starting.") driver.Main(func(s screen.Screen) { fmt.Println("I'm printed; you can see me.") w, err := s.NewWindow(&screen.NewWindowOptions{Title: "I Don't Return"}) if err != nil { log.Fatal(err) } defer w.Release() fmt.Println("You can see me too.") return }) fmt.Println("I'm not printed; alas, I can't be seen.") }

produces this:

william@hardy% go run main.go Starting. I'm printed; you can see me. You can see me too. panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x40bab37]

goroutine 1 [running, locked to thread]: golang.org/x/exp/shiny/driver/gldriver.preparedOpenGL(0x4555610, 0x463cd10, 0x1) /Users/william/src/golang.org/x/exp/shiny/driver/gldriver/cocoa.go:91 +0xb7 golang.org/x/exp/shiny/driver/gldriver._cgoexpwrap_78313b9f6607_preparedOpenGL(0x4555610, 0x463cd10, 0x1) golang.org/x/exp/shiny/driver/gldriver/_obj/_cgo_gotypes.go:345 +0x3f golang.org/x/exp/shiny/driver/gldriver._Cfunc_startDriver() golang.org/x/exp/shiny/driver/gldriver/_obj/_cgo_gotypes.go:305 +0x41 golang.org/x/exp/shiny/driver/gldriver.main(0x4108530, 0x0, 0xc420053f10) /Users/william/src/golang.org/x/exp/shiny/driver/gldriver/cocoa.go:107 +0x51 golang.org/x/exp/shiny/driver/gldriver.Main(0x4108530) /Users/william/src/golang.org/x/exp/shiny/driver/gldriver/gldriver.go:26 +0x2f golang.org/x/exp/shiny/driver.main(0x4108530) /Users/william/src/golang.org/x/exp/shiny/driver/driver_darwin.go:15 +0x2b golang.org/x/exp/shiny/driver.Main(0x4108530) /Users/william/src/golang.org/x/exp/shiny/driver/driver.go:24 +0x2b main.main() /Users/william/src/play/shiny/main.go:15 +0x7b exit status 2 william@hardy%

I don't see any documentation to think that this should be expected. It may have something to do with lifecycle events not being called. I also had a very similar panic when calling Send on the window's Event.Deque from another goroutine (one that was started from within the closure passed to driver.Main). I figured that that goroutine was calling Send too soon, i.e., before certain lifecycle events were placed on the EventDeque, but I'm only guessing.

The panic doesn't occur if I add code to process the events from the window's EventDeque, but the original issue of driver.Main not returning still persists. That is, this code:

package main

import ( "fmt" "image" "log"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"

)

func main() { fmt.Println("Starting.") driver.Main(func(s screen.Screen) { fmt.Println("I'm printed; you can see me.") w, err := s.NewWindow(&screen.NewWindowOptions{Title: "I Don't Return"}) if err != nil { log.Fatal(err) } defer w.Release()

    var b screen.Buffer
    defer func() {
        if b != nil {
            b.Release()
        }
    }()

    fmt.Println("You can see me too.")
    for {
        switch e := w.NextEvent().(type) {
        case lifecycle.Event:
            if e.To == lifecycle.StageDead {
                return
            }
        case key.Event:
            if e.Direction == key.DirPress && e.Modifiers == key.ModMeta {
                switch e.Code {
                case key.CodeQ:
                    return
                }
            }
        case paint.Event:
            w.Upload(image.Point{}, b, b.Bounds())
            w.Publish()
        case size.Event:
            if b != nil {
                b.Release()
            }
            b, err = s.NewBuffer(e.Size())
            if err != nil {
                log.Fatal(err)
            }
        case error:
            log.Print(e)
        }
    }
})
fmt.Println("I'm not printed; alas, I can't be seen.")

}

produces this:

william@hardy% go run main.go Starting. I'm printed; you can see me. You can see me too.

and I just get my prompt back after either choosing the Quit menu option on the top menu bar or hitting Command-Q. That is, the last print statement never gets called.

I understand that shiny is experimental and also that development has somewhat paused recently. However, I still think it's worthwhile submitting this report.

Please let me know if I can be of any help.

Thanks, William

ianlancetaylor commented 6 years ago

CC @nigeltao

williamhanisch commented 6 years ago

This may be a clue. I added a defer statement at the start of the closure passed to driver.Main (let's call this closure f) like so:

package main

import ( "fmt" "image" "log"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"

)

func main() { fmt.Println("Starting.") driver.Main(func(s screen.Screen) { defer func() { fmt.Println("I'm seen!") }()

    fmt.Println("I'm printed; you can see me.")
    w, err := s.NewWindow(&screen.NewWindowOptions{Title: "I Don't Return"})
    if err != nil {
        log.Fatal(err)
    }
    defer w.Release()

    var b screen.Buffer
    defer func() {
        if b != nil {
            b.Release()
        }
    }()

    fmt.Println("You can see me too.")

    for {
        switch e := w.NextEvent().(type) {
        case lifecycle.Event:
            if e.To == lifecycle.StageDead {
                return
            }
        case key.Event:
            if e.Direction == key.DirPress && e.Modifiers == key.ModMeta {
                switch e.Code {
                case key.CodeQ:
                    return
                }
            }
        case paint.Event:
            w.Upload(image.Point{}, b, b.Bounds())
            w.Publish()
        case size.Event:
            if b != nil {
                b.Release()
            }
            b, err = s.NewBuffer(e.Size())
            if err != nil {
                log.Fatal(err)
            }
        case error:
            log.Print(e)
        }
    }
})
fmt.Println("I'm not printed; alas, I can't be seen.")

}

However, "I'm seen!" is not printed after quitting. A bug? the same one as above? a different one? If the the latter, related?

Yet, if I move those three lines from the beginning of f to after the window is created like so:

package main

import ( "fmt" "image" "log"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"

)

func main() { fmt.Println("Starting.") driver.Main(func(s screen.Screen) { fmt.Println("I'm printed; you can see me.") w, err := s.NewWindow(&screen.NewWindowOptions{Title: "I Don't Return"}) if err != nil { log.Fatal(err) } defer w.Release()

    defer func() {
        fmt.Println("I'm seen!")
    }()

    var b screen.Buffer
    defer func() {
        if b != nil {
            b.Release()
        }
    }()

    fmt.Println("You can see me too.")

    for {
        switch e := w.NextEvent().(type) {
        case lifecycle.Event:
            if e.To == lifecycle.StageDead {
                return
            }
        case key.Event:
            if e.Direction == key.DirPress && e.Modifiers == key.ModMeta {
                switch e.Code {
                case key.CodeQ:
                    return
                }
            }
        case paint.Event:
            w.Upload(image.Point{}, b, b.Bounds())
            w.Publish()
        case size.Event:
            if b != nil {
                b.Release()
            }
            b, err = s.NewBuffer(e.Size())
            if err != nil {
                log.Fatal(err)
            }
        case error:
            log.Print(e)
        }
    }
})
fmt.Println("I'm not printed; alas, I can't be seen.")

}

Then the deferred closure is called. That is, "I'm seen!" is printed.

It's as if adding a defer in f before screen.NewWindow is called somehow gets deactivated, yet calling defer after works.

I'm guessing this behavior is caused by the same bug as the one above, or at any rate, one closely related to it.

nigeltao commented 6 years ago

Yes, sounds like a bug, but as you already know, I don't really have much spare time to look at it right now.

driusan commented 6 years ago

For what it's worth, the bug seems to be specific to the MacOS cocoa driver. I just tried and the first minimal example printed the final statement under the x11driver in both DragonFly and Linux, and also under the gldriver with Linux.

alexbrainman commented 6 years ago

This works on Windows too:

C:\Users\Alex\dev\src\a>type main.go
package main

import (
"fmt"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
)

func main() {
fmt.Println("Starting.")
driver.Main(func(s screen.Screen) {
fmt.Println("I'm printed; you can see me.")
return
})
fmt.Println("I'm not printed; alas, I can't be seen.")
}
C:\Users\Alex\dev\src\a>go run main.go
Starting.
I'm printed; you can see me.
I'm not printed; alas, I can't be seen.

C:\Users\Alex\dev\src\a>

If someone knows how to fix this, I am sure there are enough interested people to review the fix and submit the change.

Alex

williamhanisch commented 6 years ago

I'm going to study the gldriver and cocoa code and see if I can find anything. By the way, Dave and Alex, does the defer code (see my second post on this thread above) work properly on those other systems as well?

alexbrainman commented 6 years ago

does the defer code (see my second post on this thread above) work properly on those other systems as well?

I am not certain I did what you asked, but:

C:\Users\Alex\dev\src\a>type main.go
package main

import (
"fmt"
"image"
"log"

"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"
)

func main() {
fmt.Println("Starting.")
driver.Main(func(s screen.Screen) {
defer func() {
fmt.Println("I'm seen!")
}()

        fmt.Println("I'm printed; you can see me.")
        w, err := s.NewWindow(&screen.NewWindowOptions{Title: "I Don't Return"})
        if err != nil {
                log.Fatal(err)
        }
        defer w.Release()

        var b screen.Buffer
        defer func() {
                if b != nil {
                        b.Release()
                }
        }()

        fmt.Println("You can see me too.")

        for {
                switch e := w.NextEvent().(type) {
                case lifecycle.Event:
                        if e.To == lifecycle.StageDead {
                                return
                        }
                case key.Event:
                        if e.Direction == key.DirPress && e.Modifiers == key.ModMeta {
                                switch e.Code {
                                case key.CodeQ:
                                        return
                                }
                        }
                case paint.Event:
                        w.Upload(image.Point{}, b, b.Bounds())
                        w.Publish()
                case size.Event:
                        if b != nil {
                                b.Release()
                        }
                        b, err = s.NewBuffer(e.Size())
                        if err != nil {
                                log.Fatal(err)
                        }
                case error:
                        log.Print(e)
                }
        }
})
fmt.Println("I'm not printed; alas, I can't be seen.")
}
C:\Users\Alex\dev\src\a>go run main.go
Starting.
I'm printed; you can see me.
You can see me too.
I'm seen!
I'm not printed; alas, I can't be seen.

C:\Users\Alex\dev\src\a>

As I run main.go, the window opens and I close it and then program exists.

Alex

williamhanisch commented 6 years ago

Thanks Alex! It looks like it works properly on Windows. I assume the last two lines:

I'm seen! I'm not printed; alas, I can't be seen.

were printed after you close the window.

I just thought of something else. If it's not too much trouble, could you try on Windows the version which panics on macOS? I'll copy the code again here for easy access:

package main

import ( "fmt" "log"

"golang.org/x/exp/shiny/driver" "golang.org/x/exp/shiny/screen" )

func main() { fmt.Println("Starting.") driver.Main(func(s screen.Screen) { fmt.Println("I'm printed; you can see me.") w, err := s.NewWindow(&screen.NewWindowOptions{Title: "I Don't Return"}) if err != nil { log.Fatal(err) } defer w.Release() fmt.Println("You can see me too.") return }) fmt.Println("I'm not printed; alas, I can't be seen.") }

Thanks! William

alexbrainman commented 6 years ago

I assume the last two lines:

I'm seen! I'm not printed; alas, I can't be seen.

were printed after you close the window.

They were.

Alex