OpenFL Devlog: Weak event listeners

Update (January 2024): Unfortunately, weak event listeners have been disabled for the C++ target, due to the way that Haxe internally implements passing methods by reference. Haxe creates a wrapper object around the method, so the wrapper object would be stored weakly, instead of the original method. Weak event listeners remain available for the JavaScript target.

About a year ago, I joined the OpenFL leadership team, in recognition of my contributions to the project. Since then, I’ve been working hard to improve things by fixing bugs and filling in missing features from the Flash API. If you’re not familiar, OpenFL is an implementation of the APIs available in Adobe Flash Player (and Adobe AIR) using the Haxe programming language. Projects built with Haxe can be cross-compiled to JavaScript, C++, and many other targets — making it available on the web, desktop apps, mobile apps, and more.

Over the last year, I’ve become more and more familiar with the OpenFL codebase — including Lime, OpenFL’s lower-level sibling that bridges the high-level Haxe code in OpenFL with low-level C++ code that is needed to integrate with the underlying operating system. It had been nearly 20 years since I last seriously used C++… and it’s been surprisingly enjoyable picking it up again. Sort of. I can do without the manual memory management (give me a garbage collector any day), but it’s exciting that I am no longer limited to working with what is already exposed at a high-level in OpenFL. If I’m feeling like getting my hands dirty, I can expose new native APIs, and hardware capabilities, that will empower other developers using OpenFL.

This is the first post of (I hope) many, where I give a technical summary of the recent features that I’ve implemented in OpenFL and Lime. Ideally, if I have time, I’d like to include some interesting low-level tidbits about each feature that you won’t find in the documentation, and would otherwise need to read OpenFL’s code to learn. In most cases, I plan to write about features that are actively in development, available only on Github at the time of writing, and not yet released to Haxelib. So be warned. The exact implementation details are subject to change, and if you want to try something out, you’ll either need to learn how to download nightly builds of OpenFL and Lime from Github Actions, or you’ll need to clone the repo and build it yourself. Without further ado, let’s get started on devlog #1.

Background on strong and weak event listeners

When you add an event listener by calling addEventListener(), like this…

dispatcher.addEventListener(type, listenerFunction);

…you create a reference from the dispatcher object to the listener function. Most references work this way: If object A is not yet eligible to be garbage collected, and it holds a reference to object B, then object B is also not yet eligible to be garbage collected. Later, if the reference from object A to object B is removed, then object B may be garbage collected (as long as there are no other references to it elsewhere). In terms of event listeners, removing the reference from the dispatcher to the listener function is done by calling removeEventListener(), like this:

dispatcher.removeEventListener(type, listenerFunction);

That type of reference is called a strong reference. There’s another type of reference, which is less common, called a weak reference. If object A is not yet eligible to be garbage collected, but it holds a weak reference to object B (and there are no other strong references to object B), then object B may be garbage collected.

An event listener may be added as a weak reference using the 5th parameter of addEventListener(), as seen in the code below:

dispatcher.addEventListener(type, listenerFunction, useCapture, priority, useWeakReference);

Weak listeners help reduce memory leak bugs by allowing objects to be garbage collected when you forget to call removeEventListener(), and the dispatcher shouldn’t be garbage collected yet. This is most common when adding event listeners to OpenFL’s stage (which is where all display objects get added if you want to render them). Stage listeners are added frequently for mouse and touch events, so ensuring that those listeners are removed is critical to writing correct OpenFL code. If the stage holds a strong reference to an object, that object probably isn’t getting garbage collected any time soon. That’s a memory leak, and if it happens enough times, it really starts to add up. When you add your listener as a weak reference, though, you can feel safe knowing that the object will eventually get garbage collected, even if you forgot the removeEventListener() call.

Implementation of weak listeners

Up until somewhat recently, it wasn’t possible to implement weak event listeners in OpenFL for most available targets, so OpenFL ignored the useWeakReference parameter, and added all references strongly. Event listeners in JS couldn’t be weak, and there simply wasn’t an API to weakly reference an arbitrary object in JS at all. For a long time, this was one place where OpenFL simply couldn’t match the capabilities of Flash.

