XPConnect object wrapping

From MozillaWiki
Jump to: navigation, search

Note - This article describes XPConnect circa 2007, and is woefully out of date

Some notes on wrapped natives and native wrappers

Wrapped Natives

Every C++ nsISupports* can be "wrapped" by a JS object. In such a scenario, the JS object holds an XPCWrappedNative* in its private slot. The cluster of "JSObject+XPCWrappedNative" is sort of considered as a unit; code and comments frequently refer to the JS side or the C++ side as "a wrapped native".

The picture looks like this:

                                                              __
                                                                \
+----------------------------------------------------------+    |
| JSObject { JSClass *class = { "HTMLDivElement", ... }    |    |
|            slots[0] = ...                                |    |
|            slots[JSSLOT_PRIVATE] = XPCWrappedNative* }   |    |
+----------------------------------------------------|-----+    |  "the
                                            ^        |           \  wrapper"
                                            |        V           /
+-------------------------------------------|--------------+    |
| XPCWrappedNative { JSObject* mFlatJSObject;              |    |
|                    ...                                   |    |
|                    nsISupports* mIdentity; }             |    |
+------------------------------------------|---------------+    |
                                           |                  __/
                                           V
+----------------------------------------------------------+
| nsHTMLDivElement { ... }                                 |
+----------------------------------------------------------+

Note that the JSObject has a special JSClass that is named after the object it's wrapping. The JSObject forwards calls to the wrapped C++ object using helpers in this class.


Lifecycle and refcounts

When constructing the collection of objects pictured above, we can imagine that the caller was C++, and was providing the nsHTMLDivElement*. In such a case, the caller would have received back an XPCWrappedNative* with refcount 2. Why 2?

  1. The reference any caller usually gets to an XPCOM object it just asked for.
  2. The reference the JSObject's private slot has to the XPCWrappedNative.

This models a simple fact: for the XPCWrappedNative to die, both the C++ XPCOM world and the JS heap ought to have forgotten about it. If either side still sees it, it should stay alive.

Thus the JSObject keeps the XPCWrappedNative alive, and the XPCWrappedNative keeps the nsISupports that it's pointing to alive. But there's an important detail: the XPCWrappedNative does not directly keep the JSObject alive. Doing so unconditionally -- for example rooting the JSObject -- would give rise to a cyclic graph: the pair of objects would keep each other alive indefinitely. So the XPCWrappedNative holds a pointer to the JSObject, but doesn't root it.

This may prompt the curious reader to wonder what sort of mechanisms exist to keep the JSObject associated with an XPCWrappedNative alive. There are at least four (!) such mechanisms, each tailored to a situation in which it's "appropriate" to keep the JSObject alive:

  1. Obviously, if there's a root in the JS heap that can reach the JSObject, it stays alive.
  2. As long as a C++ caller holds a strong XPCOM reference to the XPCWrappedNative, the XPCWrappedNative has refcount >= 2. It turns out that every live XPCWrappedNative is also held in a hashtable, internal to xpconnect. The hashtable maps nsISupports* to XPCWrappedNative*. During JS GC a function (WrappedNativeJSGCThingMarker in xpcwrappednativescope.cpp) is run over that hashtable, marking the JSObject pointed to by every XPCWrappedNative with refcount > 1. This keeps things like "preserved wrappers" alive (see further below).
  3. Many C++ callers don't construct XPCWrappedNatives themselves, they actually call XPConnect::WrapNative(). WrapNative() constructs the XPCWrappedNative and tables it, but returns a XPCJSObjectHolder pointing to the JSObject. XPCJSObjectHolder does keep the JSObject alive for the duration of its use, by rooting. See XPCJSObjectHolder::XPCJSObjectHolder in xpcwrappednative.cpp.
  4. If the C++ caller is part of xpconnect itself, and is operating on XPCWrappedNatives that aren't in the table yet, the caller may store a pointer to an XPCWrappedNative in an AutoMarkingWrappedNativePtr. This is a special type which inserts itself into a global per-thread linked list and also receives a call during JS GC, which it forwards on to the AutoMark() method on the XPCWrappedNative, which subsequently causes the JSObject to be marked (thus kept alive). It's not clear to me why this is done rather than rooting or automatically inserting into the table, but it is. There is some baroque macrology and locking / work-interleaving code that suggests the answer may be complicated. AutoMarkingWrappedNativePtr is private to the implementation of xpconnect, so you won't see it in normal XPCOM-using client code.

Native Wrappers

There is a further complication to the pretty picture above (and terminology namespace): native wrappers.

A native wrapper, despite name-similarity to "wrapped native", is not just another term for one side of this JSObject+XPCWrappedNative cluster. It's a specific state that the JSObject involved in the cluster can have.

Concretely: the JSObject's JSClass can be XPCNativeWrapper::sXPC_NW_JSClass.base. When this is true, the JSObject is said to be a native wrapper.

Here is the picture:

+------------------------------------------------------------+
| JSObject { JSClass *class = { "XPCNativeWrapper", ... }    |
|            slots[0] = ...                                  |
|            slots[JSSLOT_PRIVATE] = XPCWrappedNative* }     |
+----------------------------------------------------|-------+
                                            ^        |
                                            |        V
+-------------------------------------------|----------------+
| XPCWrappedNative { JSObject* mFlatJSObject;                |
|                    ...                                     |
|                    nsISupports* mIdentity; }               |
+------------------------------------------|-----------------+
                                           |
                                           V
+------------------------------------------------------------+
| nsHTMLDivElement { ... }                                   |
+------------------------------------------------------------+

