Run_dispose() in gjs

Hi. Please, look at the code bellow:

const { GObject } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const PanelMenu = imports.ui.panelMenu;
const Me = ExtensionUtils.getCurrentExtension();

var Preferences = GObject.registerClass({
    Signals: {
        'prop1Changed': {},
        'prop2Changed': {},
        'prop3Changed': {},
    },
}, class Preferences extends GObject.Object {
    constructor() {
        super();

        this._keyProp1 = 'prop1';
        this._keyProp2 = 'prop1';
        this._keyProp3 = 'prop1';

        this._settings = ExtensionUtils.getSettings();
        this._settings.connect(`changed`, (...[, key]) => {
            switch (key) {
                case this._keyProp1: {
                    this.emit(`prop1Changed`);
                    break;
                }
                case this._keyProp2: {
                    this.emit(`prop2Changed`);
                    break;
                }
                case this._keyProp3: {
                    this.emit(`prop3Changed`);
                    break;
                }
                default:
                    break;
            }
        });
    }

    destroy() {
        this._settings.run_dispose();
        this.run_dispose();
    }

    get prop1() {
        return this._settings.get_int(this._keyProp1);
    }

    get prop2() {
        return this._settings.get_int(this._keyProp2);
    }

    get prop3() {
        return this._settings.get_int(this._keyProp3);
    }
});

const PanelIndicator = GObject.registerClass(
class PanelIndicator extends PanelMenu.Button {
    constructor() {
        super(0);

        this._preferences = new Preferences();
        this._preferences.connect(`prop1Changed`, () => {
            log(`Prop1 changed ${this._preferences.prop1}`);
        });
        this._preferences.connect(`prop2Changed`, () => {
            log(`Prop2 changed ${this._preferences.prop2}`);
        });
        this._preferences.connect(`prop3Changed`, () => {
            log(`Prop3 changed ${this._preferences.prop3}`);
        });
    }

    destroy() {
        this._preferences.destroy();
        super.destroy();
    }
});

let panelIndicator;

function init() {
    ExtensionUtils.initTranslations(Me.uuid);
}

function enable() {
    panelIndicator = new PanelIndicator();
    Main.panel.addToStatusArea(`${Me.metadata.name}`, panelIndicator);
}

function disable() {
    panelIndicator.destroy();
    panelIndicator = null;
}

Is it okay to use run_dispose() in this way?

Short answer: don’t use GObject.Object.run_dispose() in GJS.

This function is not a way to avoid disconnecting signal handlers, null-ing out traced variables, or anything else like that. There are some very rare situations in the platform where this is called from GJS code to break reference cycles, but should never need to be called by user code.

This function does not free an object’s memory, it does not drop it’s reference count to 0, and is not a “destroy” function for GObjects in GJS.

When an GObject can no longer be traced to a variable in JavaScript, and nothing in a platform library is holding a reference, it will automatically be freed and all signal handlers connected to it will be destroyed. The garbage collector is fully capable of resolving circular references.

Thanks!. A few questions:

  • an instance of GObject.Object is only destroyed when its reference count drops to 0? There is no built-in way to forcefully destroy it (except unref())?
  • when the reference count drops to 0, will the instance of GObject.Object be destroyed immediately or just marked as available for destruction?
  • documenation says that Clutter.Actor.destroy() “destroys an actor”, so it looks like I can forcefully destroy Clutter.Actor, but I can’t do this for GObject.Object? Why so?

The meaning of “destroy” in Clutter.Actor.destroy() is that the instance is left in an inert state, but the object is still alive—i.e. its reference count is left untouched. This is achieved by calling GObject.Object.run_dispose() on it, which will run the dispose() virtual function chain, which should release external references, and cause the reference count to drop to zero in the optimal case. That won’t really work if something is holding a reference to the object through signal handlers, or fields inside other objects; it’s why the whole “destroy” concept was removed from GTK4: it’s not really possible to guarantee this behaviour when third party user code is involved.

The reason why you can’t do that for any random GObject type is that GObject does not have this functionality to begin with; run_dispose() was introduced to allow implementing GtkObject::destroy back in GTK2, as a backward compatibility API.

