Skip to content

JS Wrappers and IDL Files

Overview

In addition to typical C++ translation units (.cpp) and C++ header files (.cpp) along with some Objective-C and Objective-C++ files, WebCore contains hundreds of Web IDL (.idl) files. Web IDL is an interface description language and it's used to define the shape and the behavior of JavaScript API implemented in WebKit.

When building WebKit, a perl script generates appropriate C++ translation units and C++ header files corresponding to these IDL files under WebKitBuild/Debug/DerivedSources/WebCore/ where Debug is the current build configuration (e.g. it could be Release-iphonesimulator for example).

These auto-generated files along with manually written files Source/WebCore/bindings are called JS DOM binding code and implements JavaScript API for objects and concepts whose underlying shape and behaviors are written in C++.

For example, C++ implementation of Node is Node class and its JavaScript interface is implemented by JSNode class. The class declaration and most of definitions are auto-generated at WebKitBuild/Debug/DerivedSources/WebCore/JSNode.h and WebKitBuild/Debug/DerivedSources/WebCore/JSNode.cpp for debug builds. It also has some custom, manually written, bindings code in Source/WebCore/bindings/js/JSNodeCustom.cpp. Similarly, C++ implementation of Range interface is Range class whilst its JavaScript API is implemented by the auto-generated JSRange class (located at WebKitBuild/Debug/DerivedSources/WebCore/JSRange.h and WebKitBuild/Debug/DerivedSources/WebCore/JSRange.cpp for debug builds) We call instances of these JSX classes JS wrappers of X.

These JS wrappers exist in what we call a DOMWrapperWorld. Each DOMWrapperWorld has its own JS wrapper for each C++ object. As a result, a single C++ object may have multiple JS wrappers in distinct DOMWrapperWorlds. The most important DOMWrapperWorld is the main DOMWrapperWorld which runs the scripts of web pages WebKit loaded while other DOMWrapperWorlds are typically used to run code for browser extensions and other code injected by applications that embed WebKit. Diagram of JS wrappers JSX.h provides toJS functions which creates a JS wrapper for X in a given global object’s DOMWrapperWorld, and toWrapped function which returns the underlying C++ object. For example, toJS function for Node is defined in Source/WebCore/bindings/js/JSNodeCustom.h.

When there is already a JS wrapper object for a given C++ object, toJS function will find the appropriate JS wrapper in a hash map of the given DOMWrapperWorld. Because a hash map lookup is expensive, some WebCore objects inherit from ScriptWrappable, which has an inline pointer to the JS wrapper for the main world if one was already created.

JS Wrapper Lifecycle Management

As a general rule, a JS wrapper keeps its underlying C++ object alive by means of reference counting in JSDOMWrapper temple class from which all JS wrappers in WebCore inherits. However, C++ objects do not keep their corresponding JS wrapper in each world alive by the virtue of them staying alive as such a circular dependency will result in a memory leak.

There are two primary mechanisms to keep JS wrappers alive in WebCore:

  • Visit Children - When JavaScriptCore’s garbage collection visits some JS wrapper during the marking phase, visit another JS wrapper or JS object that needs to be kept alive.
  • Reachable from Opaque Roots - Tell JavaScriptCore’s garbage collection that a JS wrapper is reachable from an opaque root which was added to the set of opaque roots during marking phase.

Visit Children

Visit Children is the mechanism we use when a JS wrapper needs to keep another JS wrapper or JS object alive.

For example, ErrorEvent object uses this method in Source/WebCore/bindings/js/JSErrorEventCustom.cpp to keep its "error" IDL attribute as follows:

template<typename Visitor>
void JSErrorEvent::visitAdditionalChildren(Visitor& visitor)
{
    wrapped().originalError().visit(visitor);
}

DEFINE_VISIT_ADDITIONAL_CHILDREN(JSErrorEvent);

Here, DEFINE_VISIT_ADDITIONAL_CHILDREN macro generates template instances of visitAdditionalChildren which gets called by the JavaScriptCore's garbage collector. When the garbage collector visits an instance ErrorEvent object, it also visits wrapped().originalError(), which is the JavaScript value of "error" attribute:

class ErrorEvent final : public Event {
...
    const JSValueInWrappedObject& originalError() const { return m_error; }
    SerializedScriptValue* serializedError() const { return m_serializedError.get(); }
...
    JSValueInWrappedObject m_error;
    RefPtr<SerializedScriptValue> m_serializedError;
    bool m_triedToSerialize { false };
};

Note that JSValueInWrappedObject uses Weak, which does not keep the referenced object alive on its own. We can't use a reference type such as Strong which keeps the referenced object alive on its own since the stored JS object may also have this ErrorEvent object stored as its property. Because the garbage collector has no way of knowing or clearing the Strong reference or the property to ErrorEvent in this hypothetical version of ErrorEvent, it would never be able to collect either object, resulting in a memory leak.