The picture looks similar to the first one, and is similar in terms of memory ownership semantics. The functional difference is that a XPCNativeWrapper performs property lookup differently from a normal wrapped native. The difference is related to a certain type of attack vector involving property-name shadowing. (See mdc:Safely accessing content DOM from chrome and mdc:XPCNativeWrapper.)

For example, suppose chrome JS associated with some browser logic (to maintain, e.g., the current document's title in the window titlebar; or hover-sensitive context menu items) or extension were to walk the content DOM. Doing so involves acquiring wrappers to content DOM nodes and calling DOM methods on the JSObject side of those wrappers, but with chrome privilege. If content were malicious, it could shadow DOM methods that are supposed to pass through to the underlying C++ DOM element, by attaching same-named properties to the wrapper JSObject. If these shadowed properties contained attack code, and other bugs bit, chrome would call the attack code with chrome privilege. Even without other bugs, chrome could be misled by content properties.

So instead of returning a normal wrapper to JS chrome in such a case, we return a wrapper whose JSObject has class XPCNativeWrapper. This wrapper still has an XPCWrappedNative object that points to the DOM node, but the JSObject in the cluster routes property-lookup requests through JSClass methods in XPCNativeWrapper.cpp. Such a wrapper performs special property lookup that's guaranteed to pass through to the underlying wrapped C++ object, and ignore script properties that might shadow native properties.

Unfortunately code and comments are not always careful with the term "wrapper", and use it to refer to either case, as well as either the JS or C++ objects in either case. So when a comment mentions "the wrapper" it may be talking about an XPCWrappedNative or a JSObject, in either "native wrapper" or "wrapped native" scenarios.

Personally, I wish XPCNativeWrapper had been called "security device" or something. It looks a lot like XPCWrappedNative, and my eyes often gloss one as the other (it's especially annoying given that both classes have some of the same methods!)

Oh well.

Preserved Wrappers

When a script obtains "a wrapper" (either a plain wrapped native or a native wrapper, see above) it thinks it has a normal JS object. Normal JS objects permit scripts to stick new properties on them. So scripts expect wrappers to retain properties that are stuck to them.

Unfortunately if you draw a picture of this you'll soon realize that there's not enough connectivity for that to work by default. Here's a picture of an HTML element with a script property -- a JSString -- stuck on it:

+------------------------------------------------------------+
| JSString { "hello world" }                                 |
+------------------------------------------------------------+
                            ^
                            |
+---------------------------|--------------------------------+
| JSObject { slots[N] = { jsval }                            |
|            JSClass *class = { "HTMLDivElement", ... }      |
|            slots[JSSLOT_PRIVATE] = XPCWrappedNative* }     |
+----------------------------------------------------|-------+
                                            ^        |
                                            |        V
+-------------------------------------------|----------------+
| XPCWrappedNative { JSObject* mFlatJSObject;                |
|                    ...                                     |
|                    nsISupports* mIdentity; }               |
+-------------------------------------------|----------------+
                                            |
                                            V
+------------------------------------------------------------+
| nsHTMLDivElement { ... }                                   |
+------------------------------------------------------------+

The script asked for a div element from the DOM, got a wrapper (an XPCWrappedNative+JSObject cluster) and put a string on the JSObject side of the wrapper. All well and good until the script then stops running. The DOM isn't holding any references to the XPCWrappedNative, and there are no roots, so the JSObject is soon GC'ed and the XPCWrappedNative is recycled. That's ideal most of the time because wrappers are generally supposed to be ephemeral; they're instantiated for the duration of a call and then thrown out. We wouldn't normally want to keep them around.

But in this case it's no good: the string is forgotten about. If script later goes looking for the property it set, the string is gone. So we have one more trick: when someone mutates a wrapper, we tell the C++ DOM node to "preserve" the wrapper (at least the XPCWrappedNative side of it, which is enough), keeping it alive by storing a strong XPCOM reference to it in a hashtable associated with the DOM node.

When such a DOM node is deleted, it releases all the "preserved wrappers" associated with it. Until then, properties added to the wrapper by script content persist.

Tearoffs

There are two uses of the term "tearoff" in XPCOM.

The first use describes a space optimization in objects that nominally support quite a lot of interfaces, but in reality only ever have a smaller number of those interfaces used. In such cases it pays to move the less-often-used parts of the interface into auxiliary objects that are constructed on demand. These auxiliary objects are called "tearoffs". A tearoff "pretends" to share its identity with the base object it was "torn off" of, by maintaining a pointer to the base object. Any time someone attempts to QI the tearoff to its base type, the tearoff addref's the base object and returns the pointer to the base object it holds. Any time someone attempts to QI the base object for the tearoff type, the base object manufactures a tearoff (that points back to the base object) and returns it. A tearoff typically keeps its base object alive. The base object may also cache its tearoff, if it expects to reuse it, but this will typically create an ownership cycle.

The second use describes a particular class in XPConnect called XPCWrappedNativeTearOff. Objects of this type are a speed optimization. They are manufactured by XPConnect in order to cache the results of a QI performed by script code manipulating a wrapped native. Suppose a script has a wrapped native base object: XPConnect thinks this base object is an nsISupports. Every time a script accesses it as an nsIFoo, XPConnect is obliged to QI the nsISupports to nsIFoo. This takes time. To avoid doing this on every call, XPConnect builds a an XPCWrappedNativeTearOff after the first QI and stores an XPCNativeInterface, a reflected JSObject, and the nsISupports returned from the QI (which may itself be a normal XPCOM tearoff, so is not necessarily the base nsISupports). The reflected JSObject in the tearoff is returned to the script, so that future calls through it will use the cached XPCNativeInterface in the XPCWrappedNativeTearOff, and not perform any new QI.