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 criterion 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.