To use this method of keeping a JavaScript object or wrapper alive, add JSCustomMarkFunction to the IDL file, then introduce JS*Custom.cpp file under Source/WebCore/bindings/js and implement template<typename Visitor> void JS*Event::visitAdditionalChildren(Visitor& visitor) as seen above for ErrorEvent.

visitAdditionalChildren is called concurrently while the main thread is running. Any operation done in visitAdditionalChildren needs to be multi-thread safe. For example, it cannot increment or decrement the reference count of a RefCounted object or create a new WeakPtr from CanMakeWeakPtr since these WTF classes are not thread safe.

Opaque Roots

Reachable from Opaque Roots is the mechanism we use when we have an underlying C++ object and want to keep JS wrappers of other C++ objects alive.

To see why, let's consider a StyleSheet object. So long as this object is alive, we also need to keep the DOM node returned by the ownerNode attribute. Also, the object itself needs to be kept alive so long as the owner node is alive since this [StyleSheet object] can be accessed via sheet IDL attribute of the owner node. If we were to use the visit children mechanism, we need to visit every JS wrapper of the owner node whenever this StyleSheet object is visited by the garbage collector, and we need to visit every JS wrapper of the StyleSheet object whenever an owner node is visited by the garbage collector. But in order to do so, we need to query every DOMWrapperWorld's wrapper map to see if there is a JavaScript wrapper. This is an expensive operation that needs to happen all the time, and creates a tie coupling between Node and StyleSheet objects since each JS wrapper objects need to be aware of other objects' existence.

Opaque roots solves these problems by letting the garbage collector know that a particular JavaScript wrapper needs to be kept alive so long as the gargabe collector had encountered specific opaque root(s) this JavaScript wrapper cares about even if the garbage collector didn't visit the JavaScript wrapper directly. An opaque root is simply a void* identifier the garbage collector keeps track of during each marking phase, and it does not conform to a specific interface or behavior. It could have been an arbitrary integer value but void* is used out of convenience since pointer values of live objects are unique.

In the case of a StyleSheet object, StyleSheet's JavaScript wrapper tells the garbage collector that it needs to be kept alive because an opaque root it cares about has been encountered whenever ownerNode is visited by the garbage collector.

In the most simplistic model, the opaque root for this case will be the ownerNode itself. However, each Node object also has to keep its parent, siblings, and children alive. To this end, each Node designates the root node as its opaque root. Both Node and StyleSheet objects use this unique opaque root as a way of communicating with the gargage collector.

For example, StyleSheet object informs the garbage collector of this opaque root when it's asked to visit its children in JSStyleSheetCustom.cpp:

template<typename Visitor>
void JSStyleSheet::visitAdditionalChildren(Visitor& visitor)
{
    visitor.addOpaqueRoot(root(&wrapped()));
}

Here, void* root(StyleSheet*) returns the opaque root of the StyleSheet object as follows:

inline void* root(StyleSheet* styleSheet)
{
    if (CSSImportRule* ownerRule = styleSheet->ownerRule())
        return root(ownerRule);
    if (Node* ownerNode = styleSheet->ownerNode())
        return root(ownerNode);
    return styleSheet;
}

And then in JSStyleSheet.cpp (located at WebKitBuild/Debug/DerivedSources/WebCore/JSStyleSheet.cpp for debug builds) JSStyleSheetOwner (a helper JavaScript object to communicate with the garbage collector) tells the garbage collector that JSStyleSheet should be kept alive so long as the garbage collector had encountered this StyleSheet's opaque root:

bool JSStyleSheetOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void*, AbstractSlotVisitor& visitor, const char** reason)
{
    auto* jsStyleSheet = jsCast<JSStyleSheet*>(handle.slot()->asCell());
    void* root = WebCore::root(&jsStyleSheet->wrapped());
    if (UNLIKELY(reason))
        *reason = "Reachable from jsStyleSheet";
    return visitor.containsOpaqueRoot(root);
}

Generally, using opaque roots as a way of keeping JavaScript wrappers involve two steps:

  1. Add opaque roots in visitAdditionalChildren.
  2. Return true in isReachableFromOpaqueRoots when relevant opaque roots are found.

The first step can be achieved by using the aforementioned JSCustomMarkFunction with visitAdditionalChildren. Alternatively and more preferably, GenerateAddOpaqueRoot can be added to the IDL interface to auto-generate this code. For example, AbortController.idl makes use of this IDL attribute as follows:

[
    Exposed=(Window,Worker),
    GenerateAddOpaqueRoot=signal
] interface AbortController {
    [CallWith=ScriptExecutionContext] constructor();

    [SameObject] readonly attribute AbortSignal signal;

    [CallWith=GlobalObject] undefined abort(optional any reason);
};

Here, signal is a public member function funtion of the underlying C++ object:

class AbortController final : public ScriptWrappable, public RefCounted<AbortController> {
    WTF_MAKE_ISO_ALLOCATED(AbortController);
public:
    static Ref<AbortController> create(ScriptExecutionContext&);
    ~AbortController();

