How to make a C++ class cycle collected
Should my class be cycle collected?
First, you need to decide if your class should be cycle collected. There are three main criteria:
It can be part of a cycle of strong references, including refcounted objects and JS. Usually, this happens when it can hold alive and be held alive by cycle collected objects or JS.
It must be refcounted.
It must be single threaded. The cycle collector can only work with objects that are used on a single thread. The main thread and DOM worker and worklet threads each have their own cycle collectors.
If your class meets the first criteria but not the second, then whatever class uniquely owns it should be cycle collected, assuming that is refcounted, and this class should be traversed and unlinked as part of that.
The cycle collector supports both nsISupports and non-nsISupports (known as “native” in CC nomenclature) refcounting. However, we do not support native cycle collection in the presence of inheritance, if two classes related by inheritance need different CC implementations. (This is because we use QueryInterface to find the right CC implementation for an object.)
Once you’ve decided to make a class cycle collected, there are a few things you need to add to your implementation:
Cycle collected refcounting. Special refcounting is needed so that the CC can tell when an object is created, used, or destroyed, so that it can determine if an object is potentially part of a garbage cycle.
Traversal. Once the CC has decided an object might be garbage, it needs to know what other cycle collected objects it holds strong references to. This is done with a “traverse” method.
Unlinking. Once the CC has decided that an object is garbage, it needs to break the cycles by clearing out all strong references to other cycle collected objects. This is done with an “unlink” method. This usually looks very similar to the traverse method.
The traverse and unlink methods, along with other methods needed for cycle collection, are defined on a special inner class object, called a “participant”, for performance reasons. The existence of the participant is mostly hidden behind macros so you shouldn’t need to worry about it.
Next, we’ll go over what the declaration and definition of these parts look like. (Spoiler: there are lots of ALL_CAPS macros.) This will mostly cover the most common variants. If you need something slightly different, you should look at the location of the declaration of the macros we mention here and see if the variants already exist.
Reference counting
nsISupports
If your class inherits from nsISupports, you’ll need to add NS_DECL_CYCLE_COLLECTING_ISUPPORTS to the class declaration. This will declare the QueryInterface (QI), AddRef and Release methods you need to implement nsISupports, as well as the actual refcount field.
In the .cpp file for your class you’ll have to define the QueryInterface, AddRef and Release methods. The ins and outs of defining the QI method is out-of-scope for this document, but you’ll need to use the special cycle collection variants of the macros, like NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION. (This is because we use the nsISupports system to define a special interface used to dynamically find the correct CC participant for the object.)
Finally, you’ll have to actually define the AddRef and Release methods for your class. If your class is called MyClass, then you’d do this with the declarations NS_IMPL_CYCLE_COLLECTING_ADDREF(MyClass) and NS_IMPL_CYCLE_COLLECTING_RELEASE(MyClass).
non-nsISupports
If your class does not inherit from nsISupports, you’ll need to add NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING to the class declaration. This will give inline definitions for the AddRef and Release methods, as well as the actual refcount field.
Cycle collector participant
Next we need to declare and define the cycle collector participant. This is mostly boilerplate hidden behind macros, but you will need to specify which fields are to be traversed and unlinked because they are strong references to cycle collected objects.
Declaration
First, we need to add a declaration for the participant. As before, let’s say your class is MyClass.
The basic way to declare this for an nsISupports class is NS_DECL_CYCLE_COLLECTION_CLASS(MyClass).
If your class inherits from multiple classes that inherit from nsISupports classes, say Parent1 and Parent2, then you’ll need to use NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MyClass, Parent1) to tell the CC to cast to nsISupports via Parent1. You probably want to pick the first class it inherits from. (The cycle collector needs to be able to cast MyClass* to nsISupports*.)
Another situation you might encounter is that your nsISupports class inherits from another cycle collected class CycleCollectedParent. In that case, your participant declaration will look like NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MyClass, CycleCollectedParent). (This is needed so that the CC can cast from nsISupports down to MyClass.) Note that we do not support inheritance for non-nsISupports classes.
If your class is non-nsISupports, then you’ll need to use the NATIVE family of macros, like NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(MyClass).
In addition to these modifiers, these different variations have further SCRIPT_HOLDER variations which are needed if your class holds alive JavaScript objects. This is because the tracing of JS objects held alive by this class must be declared separately from the tracing of C++ objects held alive by this class so that the garbage collector can also use the tracing. For example, NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(MyClass, Parent1).
There are also WRAPPERCACHE variants of the macros which you need to use if your class is wrapper cached. These are effectively a specialized form of SCRIPT_HOLDER, as a cached wrapper is a JS object held alive by the C++ object. For example, NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(MyClass, Parent1).
There is yet another variant of these macros, SKIPPABLE. This document won’t go into detail here about how this works, but the basic idea is that a class can tell the CC when it is definitely alive, which lets the CC skip it. This is a very important optimization for things like DOM elements in active documents, but a new class you are making cycle collected is likely not common enough to worry about.
Implementation
Finally, you must write the actual implementation of the CC participant, in the .cpp file for your class. This will define the traverse and unlink methods, and some other random helper functions. In the simplest case, this can be done with a single macro like this: NS_IMPL_CYCLE_COLLECTION(MyClass, mField1, mField2, mField3), where mField1 and the rest are the names of the fields of your class that are strong references to cycle collected objects. There is some template magic that says how many common types like RefPtr, nsCOMPtr, and even some arrays, should be traversed and unlinked. There’s also a variant NS_IMPL_CYCLE_COLLECTION_INHERITED, which you should use when there’s a parent class that is also cycle collected, to ensure that fields of the parent class are traversed and unlinked. The name of that parent class is passed in as the second argument. If either of these work, then you are done. Your class is now cycle collected. Note that this does not work for fields that are JS objects.
However, if that doesn’t work, you’ll have to get into the details a bit more. A good place to start is by copying the definition of NS_IMPL_CYCLE_COLLECTION.
For a script holder method, you also need to define a trace method in addition to the traverse and unlink, using NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN and other similar macros. You’ll need to include all of the JS fields that your class holds alive. The trace method will be used by the GC as well as the CC, so if you miss something you can end up with use-after-free crashes. You’ll also need to call mozilla::HoldJSObjects(this); in the ctor for your class, and mozilla::DropJSObjects(this); in the dtor. This will register (and unregister) each instance of your object with the JS runtime, to ensure that it gets traced properly. This does not apply if you have a wrapper cached class that does not have any additional JS fields, as nsWrapperCache deals with all of that for you.