In general, now I’m even more confused. Is there any virtual method that is called before the object is destroyed? Clutter.Actor has a destroy signal which indicates that I should disconnect all handlers. Is there something similar for QObject.Object? Maybe it’s a vfunc_dispose()?

The GObject.Object.dispose() and GObject.Object.finalize() virtual functions were created to add destructor functionality for the C programming language, that works with GObject’s reference counting system. These are not necessary for a garbage collected language like JavaScript, which uses reference tracing.

If you are trying to do cleanup as part of a object like Clutter.Actor that has a destroy signal, you should just attach a callback:

const MySubclass = GObject.registerClass(
class MySubclass extends St.Widget {
    constructor(params = {}) {
        super(params);

        this.connect('destroy', (_actor) => {
            // do cleanup here
        });
    }
});

If you are trying to add some necessary cleanup to an object that has no destroy signal, you can simply create your own:

class MyClass {
    constructor() {
        this._datadirId = global.connect('notify::datadir', () => {});
    }

    destroy() {
        if (this._datadirId) {
            global.disconnect(this._datadirId);
            this._datadirId = null;
        }
    }
}

This mix of JavaScript and GLib drives me crazy. Let’s clear up some points.

Documentation says:

GJS is JavaScript bindings for GNOME, which means that behind the scenes there are two types of memory management happening: reference tracing (JavaScript) and reference counting (GObject).

The concept of reference counting is very simple. When a GObject is first created, it has a reference count of 1. When the reference count drops to 0, all the object’s resources and memory are automatically freed.

The concept of reference tracing is also quite simple, but can get confusing because it relies on external factors. When a value or object is no longer assigned to any variable, it will be garbage collected. In other words, if the JavaScript engine can not “trace” a value back to a variable it will free that value.

Put simply, as long as GJS can trace a GObject to a variable, it will ensure the reference count does not drop to 0.

As far as I understand, the GObject.Object’s instance (hereinafter referred to as the “object”)) is physically destroyed (i.e. memory freed) only by the GLib. In fact, I can destroy the object by calling unref(), but this is not recommended as it can cause a “use after free” problem (but in the case of the Clutter.Actor this is allowed for some reason). It turns out that JavaScript engine by tracing references increases/decreases the object’s internal reference count, and when it drops to 0, the object is destroyed by GLib?

Honestly, I tried to check how the objects (both GObject.Object and JavaScript’s object) are destroyed, but the following code didn’t lead to the destroing of both objects, even after 2 hours:

const registry = new FinalizationRegistry((heldValue) => {
    console.log(heldValue)
});
...
let obj1 = new Object();
let obj2 = new GObject.Object();
registry.register(obj1, 'obj1 has been destroyed');
registry.register(obj2, 'obj2 has been destroyed');
obj1 = null;
obj2 = null;

What is FinalizationRegistry?

And how is the script kept running?

Sure, you could frame it that way if you like. When the reference count drops to 0, each subclass of GObject.Object will, on way or the other, call GObject.Object.finalize() and chain-up to the parent class, until it hits the top-most class.

Never, ever call GObject.Object.unref() in GJS. Ever. That’s even worse that GObject.Object.run_dispose(). This is not allowed with Clutter.Actor in GJS either.

import GObject from 'gi://GObject';

// The object (in principle), has one reference. GJS ensures the refcount
// does not drop to `0`, because it is being traced to a variable
let objectInstance = new GObject.Object();

// The object is no longer being traced to a variable; it will be collected
// the next time the garbage collector is run.
objectInstance = null;

I put that code in my extension:

let registry;

function enable() {
    if (!registry) {
        registry = new FinalizationRegistry((heldValue) => {
            console.log(heldValue)
        });
    }
    let obj1 = new Object();
    let obj2 = new GObject.Object();
    registry.register(obj1, 'obj1 has been destroyed');
    registry.register(obj2, 'obj2 has been destroyed');
    obj1 = null;
    obj2 = null;
    //...
}

function disable() {
    //...
}