    AbortSignal& signal();
    void abort(JSDOMGlobalObject&, JSC::JSValue reason);

private:
    explicit AbortController(ScriptExecutionContext&);

    Ref<AbortSignal> m_signal;
};

When GenerateAddOpaqueRoot is specified without any value, it automatically calls opaqueRoot() instead.

Like visitAdditionalChildren, adding opaque roots happen concurrently while the main thread is running. Any operation done in visitAdditionalChildren needs to be multi-thread safe. For example, it cannot increment or decrement the reference count of a RefCounted object or create a new WeakPtr from CanMakeWeakPtr since these WTF classes are not thread safe.

The second step can be achived by adding CustomIsReachable to the IDL file and implementing JS*Owner::isReachableFromOpaqueRoots in JS*Custom.cpp file. Alternatively and more preferably, GenerateIsReachable can be added to IDL file to automatically generate this code with the following values:

  • No value - Adds the result of calling root(T*) on the underlying C++ object of type T as the opaque root.
  • Impl - Adds the underlying C++ object as the opaque root.
  • ReachableFromDOMWindow - Adds a DOMWindow returned by window() as the opaque root.
  • ReachableFromNavigator - Adds a Navigator returned by navigator() as the opaque root.
  • ImplDocument - Adds a Document returned by document() as the opaque root.
  • ImplElementRoot - Adds the root node of a Element returned by element() as the opaque root.
  • ImplOwnerNodeRoot - Adds the root node of a Node returned by ownerNode() as the opaque root.
  • ImplScriptExecutionContext - Adds a ScriptExecutionContext returned by scriptExecutionContext() as the opaque root.

Similar to visiting children or adding opaque roots, whether an opaque root is reachable or not is checked in parallel. However, it happens while the main thread is paused unlike visiting children or adding opaque roots, which happen concurrently while the main thread is running. This means that any operation done in JS*Owner::isReachableFromOpaqueRoots or any function called by GenerateIsReachable cannot have thread unsafe side effects such as incrementing or decrementing the reference count of a RefCounted object or creating a new WeakPtr from CanMakeWeakPtr since these WTF classes' mutation operations are not thread safe.

Active DOM Objects

Visit children and opaque roots are great way to express lifecycle relationships between JS wrappers but there are cases in which a JS wrapper needs to be kept alive without any relation to other objects. Consider XMLHttpRequest. In the following example, JavaScript loses all references to the XMLHttpRequest object and its event listener but when a new response gets received, an event will be dispatched on the object, re-introducing a new JavaScript reference to the object. That is, the object survives garbage collection's mark and sweep cycles without having any ties to other "root" objects.

function fetchURL(url, callback)
{
    const request = new XMLHttpRequest();
    request.addEventListener("load", callback);
    request.open("GET", url);
    request.send();
}

In WebKit, we consider such an object to have a pending activity. Expressing the presence of such a pending activity is a primary use case of ActiveDOMObject.

By making an object inherit from ActiveDOMObject and annotating IDL as such, WebKit will automatically generate isReachableFromOpaqueRoot function which returns true whenever ActiveDOMObject::hasPendingActivity returns true even though the garbage collector may not have encountered any particular opaque root to speak of in this instance.

In the case of XMLHttpRequest, hasPendingActivity will return true so long as there is still an active network activity associated with the object. Once the resource is fully fetched or failed, it ceases to have a pending activity. This way, JS wrapper of XMLHttpRequest is kept alive so long as there is an active network activity.

There is one other related use case of active DOM objects, and that's when a document enters the back-forward cache and when the entire page has to pause for other reasons.

When this happens, each active DOM object associated with the document gets suspended. Each active DOM object can use this opportunity to prepare itself to pause whatever pending activity; for example, XMLHttpRequest will stop dispatching progress event and media elements will stop playback. When a document gets out of the back-forward cache or resumes for other reasons, each active DOM object gets resumed. Here, each object has the opportunity to resurrect the previously pending activity once again.

Creating a Pending Activity

There are a few ways to create a pending activity on an active DOM objects.

When the relevant Web standards says to queue a task to do some work, one of the following member functions of ActiveDOMObject should be used:

These functions will automatically create a pending activity until a newly enqueued task is executed.

Alternatively, makePendingActivity can be used to create a pending activity token for an active DOM object. This will keep a pending activity on the active DOM object until all tokens are dead.

Finally, when there is a complex condition under which a pending activity exists, an active DOM object can override virtualHasPendingActivity member function and return true whilst such a condition holds. Note that virtualHasPendingActivity should return true so long as there is a possibility of dispatching an event or invoke JavaScript in any way in the future. In other words, a pending activity should exist while an object is doing some work in C++ well before any event dispatching is scheduled. Anytime there is no pending activity, JS wrappers of the object can get deleted by the garbage collector.