I’m the author of giD which is a GObject Introspection binding generator for the D language. I’m currently trying to resolve some memory leak issues with the Gtk4 bindings. A similar test application written with the Gtk3 bindings does not exhibit the same leaks, because of the difference in how it handles the destruction of Window objects.
It appears that Gtk3 performs a recursive gtk_widget_destroy when a Window is closed. On the other hand it appears that Gtk4 may rely on the removal of the global window reference to ripple down through it’s children as the objects are individually unref’d and destroyed. Though I’m not positive on this.
I created a test application with an ApplicationWindow, Box, and a Button. This diagram illustrates the object relationships and references.
The particular problem I’m having is with the Button signal callback. The D language has a garbage collector, which can resolve reference loops. However, these loops are only collected if they aren’t referenced from memory roots (global variables, etc). In the test example there is a signal callback on a child Button widget that references the grandparent ApplicationWindow in its callback delegate context.
This is what happens when the window is closed:
The global Gtk window reference is removed
ApplicationWindow reference count reaches 1 and the giD GObject toggle reference callback is called indicating it is the last reference
The toggle reference callback removes the D wrapper object from a global reference array, which is used to prevent the object from being garbage collected
The ApplicationWindow D wrapper object destructor is never called though, because the Button signal callback context still references the ApplicationWindow in its delegate context and the Button itself is still referenced from the global root D object reference array, thus preventing the garbage collector from collecting it (this chain is illustrated by the red arrows)
The Button isn’t removed because it is still referenced by a chain of GObject C references from the ApplicationWindow
Does anyone know how language bindings typically handle these types of situations? One possible solution I was thinking of, was to have a callback on the ApplicationWindow perform a recursive removal of all signal handlers. I’m not sure if this could be a solution that would work for all container like widgets though.
The other thought I had was to try to make the delegate references be “weak”, by marking them as NO_SCAN with the garbage collector, which would then not consider any referenced object pointers. This could cause other issues though, especially for lambda callbacks which can have any number of pointers possibly not even related to GObjects, potentially resulting in memory being freed when it is still being used.
For reference I also created a Gtk3 test application in the same repo as the Gtk4 one which is virtually identical other than API differences between Gtk3 vs Gtk4.
The scenario that occurs in this case when the window is closed:
A delete event is created in response to the close
g_object_run_despose is executed which ultimately recurses through container children with gtk_widget_destroy
The result is that all GObject C references are removed
This results in all toggle reference callbacks being called with last references, which removes the D global wrapper object references in D memory
Even if the Button delegate still remains, no objects are connected to any GC memory roots
ApplicationWindow and all child widgets are collected by the garbage collector
It seems like calling g_object_run_dispose on the ApplicationWindow in a “close-request” signal handler accomplishes something similar to what Gtk3 does. Other container widgets don’t really have an equivalent to this signal though, so I’m not sure where to hook those widgets to execute g_object_run_dispose or if that is even a recommended way to do this.
It looks like I’m affected, too with C# and GirCore and this code:
using System;
using System.Threading.Tasks;
using Gtk;
var application = Gtk.Application.New("org.gir.core", Gio.ApplicationFlags.FlagsNone);
application.OnActivate += (sender, args) =>
{
var window = Gtk.ApplicationWindow.New((Gtk.Application) sender);
window.Title = "win1";
var b1 = Button.NewWithLabel("click");
b1.OnClicked += (_, _) =>
{
Console.WriteLine("Clicked");
};
window.Child = b1;
var w = Gtk.ApplicationWindow.New((Gtk.Application) sender);
w.Title = "leak";
var b = Button.NewWithLabel("click");
b.OnClicked += (_, _) =>
{
w.Close(); //If this line is present ApplicationWindow will not be collected from the GC if the window was closed
Console.WriteLine("Clicked");
};
w.Child = b;
window.Present();
w.Present();
Task.Run(async () =>
{
while (true)
{
await Task.Delay(5000);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
});
};
return application.RunWithSynchronizationContext(null);
This is a problem that exists in any language with closures. (in this context, I’m using “closures” to mean inline functions that use variables from the surrounding scope)
For example, here are two blog posts that describe the phenomenon in JS (just regular web-based JS, no gobject-introspection)
Different languages deal with this in different ways. C++'s lambdas have capture lists in order to avoid this problem, but it is still possible to use-after-free with lambda captures if you try hard enough. JavaScript and most other interpreted languages with closures do not have explicit capturing syntax and cannot let the programmer intentionally use-after-free, so the burden is on the language runtime to analyze the code so that they capture as little as possible while never releasing an object that could still be used by a live closure.
Thank you for more insight into this being a general issue with closures in many languages. I saw some posts about Python as well where these types of issues occur. After much effort I moved on from trying to solve this issue in an automated way at this time and instead figured out different workarounds for some of the problems I encountered. This includes solutions like: calling run_dispose on Widgets to ensure all children are disconnected, clearing closure callbacks, and null-ing out class members which reference other objects. These solutions are discribed here for anyone interested.
I can think of a way to solve this in giD for closure delegates which only reference a single object (method delegates), but not for lambda delegates or embedded functions which can have a variable length context without the ability to introspect it at compile time with templates (or runtime).
In dotnet there is an interface called IDisposable. It has a Dispose method which can be called by a user to free all native and managed ressources.
This interface is implemented for all GObjects in GirCore. So if a user calls w.Dispose() after the w.Close() call the global instance cache will remove the corresponding instance, thus the ref count of the ApplicationWindow will drop to 0 which breaks the cycle and the garbage collector can collect the Button and delegate and so on.