I would just then assume FinalizationRegistry does not work in GJS, possible it’s interfering with toggle references, which is another concept you don’t need to know about as a GJS programmer.

The short version of this is: you do not free memory explicitly in GJS. There really is nothing more to it, or more to understand, as GJS developer anyways.

That being said, but all means explode things for fun, if you want to. But none of this is really necessary to learn, and trying to do things like this in “production” code will give you no end of headaches.

FWIW, I went all the way down this rabbit-hole a few years ago. You may be interested in heapgraph, which might help visualize things a bit better. You’ll need a chunk of RAM to run this on GNOME Shell, though.

Yes, I got it. Thank you very much for your time.

Actually, I started diving into all this because I find it inconvenient to store the ID of a handler in case I need to disconnect it later. connectObject() and disconnectObject() partially solved the problem, but unfortunately they are only available on the Shell. It also seems inconvenient to me to have to disconnect all handlers when “destroying” the object (but now I understand why this is necessary).

Mmm, I feel like I’m not being totally clear :upside_down_face:

When an object is destroyed, all signal handlers connected to it are destroyed with it.

const emittingObject = new St.Label({text: 'Emitting Object'});
const otherObject = new St.Label({text: 'Other Object'});

emittingObject.connect('notify::text', (emitter) => {
    otherObject.text = emitter.text;
});

// BAD: if emittingObject's text property changes,
//      it will touch a destroyed object in its callback
otherObject.destroy();

// FINE: emittingObject is destroyed, it can not emit signals
emittingObject.destroy();

connectObject() more or less does this:

const emittingObject = new St.Label({text: 'Emitting Object'});
const otherObject = new St.Label({text: 'Other Object'});

let handlerId = emittingObject.connect('notify::text', (emitter) => {
    otherObject.text = emitter.text;
});

// Disconnect the handler, so it can not callback
// to otherObject if it is destroyed
otherObject.connect('destroy', () => {
    emittingObject.disconnect(handlerId);
});

As a tangent, I’m also a bit perplexed that FinalizationRegistry doesn’t do anything in this standalone script:

import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk?version=4.0';
import System from 'system';

const App = GObject.registerClass(class App extends Gtk.Application {
    constructor() {
        super({
            application_id: 'org.gnome.gjs.ExampleApplication',
        });
    }

    vfunc_startup() {
        super.vfunc_startup();
        console.log('finalization registry');
        this.registry = new FinalizationRegistry(heldValue => console.log(heldValue));
        const obj1 = {};
        const obj2 = new GObject.Object();
        this.registry.register(obj1, 'obj1 has been finalized');
        this.registry.register(obj2, 'obj2 has been finalized');
    }

    vfunc_activate() {
        super.vfunc_activate();

        // Example ApplicationWindow
        const win = new Gtk.ApplicationWindow({
            application: this,
            title: 'Example Application Window',
        });
        const button = new Gtk.Button({ label: 'gc' });
        button.connect('clicked', () => System.gc());
        win.child = button;
        win.connect('close-request', () => this.quit());
        win.present();
    }
});

const app = new App();
app.run([System.programInvocationName].concat(ARGV));

But keep in mind, according to the documentation of FinalizationRegistry on MDN:

  • A conforming JavaScript implementation, even one that does garbage collection, is not required to call cleanup callbacks. When and whether it does so is entirely down to the implementation of the JavaScript engine. When a registered object is reclaimed, any cleanup callbacks for it may be called then, or some time later, or not at all.
  • It’s likely that major implementations will call cleanup callbacks at some point during execution, but those calls may be substantially after the related object was reclaimed.

It might be worth investigating with heapgraph whether something is holding on to the objects, or they are getting collected and FinalizationRegistry simply isn’t calling the callback.

Not much luck on my end:

image

I’m pretty sure these are just the strings themselves, set with objectInstance.__heapgraph_name. Although it has been awhile since I’ve graphed a heap :slight_smile:

That’s exactly what I meant.

But only for the objects that have a destroysignal :frowning: And, again, I can’t use connectObject() and disconnectObject() in the prefs.js :frowning: :frowning:

The same for WeakRef :frowning: