gi-crystal icon indicating copy to clipboard operation
gi-crystal copied to clipboard

RFC: GC Collection of `MainWindow`

Open BigBoyBarney opened this issue 10 months ago • 4 comments

I'm closing https://github.com/hugopl/gtk4.cr/issues/74 in favour of this, as that one is quite cluttered with irrelevant stuff.

If the GC runs before something gets properly referenced in Crystal land, MainWindow will get collected.

Example: (Using GeopJR's Non::Blocking.spawn to make sure the fiber is spawned to a thread unoccupied by GLib.)

class MainWindow < Gtk::ApplicationWindow
  def initialize(application : Gtk::Application)
    super(application: application)

    label = Gtk::Label.new(label: "Will close in 2 seconds! Oh no!")
    self.child = label

    Non::Blocking.spawn do
      sleep 2.seconds
      GC.collect
    end
  end
end

The above will reliable close itself, after the GC collects MainWindow.


A very simple solution would be to create an array and add self to it, so that a reference exists in Crystal land.

KEEP_ALIVE = [] of GObject::Object

class MainWindow < Gtk::ApplicationWindow
  def initialize(application : Gtk::Application)
    super(application: application)
    KEEP_ALIVE << self # MainWindow now has something referencing it from Crystal

    label = Gtk::Label.new(label: "Will not close! Yay!")
    self.child = label

    spawn do
      sleep 2.seconds
      GC.collect
    end
  end
end

The above is not necessary if something else, like a signal on one of its children, keeps it alive already.


I don't see an immediate solution here that wouldn't have downsides, and the needed "fix" is very low-effort, so I was thinking about creating a PR to add this note to the github readme. Would that be alright @hugopl? Or do you have something else in mind?

BigBoyBarney avatar Feb 11 '25 21:02 BigBoyBarney

What's the complete code that causes that? Because usually you will have a GApplication instance with a reference to the main window in some variable, so it's never GC'ed.

hugopl avatar Feb 12 '25 11:02 hugopl

I mean, window.present should increase the reference count of the window object, then the toggle ref mechanism that disallow Crystal wrappers to be garbage collected if there are more than gobject 1 references to it will take care, at least that was the plan.

Unfortunately last months I don't have time to give more attention to your recent reports, I had twins the my schedule is "a bit" busy, hope you understand.

hugopl avatar Feb 12 '25 11:02 hugopl

This is the the entire code:

require "gtk4"
require "non-blocking-spawn"

class MainWindow < Gtk::ApplicationWindow
  def initialize(application : Gtk::Application)
    super(application: application)

    Non::Blocking.spawn do
      sleep 2.seconds
      GC.collect
    end
  end
end

class App < Gtk::Application
  def initialize
    super(application_id: "my.app")
  end

  @[GObject::Virtual]
  def activate
    window = active_window.nil? ? MainWindow.new(application: self) : active_window.not_nil!
    window.present
  end
end

app = App.new
app.run

Unfortunately last months I don't have time to give more attention to your recent reports, I had twins the my schedule is "a bit" busy, hope you understand.

Oh absolutely, do not worry about it all! I only got into more "serious" GTK development a few months ago, which is why some of my earlier issues are super low quality and cluttered. I don't necessarily expect the issues to be solved or even looked at, I'm just opening them so it's documented somewhere 😄

Thanks for the bindings in the first place, and good luck with the twins!!

BigBoyBarney avatar Feb 12 '25 11:02 BigBoyBarney

After looking at the simple hello world example, the issue seems to stem from subclassing.

The following closes when the GC is run.

class MainWindow < Gtk::ApplicationWindow
  def initialize(app : Gtk::Application)
    super(application: app)
  end
end

Non::Blocking.spawn do
  sleep 2.seconds
  GC.collect
end

app = Gtk::Application.new
app.activate_signal.connect do
  window = MainWindow.new(app)
  window.present
end
exit(app.run)

However, doing the following does not close, even though they're essentially the same 🤔

- window = MainWindow.new(app)
+ window = Gtk::ApplicationWindow.new(app)

BigBoyBarney avatar Feb 12 '25 12:02 BigBoyBarney