AndyObtiva / glimmer-dsl-libui

Glimmer DSL for LibUI - Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library - The Quickest Way From Zero To GUI - If You Liked Shoes, You'll Love Glimmer! - No need to pre-install any prerequisites. Just install the gem and have platform-independent GUI that just works on Mac, Windows, and Linux.
MIT License
458 stars 15 forks source link

Launching window twice causes segfault #48

Open nathan-b opened 1 year ago

nathan-b commented 1 year ago

Probably I'm just doing something stupid and wrong, but the documentation didn't tell me how to do what I was trying to do (write a function that displays a window whenever its called). I figured I'd give it a go anyway, and it works brilliantly the first time it's called, but the second time the world falls apart.

Minimal reproduction:

#!/usr/bin/ruby

require 'glimmer-dsl-libui'

include Glimmer

def show_win
  window('foo') {
    button('Boom') {
      on_clicked {
        return "boom"
      }
    }
  }.show
  return "done"
end

puts show_win
puts show_win

I assume return "boom" is the problem here; probably the UI loop needs to clean something up. I tried calling window.destroy before return, but that did not help.

nathan-b commented 1 year ago

Expected behavior: throw an exception, not segfault.

Extra credit: add an example in the documentation explaining how to do what I'm trying to do, but the correct way (or tell me personally and I'll update the docs).

AndyObtiva commented 1 year ago

So, the issue is the application is returning from the show_win method in the middle of a GUI application runtime, which interrupts it and breaks it. It is not expected GUI behavior (as per the MVC pattern), and should never really be needed. Any application that does that could be re-written in a way to work correctly without it (for example, by triggering a model method from the listener to do some work as per the proper GUI MVC pattern). If the code was to avoid returning from the method, the application would work just fine, like in this code, which simply puts in the on_clicked listener instead of outside the show_win method (though it could call a method on a model if there is a need to do something more sophisticated):

#!/usr/bin/ruby

require 'glimmer-dsl-libui'

include Glimmer

def show_win
  window('foo') {
    button('Boom') {
      on_clicked {
        puts "boom"
      }
    }
  }.show
end

show_win
show_win

Returning from a method is not a proper way for exiting a GUI application. The proper way is for the user to click the x button in the window, or for the application to destroy the window and then quit LibUI's event loop like in the following code:

#!/usr/bin/ruby

require 'glimmer-dsl-libui'

include Glimmer

def show_win
  window('foo') { |w|
    button('Boom') {
      on_clicked {
        puts "boom"
        w.destroy
        ::LibUI.quit
      }
    }
  }.show
end

show_win
show_win

That should accomplish what you want as every time you click the Boom button, it destroys the window and quits LibUI's event loop, thus allowing the next invocation of show_win to relaunch a new window from scratch.

This behavior is partially documented under: https://github.com/AndyObtiva/glimmer-dsl-libui#smart-defaults-and-conventions

I just added extra documentation about it under Common Control Operations: https://github.com/AndyObtiva/glimmer-dsl-libui/tree/master#common-control-operations

### Common Control Operations
- `destroy` (note that for closing a `window`, in addition to calling `somewindow.destroy`, you also have to call `::LibUI.quit`)
...

If you feel there is a need to provide further clarification (like for a use-case I am not aware of), or even an example as you suggested, please feel free to ask or just share a Pull Request!

Please confirm to me that the examples above resolve your issue in order for me to close this issue (or feel free to ask other questions if needed).

AndyObtiva commented 1 year ago

Interesting!

Apparently, if you really insist on the method return approach, this code makes it work (on the Mac at least, where I ran it):

#!/usr/bin/ruby

require 'glimmer-dsl-libui'

include Glimmer

def show_win
  window('foo') { |w|
    button('Boom') {
      on_clicked {
        w.destroy
        ::LibUI.quit
        return "boom"
      }
    }
  }.show
  return "done"
end

puts show_win
puts show_win

I don't recommend it, but if you really want to return from the method (instead of calling a model method as per MVC), the code above works.

I am of the belief that there are always exceptions to the rule, and it is good to bend rules to be pragmatic every once in a while (staying mindful of the pros/cons vs other approaches). So, perhaps in a quick and dirty Ruby scripting situation, this method return approach might be helpful. But, I'd still think twice before applying it.

AndyObtiva commented 1 year ago

One more thing. When running the original application, I don't get a seg fault on the Mac. I get an unexpected return (LocalJumpError) error:

tmp/test_open_window_multiple_times0.rb:13:in `block (3 levels) in show_win': unexpected return (LocalJumpError)
    from /Users/andymaleh/code/glimmer-dsl-libui/lib/glimmer/libui/control_proxy.rb:171:in `block in handle_listener'
    from /Users/andymaleh/code/glimmer-dsl-libui/lib/glimmer/libui/control_proxy.rb:175:in `block (2 levels) in handle_listener'
    from /Users/andymaleh/code/glimmer-dsl-libui/lib/glimmer/libui/control_proxy.rb:175:in `map'
    from /Users/andymaleh/code/glimmer-dsl-libui/lib/glimmer/libui/control_proxy.rb:175:in `block in handle_listener'
    from /Users/andymaleh/.rvm/rubies/ruby-3.1.0/lib/ruby/3.1.0/fiddle/closure.rb:45:in `call'
    from /Users/andymaleh/.rvm/gems/ruby-3.1.0@glimmer-dsl-libui/gems/libui-0.1.2.pre-arm64-darwin/lib/libui/ffi.rb:20:in `call'
    from /Users/andymaleh/.rvm/gems/ruby-3.1.0@glimmer-dsl-libui/gems/libui-0.1.2.pre-arm64-darwin/lib/libui/ffi.rb:20:in `uiMain'
    from /Users/andymaleh/.rvm/gems/ruby-3.1.0@glimmer-dsl-libui/gems/libui-0.1.2.pre-arm64-darwin/lib/libui/libui_base.rb:46:in `public_send'
    from /Users/andymaleh/.rvm/gems/ruby-3.1.0@glimmer-dsl-libui/gems/libui-0.1.2.pre-arm64-darwin/lib/libui/libui_base.rb:46:in `block (2 levels) in <module:LibUIBase>'
    from /Users/andymaleh/code/glimmer-dsl-libui/lib/glimmer/libui/control_proxy/window_proxy.rb:69:in `show'
    from tmp/test_open_window_multiple_times0.rb:16:in `show_win'
    from tmp/test_open_window_multiple_times0.rb:21:in `<main>'

That is totally correct as it is mentioning that returning is unexpected behavior.

If you are still encountering this somehow, it might be an issue on a specific environment. I know that Linux does not support launching a window multiple times within the same Ruby session unfortunately. It is a known problem observed when using girb (Glimmer IRB) on Linux, which requires exiting and restarting on every window launch (reported here: https://github.com/AndyObtiva/glimmer-dsl-libui/issues/45).

Please confirm what environment you are on (Linux, Windows, or Mac) if the code snippets above do not resolve your issue.

AndyObtiva commented 1 year ago

OK, I just tested your application in Linux and confirmed that in Linux, unlike on the Mac, I don't get an unexpected return (LocalJumpError) exception, yet a segfault. Additionally, as mentioned in my last comment, the Linux implementation of the underlying C libui library does not currently support launching a window multiple times in the same Ruby session.

I am reporting this issue to the upstream projects (libui C library and bindings). I will report back here once it is fixed.

nathan-b commented 1 year ago

Thanks so much for the information and for reporting the issue upstream!

The problem I'm (clumsily) trying to solve is that I need a dialog box asking for input from the user. I need this at several points in the application, so I wrote a function that creates said dialog box. Once the user presses "OK" or "Cancel" I want the function to return the user input (or nil if the user pressed Cancel). Hopefully this explains why I'm trying to do such a seemingly silly thing :)

I actually tried something similar to your approach (calling destroy on the window and quit on LibUI), but I guess at that point I ran afoul of the upstream bug you mentioned. I'll watch this issue and try again once it gets fixed and merged!

AndyObtiva commented 1 year ago

You're welcome.

I reported your issue at the underlying C libui project: https://github.com/libui-ng/libui-ng/issues/205

And, the libui binding project: https://github.com/kojix2/LibUI/issues/45

In the meantime, here is a workaround.

You can write multiple Ruby applications that call each other conditionally based on your specific needs' logic. That way, instead of attempting to relaunch a window in the same Ruby application, you simply call another Ruby application to launch a window in a separate Ruby session (using system, ticks, or IO.popen).

I actually used this approach (using IO.popen) in building the Glimmer Meta-Example, which enables launching many Glimmer DSL for LibUI windows at the same time side by side from the same Ruby application session:

meta-example

You can go ahead and verify that for yourself by launching it via these instructions (ruby -r './lib/glimmer-dsl-libui' examples/meta_example.rb if you have the glimmer-dsl-libui project cloned locally): https://github.com/AndyObtiva/glimmer-dsl-libui#examples

I hope this will address your needs until the reported issue is fixed.

Cheers!