Search code examples
javascriptgnome-shellgnome-shell-extensionsgjs

Avoid allocation errors with destroy() and async functions


The following is a simple scenario for some GNOME extension:

  1. Enable the extension. The extension is a class which extends Clutter.Actor.
  2. It creates an actor called myActor and adds it: this.add_child(myActor).
  3. Then it calls an asynchronous time-consuming function this._tcFunction() which in the end does something with myActor.

Here's where I run into a problem:

  1. We disable (run this.destroy()) the extension immediately after enabling it.

  2. On disabling, this.destroy() runs GObject's this.run_dispose() to collect garbage. However, if this._tcFunction() has not finished running, it'll later try to access myActor which might have been already deallocated by this.run_dispose().

One way to go about this, is to define a boolean variable in this.destroy()

destroy() {
    this._destroying = true
    // ...
    this.run_dispose;
}

and then add a check in this._tcFunction(), e.g.

async _tcFunction() {
    await this._timeConsumingStuff();

    if (this._destroying === true) { return; }

    myActor.show();

}

My question: is there a nicer way to deal with these situations? Maybe with Gio.Cancellable()? AFAIK, there's no easy way to stop an async function in javascript...


Solution

  • Firstly, two things to be aware of:

    1. Avoid calling low-level memory management functions like GObject.run_dispose() as there are a cases in the C libraries where these objects are being cached for reuse and aren't actually being disposed when you think they are. There is also no dispose signal and other objects may need notification.

    2. Avoid overriding functions that trigger disposal like Clutter.Actor.destroy() and instead use the destroy signal. GObject signal callback always get the emitting object as the first argument and it is safe to use that in a destroy callback.

    There are a couple ways I could think of solving this, but it depends on the situation. If the async function is a GNOME library async function, it probably does have a cancellable argument:

    let cancellable = new Gio.Cancellable();
    
    let actor = new Clutter.Actor();
    actor.connect('destroy', () => cancellable.cancel());
    
    Gio.File.new_for_path('foo.txt').load_contents_async(cancellable, (file, res) => {
        try {
            let result = file.load_contents_finish(res);
    
            // This shouldn't be necessary if the operation succeeds (I think)
            if (!cancellable.is_cancelled())
                log(actor.width);
        } catch (e) {
            // We know it's not safe
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                log('it was cancelled');
    
            // Probably safe, but let's check
            else if (!cancellable.is_cancelled())
                log(actor.width);
        }
    });
    
    // The above function will begin but not finish before the
    // cancellable is triggered
    actor.destroy();
    

    Of course, you can always use a cancellable with a Promise, or just a regular function/callback pattern:

    new Promise((resolve, reject) => {
       // Some operation
       resolve();
    }).then(result => {
        // Check the cancellable
        if (!cancellable.is_cancelled())
            log(actor.width);
    });
    

    Another option is to null out your reference, since you can safely check for that:

    let actor = new Clutter.Actor();
    actor.connect('destroy', () => {
        actor = null;
    });
    
    if (actor !== null)
        log(actor.width);