However, between 2020 and 2021, web browser vendors started shipping the new WeakRef type for JavaScript. It provides a simple API for creating a weak reference to an object, instead of the normal strong reference. Here’s an example in JavaScript:

let ref = new WeakRef(targetObject);
let maybeTarget = ref.deref();
if (maybeTarget) {
    // object hasn't been garbage collected
} else {
    // object no longer exists
}

According to CanIUse, WeakRef has reached 92% availability. It’s ready for prime-time. However, when using WeakRef in OpenFL’s EventDispatcher implementation, I made sure to check if it’s available, and fall back to strong references, if needed.

Similarly, Haxe provides a cpp.vm.WeakRef for C++ desktop/mobile native targets that works very similarly. If you call its get() method, and the result is null, that means the object has been garbage collected.

var ref = new cpp.vm.WeakRef(targetObject);
var maybeTarget = ref.get();
if (maybeTarget != null) {
    // object hasn't been garbage collected
} else {
    // object no longer exists
}

In OpenFL’s EventDispatcher class, each call to addEventListener() results in the creation of a Listener object, which stores data about each listener that is added, such as whether the listener should be called for the capture phase, and what the listener’s priority is, which affects the order in which listeners are called. Basically, it’s a data structure that looks kind of like this:

private class Listener {
    public var callback:(Dynamic)->Void;
    public var priority:Int;
    public var useCapture:Bool;
}

In order to store a listener function as a weak reference, that callback member variable won’t work because setting it creates a strong reference. Weak listeners need to be stored differently.

private class Listener {
    public var callback:(Dynamic)->Void;
    public var priority:Int;
    public var useCapture:Bool;

    public var useWeakReference:Bool;
    #if (js && html5)
    public var weakRefCallback:WeakRef;
    #elseif cpp
    public var weakRefCallback:cpp.vm.WeakRef<(Dynamic)->Void>;
    #end
}

Similar to the useCapture field, I added a useWeakReference boolean field. Then, I added a separate weakRefCallback field to store the listener using the appropriate weak reference type for the target (JS vs C++).

Now, when dispatchEvent() is called, the dispatcher can loop through the Listener objects and quickly check whether a particular listener was added weakly or strongly. If the listener was added weakly, it can check whether the listener was garbage collected or not.

if (listener.useWeakReference) {
    var weakCallback = listener.weakRefCallback.deref();
    if (weakCallback != null) {
        weakCallback(event);
    }
} else {
    listener.callback(event);
}

Now, you might wonder, what happens to the Listener objects once the function held by a WeakRef has been garbage collected? Obviously, they will stick around in memory, which could slow down dispatchEvent() if you added a ton of weak listeners and never removed them. Avoiding memory leaks by trading for slower performance isn’t good, so EventDispatcher needs a way to clean things up from time to time so that it stays fast.

In the code above that calls the listener functions, I skipped some of the details. The Listener and WeakRef are actually cleaned up right away when EventDispatcher detects that the function has been garbage collected. The real code looks much closer to this:

var weakCallback = listener.weakRefCallback.get();
if (weakCallback != null) {
    iterator.remove(listener);
} else {
    weakCallback(event);
}

Maybe in the future, it might make sense to detect garbage collected weak listeners in other methods of EventDispatcher (not only in the dispatchEvent() method), but I feel like this is a good start that adds very little overhead.

Where can I find the code?

If you want to try this feature out, you’ll need to check out OpenFL’s 9.3.0-Dev branch on Github, or download the openfl-haxelib artifact from a successful Github Actions 9.30-Dev nightly build. Of course, this code is not yet ready for release to Haxelib, so use at your own risk in production. There may still be some bugs.

And that’s the end of OpenFL Devlog #1. Stay tuned for more posts about my work on OpenFL internals and some new features that I’ll be implementing in the near future.

About Josh Tynjala

Josh Tynjala is a frontend software developer, open source contributor, karaoke enthusiast, and he likes bowler hats. Josh develops Feathers UI, a user interface component library for creative apps, and he contributes to OpenFL. One of his side projects is Logic.ly, a digital logic circuit simulator for education. You should follow Josh on Mastodon.

Discussion

Leave a Reply

Your email address will not be published. Required fields are marked *