Mozilla DOM Hacking Guide

This article is in need of a technical review.

Mozilla gives you the opportunity not only to use very powerful and complete DOM support, but also to work on a world-class implementation of one of the greatest Internet technologies ever created.

Mozilla's DOM is coded almost entirely in C++. Seriously hacking on it requires excellent knowledge of C++ and XPCOM, Mozilla's own component model. In this document I will try to outline the main aspects of the implementation, beginning with the Class Info mechanism, which lies at the heart of the DOM, then with the description of various interfaces and classes. Since I am myself still learning how it works, don't expect this to be a complete reference quite yet. If you can contribute any time or knowledge to this document, it is greatly appreciated!

Target audience: People interested in learning how the DOM is implemented. Prior knowledge of C++ and XPCOM is assumed. If you don't know XPCOM yet, and would like to be able to read this document quickly, you can read the Introduction to XPCOM for the DOM. Otherwise, for more detailed XPCOM documentation, please see the XPCOM project page.

Class Info and Helper Classes

Introduction to Class Info

Class Info is what gives the DOM classes their correct behavior when used through XPConnect. It lies at the heart of the famous "XPCDOM landing" that happened in May. We will talk a lot about XPConnect in this document, since it is so important for the DOM. By "correct behavior", I mean "the intended behavior with respect to the specification or de facto standard". We will see that Class Info is mainly used to implement the DOM Level 0. The W3C DOM is mainly implemented in IDL. The goals of Class Info are twofold: Interface flattening, and implementing behaviors that are not possible with IDL alone.

A brief introduction to JavaScript and XPConnect.

Before we begin the explanation of Class Info, I'd like to introduce quickly the JavaScript engine and XPConnect. In JavaScript, there is no knowledge of types, like there is in C++. A function for example can be represented by a JSFunction, a JSObject, a jsval, ... This means that when we use the DOM from JavaScript, we pass arguments that have no type. However, since the DOM is coded in C++, we expect to receive an argument of the correct type for our function. This is one of the jobs of XPConnect. XPConnect will "wrap" the argument in a wrapper that will be of the type expected by our C++ function. Similarly, then return type of the C++ function will be wrapped by XPConnect so that JavaScript can use it safely.

When, in JavaScript, a client tries to access a DOM object or a DOM method on a DOM object, the JS engine asks XPConnect to search for the relevant C++ method to call. For example, when we ask for |document.getElementById("myID");|, XPConnect will find that |document| is a property of the window object, so it will look on the interface nsIDOMWindow, and it will find the GetDocument() method. The return value of GetDocument() is a nsIDOMDocument. So XPConnect will then try to find a method named GetElementById() on the nsIDOMDocument interface. And indeed it will find it, and thus call it.

This is the schema used most of the time when using W3C DOM objects and methods. It is however different for some DOM Level 0 objects and methods. I'll take two very different examples. The first one is the window.location object (the same holds true for document.location, actually). We can change the URL of the current window by assigning window.location. In IDL, location is declared to be a readonly attribute. This is because, if we had a SetLocation() method, it would take an nsIDOMLocation parameter, and not a URL string. Instead, in the helper class for the window object (nsWindowSH, see the next Section), we define the GetProperty() member function. GetProperty() is a function used by XPConnect when we are setting an unknown property on the object (window, in our case). In GetProperty(), we check if the property being set is "location". If that is the case, we call nsIDOMLocation::SetHref(). In fact, when setting window.location, we really set window.location.href. This is all possible thanks to the magic of the interaction between XPConnect and the DOM.

The second example is the history object. Other browsers allow the history object to be used like an array, e.g. history[1]. The behavior "act as an array" cannot be reflected in the IDL itself. Fortunately, XPConnect provides us with a way to make our class available as an array in JavaScript. I'm talking about the "scriptable flags". The nsIXPCScriptable interface, implemented by the nsDOMClassInfo class (see Section) defines several flags, one of which is the WANT_GETPROPERTY flag. When set, it allows us to define a GetProperty() function on nsHistorySH (the helper class for the history object), which will handle the array behavior. Indeed, it will forward the call history[1] to history.item(1), which is defined in the IDL and easily coded. The relevant code is at nsDOMClassInfo.cpp, around line 4520.

These two examples demonstrate the power of the DOM combined with XPConnect and the JavaScript engine. The possibilities are endless. "What do you want to code today?" ;-)

All the DOM classes are listed in an enum defined in nsIDOMClassInfo.h, nsDOMClassInfoID. There are classes for the DOM0, Core DOM, HTML, XML, XUL, XBL, range, css, events, etc...

The array sClassInfoData, defined in nsDOMClassInfo.cpp, maps each DOM class to its helper class and to the interfaces that are exposed to JavaScript. It is an array of type nsDOMClassInfoData, which is a structure defined in nsIDOMClassInfo.h. The array uses two macros to define its items: NS_DEFINE_CLASSINFO_DATA and NS_DEFINE_CLASSINFO_DATA_WITH_NAME. The first one calls the second one. The first argument passed to NS_DEFINE_CLASSINFO_DATA_WITH_NAME, _class, is used for debug purposes. The second argument, _name, is the name that should appear in JavaScript. The third argument, _helper, is the name of the helper class for this DOM class. Helper classes are detailed in Section 1.3. The fourth and last argument, _flags, is a bitvector of nsIXPCScriptable flags. The macros for those flags are defined nsDOMClassInfo.cpp. The flags give special behavior through XPConnect. See also Section 1.9.

The nsDOMClassInfoData objects are created in the sClassInfoData array by explicitly initializing it. Here is the description of the structure:

  • const char *mName: C-style string that is passed as second argument to the macro. It is the name of the JavaScript object that will be available in the browser through the DOM.
  • union {
    nsDOMClassInfoConstructorFnc mConstructorFptr;
    nsDOMClassInfoExternalConstructorFnc mExternalConstructorFptr;
    } u;
    
    This union is a pointer to a function typedef'ed:
    typedef nsIClassInfo* (*nsDOMClassInfoConstructorFnc)(nsDOMClassInfoID aID);
    or
    typedef nsIClassInfo* (*nsDOMClassInfoExternalConstructorFnc) (const char* aName);
    It is initialized with the doCreate member function of the helper class passed as third argument to the macro.
  • nsIClassInfo *mCachedClassInfo: mCachedClassInfo holds an nsIClassInfo pointer to an instance of the relevant helper class.
  • const nsIID *mProtoChainInterface: Pointer to the IID of the first interface available to JavaScript clients. This is used in global resolve functions, when XPConnect has to find the member function to call.
  • const nsIID **mInterfaces: Pointer to the first element of an array of pointers to all the interfaces available through JS for this class.
  • PRUInt32 mScriptableFlags: 31; : The fourth argument passed to NS_DEFINE_CLASSINFO_DATA_WITH_NAME.
  • PRBool mHasClassInterface: 1; : Help me?

mName and mConstructorFptr, mScriptableFlags and mHasInterface are initialized by NS_DEFINE_CLASSINFO_DATA_WITH_NAME. mCachedClassInfo, mProtoChainInterface and mInterfaces, however, are initialized in nsDOMClassInfo::Init(), described in Section 1.5.

Interface flattening

One of the nicest -- and most important -- features of the XPConnect'ed DOM is the interface flattening. "Interface flattening is the ability to call methods on an object regardless of the interface it was defined on." For example, when we have the document object in JavaScript, we can call indistinctly document.getElementById(), or document.addEventListener(), although they are defined on two different interfaces (DOMDocument and DOMEventTarget. Needless to say this is critical for the use of the DOM in real-world content.

In Mozilla interface flattening is obtained through the use of the nsIClassInfo interface. nsIClassInfo stores the interfaces available for an object and later on XPConnect uses those interfaces to lookup the right method to call.

The great thing is that one can easily see the interfaces available from JS through interface flattening by looking at the code. The interesting part is in nsDOMClassInfo::Init(). There we have a long list of macros. There is one set of macros per DOM class. There you can see, for each object, what interfaces are part of the "flattened" set. As an example, on the window object, we can call all the methods defined on the following interfaces: nsIDOMWindow, nsIDOMJSWindow, nsIDOMEventReciever, nsIDOMEventTarget, nsIDOMViewCSS, and nsIDOMAbstractView. Again, without caring on what interface the method is defined. See Section 1.5 for more information about the Init() method.

For the W3C DOM (Level 1, 2, 3) objects, for each object there is one "standards-compliant" interface, which is exactly the same as the W3C one, named nsIDOM<ObjectName>.idl, and a mozilla-specific extension interface, named nsIDOMNS<ObjectName>.idl, for compatibility with DOM Level 0. For example, the HTML "area" element has the following interfaces in its flattened set: nsIDOMHTMLAreaElement and nsIDOMNSHTMLAreaElement.

Helper Classes

nsDOMClassInfo.h defines several new classes. They all end in "SH", for "Scriptable Helper" e.g. nsWindowSH, nsElementSH, ... . We call these classes the "Helper Classes". All the helper classes inherit from the nsDOMClassInfo class. To demonstrate this, look in nsDOMClassInfo.h. We can see that the nsEventRecieverSH helper class inherits from nsDOMGenericSH:

class nsEventRecieverSH : public nsDOMGenericSH

And nsDOMGenericSH is typedef'ed to nsDOMClassInfo:

typedef nsDOMClassInfo nsDOMGenericSH;

Another example is nsWindowSH, which inherits from nsEventReceiverSH, thus inheriting from nsDOMClassInfo.

Each DOM class is mapped to its helper class during the initialization of the sClassInfoData array.

Each helper class has a public doCreate member function that is called by GetClassInfoInstance (see also Section 1.6) to create a new instance of the class if needed. Remember that the doCreate member function is called through a pointer to a function named mConstructorFptr, a member of the nsDOMClassInfoData struct. An instance of a helper class is created the first time XPConnect needs access to the flattened set of interfaces of an object. The instance is then cached for further use.

Most of the helper classes implement one or more nsIXPCScriptable methods. Those methods are used by XPConnect when we require something from JavaScript that was not defined in IDL. For example, GetProperty() is used when retrieving an attribute that was not defined in IDL, and NewResolve() is used when resolving for the first time an attribute or method that was not previously resolved. Please see the nsIXPCScriptable interface for more information.

The nsDOMClassInfo class

The heart of Class Info is the nsDOMClassInfo class, defined in nsDOMClassInfo.h. It implements two interfaces besides nsISupports: nsIXPCScriptable and nsIClassInfo.

We already know what nsIXPCScriptable is used for (see the previous Section).

nsIClassInfo is an XPCOM interface, very well described by Mike Shaver in this overview of nsIClassInfo. Basically it contains convenient methods to find out about the interfaces an object promises to support. In our case, this list of interfaces will be populated by "Class Info". See also Section 1.5 on the Init() function and Section 1.2 on interface flattening.

We saw in Section 1.3 that nsDOMClassInfo is the base class for all the helper classes. Let's see what it's made of. Let's begin with the public interface.

  • Constructor: It is called for each created helper class through the member initialization list. It simply initializes the mID data member with the aID argument.
  • The nsIXPCScriptable, nsISupports, and nsIClassInfo member functions, declared with NS_DECL_X macros.
  • static nsIClassInfo* GetClassInfoInstance(nsDOMClassInfoID aID):
    this helper method returns a non-refcounted nsIClassInfo pointer to an instance of the helper class corresponding to the ID passed in. The implementation is detailed in Section 1.6.
  • static nsIClassInfo* GetClassInfoInstance(nsDOMClassInfoData* aData);:
    this helper method returns a non-refcounted nsIClassInfo pointer to an instance of the helper class corresponding to the Data passed in. The implementation is detailed in Section 1.6.
  • static void ShutDown():
    Releases the interface pointers.
  • static nsIClassInfo* doCreate(nsDOMClassInfoData* aData):
    Inline function that returns a nsIClassInfo pointer to a new instance of the nsDOMClassInfo class.
  • static nsresult WrapNative(...): XPConnect fu, not our problem.
  • static nsresult ThrowJSException(JSContext *cx, nsresult aResult);:
    help me!
  • static nsresult InitDOMJSClass(JSContext *cx, JSObject *obj);:
    help me!
  • static JSClass sDOMJSClass;:
    help me!

Protected section:

  • const nsDOMClassInfoData* mData;: help me!
  • static nsresult Init(): Called only once, it is used to initialize the remaining members of the nsDOMClassInfoData structure, as mentioned above. Once called, Init() sets sIsInitialized to true, to remember that the initialization has been performed. The implementation is described in Section 1.5.
  • static nsresult RegisterClassName(PRInt32 aDOMClassInfoID): help me!
  • static nsresult RegisterClassProtos(PRInt32 aDOMClassInfoID): help me!
  • static nsresult RegisterExternalClasses();: help me!
  • nsresult ResolveConstructor(JSContext *cx, JSObject *obj, JSObject **objp);: help me!
  • static PRInt32 GetArrayIndexFromId(JSContext *cx, jsval id, PRBool *aIsNumber =
    nsnull)
    :
    If the JS value is an integer, then *aIsNumber is true, and the integer is returned. Else, *aIsNumber is false and -1 is returned.
  • static inline PRBool IsReadonlyReplaceable(jsval id) { ... }: help me!
  • static inline PRBool IsWritableReplaceable(jsval id) { ... }: help me!
  • nsresult doCheckPropertyAccess(...): help me! (bug 90757)
  • static JSClass sDOMConstructorProtoClass: XPConnect fu to expose the DOM objects constructors to JavaScript.
  • static JSFunctionSpec sDOMJSClass_methods[];: help me! (bug 91557)
  • static nsIXPConnect *sXPConnect: Used to call nsIXPConnect methods that we need. Initialized in Init().
  • static nsIScriptSecurityManager *sSecMan: Used by the DOM security engine. Initialized in Init().
  • static nsresult DefineStaticJSVals(JSContext *cx);: Used to define all the static JSString data members of nsDOMClassInfo.
  • static PRBool sIsInitialized:
    Keeps track of wether Class Info was already initialized, because Init() shouldn't be called twice.
  • static jsval *sX_id: strings used in the global resolve methods for comparison with the passed in arguments. They represent special words for the DOM. Initialized by DefineStaticJSVals().
  • static const JSClass *sObjectClass: help me!
  • static PRBool sDoSecurityCheckInAddProperty;: help me!

nsDOMClassInfo::Init()

This method is to be called only once. Its purpose is, well, to initialize... It does a lot of different things: Fill the blanks in the sClassInfoData array, initialize the sXPConnect and sSecMan data members, create a new JavaScript Context, define the JSString data members, and register class names and class prototypes. Finally it sets sIsInitialized to true. The actions that concern the DOM are described below.

First, the call to CallGetService() initializes sXPConnect. Then the Script Security Manager (sSecMan) is initialized. GetSafeJSContext() grabs us a cool JS context to run our JavaScript code in. The part about ComponentRegistrar is designed to allow external modules (in this case XPath) to be included in DOMClassInfo and as such benefit from the JavaScript benefits it provides. After that, we fill the blanks in the sClassInfoData array.

If you remember the discussion in the introduction to Class Info, there is the main array, sClassInfoData, filled with objects of type nsDOMClassInfoData. However when the array is created, three data members of the structure are left as null pointers: mCachedClassInfo, mProtoChainInterface, and mInterfaces. Init() uses a set of macros to fill the blanks: the DOM_CLASSINFO_MAP family. Each DOM class needs to use these macros, otherwise bad things will happen. I will use the example of the Window class to illustrate the use of the macros. Here is the relevant piece of code.

DOM_CLASSINFO_MAP_BEGIN(Window, nsIDOMWindow)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMWindow)
...
DOM_CLASSINFO_MAP_ENTRY(nsIDOMAbstractView)
DOM_CLASSINFO_MAP_END

DOM_CLASSINFO_MAP_BEGIN(_class, _interface) maps to _DOM_CLASSINFO_MAP_BEGIN(_class, &NS_GET_IID(_interface), PR_TRUE). NS_GET_IID is a macro that expands to the IID of the interface passed in. We pass the address of this nsIID object to the second macro.

#define _DOM_CLASSINFO_MAP_BEGIN(_class, _ifptr, _has_class_if)
{
nsDOMClassInfoData &d = sClassInfoData[eDOMClassInfo_##_class##_id];
d.mProtoChainInterface = _ifptr;
d.mHasClassInterface = _has_class_if;
static const nsIID *interface_list[] = {

In this macro, |d| is a reference to the entry of the sClassInfoData array that corresponds to the class passed as an argument to the macro. The mProtoChainInterface member pointer is initialized to the address of the IID of the interface passed as an argument to DOM_CLASSINFO_MAP_BEGIN. A static array of pointers to objects of type nsIID is then declared. It is initialized explicitly with the DOM_CLASSINFO_MAP_ENTRY macro (see below).

There are two other similar macros:

#define DOM_CLASSINFO_MAP_BEGIN_NO_PRIMARY_INTERFACE(_class)
_DOM_CLASSINFO_MAP_BEGIN(_class, nsnull, PR_TRUE)

This macro is used if the DOM class (for example XMLHTTPRequest) does not have any interface, yet you want the XMLHTTPRequest object to be available from JavaScript.

#define DOM_CLASSINFO_MAP_BEGIN_NO_CLASS_IF(_class, _interface)
_DOM_CLASSINFO_MAP_BEGIN(_class, &NS_GET_IID(_interface), PR_FALSE)

This macro should be used for DOM classes that have no "leaf" interface. For example, there is no HTMLSpanElement in the W3C DOM specification. Therefore, the first interface in the prototype chain for the span element is HTMLElement. However we do want to be able to access HTMLSpanElement to modify it. This macro allows you to do that. See bug 92071 for more information. Let's now see how to specify the interfaces available from JavaScript for a particular DOM class.

#define DOM_CLASSINFO_MAP_ENTRY(_if)
&NS_GET_IID(_if),

The array of pointers interface_list is filled with the addresses of the IID's of all the interfaces passed as arguments to the macro. In our Window example, the interfaces are nsIDOMWindow, nsIDOMJSWindow, nsIDOMEventReciever, nsIDOMEventTarget, nsIDOMViewCSS, and nsIDOMAbstractView. Please see Section 1.2 on interface flattening for an explanation of the use of these interfaces. The initialization for a class is finished by the DOM_CLASSINFO_MAP_END macro.

#define DOM_CLASSINFO_MAP_END
nsnull
};
d.mInterfaces = interface_list;
}

The interface_list array is terminated by a null pointer. The line d.mInterfaces = interface_list assigns to mInterfaces the address of the first element of the interface_list array, which is itself a pointer. mInterfaces is thus correctly a pointer to a pointer to an object of type nsIID.

To define the jsvals, Init() simply calls DefineStaticJSVals(). To register the class names and class protos, Init() simply calls RegisterClassProtos and RegisterClassNames. This process might be described in a later document. Finally, sIsInitialized is set to true. Init() returns NS_OK if everything went fine.

nsDOMClassInfo::GetClassInfoInstance()

There are two versions of this function. The first one takes an ID as argument, the second takes a Data struct as argument. This function is very important so let's take a closer look at it. Here is the function definition.

nsIClassInfo* nsDOMClassInfo::GetClassInfoInstance(nsDOMClassInfoID aID)
{
if(!sIsInitialized) {
nsresult rv = Init();
}

if(!sClassInfoData[aID].mCachedClassInfo) {
nsDOMClassInfoData &data = sClassInfoData[aID];
data.mCachedClassInfo = data.u.mConstructorFptr(&data);
NS_ADDREF(data.mCachedClassInfo);
}

return sClassInfoData[aID].mCachedClassInfo;
}

Here is the short explanation:
This method returns the mCachedClassInfo member of the nsDOMClassInfoData structure that corresponds to aID in the sClassInfoData array, if it exists, i.e. if this method has been called before. If it is called for the first time however, mCachedClassInfo is still a null pointer, and the function will create a new instance of the relevant helper class, and cache it in the mCachedClassInfo pointer, then return it.

And for those interested, here the longer explanation.

The first time GetClassInfoInstance() is called, passing in an aID, mCachedClassInfo for that class will still be null. The body of the "if" clause is thus executed. We initialize "data" to be a reference to the nsDOMClassInfoData object that corresponds to the DOM class we want to "help". On the next line, there is a call to data.mConstructorFptr(aID), which, if you remember the introduction to Class Info, maps to the doCreate static member function of the relevant helper class. doCreate creates a new instance of the helper class, and returns a pointer to the nsIClassInfo interface, which is then assigned into mCachedClassInfo. mCachedClassInfo is AddRef'ed to keep it from being destroyed without our permission. Finally it is returned.

On subsequent calls to this function with the same aID passed in, mCachedClassInfo will still be there, and thus the creation of a new helper class will not be necessary.

"Where is GetClassInfoInstance used and why should I use it", would be an excellent question for now. The short answer is, "to implement the QueryInterface for nsIClassInfo". Indeed, a QueryInterface to nsIClassInfo cannot be implemented the same way as other interfaces. If you don't like macros, you can see the full QueryInterface implementation in nsXMLElement.cpp. Two other GetClassInfoInstance member functions are defined in Mozilla, as member of class nsContentUtils and class nsDOMSOFactory. Both of these methods end up calling nsDOMClassInfo::GetClassInfoInstance so there is no real point in documenting them further. GetClassInfoInstance is used in the NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO macro, which is used to implement QueryInterface for the nsIClassInfo interface in most of the DOM classes, and in the NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO macro, which is used to implement QueryInterface for the nsIClassInfo interface in most global object properties.

I think that's all there is to say about this function. If you think of something else don't hesitate to contact me, as usual.

nsWindowSH::GlobalResolve()

This Section will describe in detail the absolutely horrific GlobalResolve() member function of the nsWindowSH helper, as an example of those functions. This is not for the faint of heart, and is not absolutely necessary, so you might want to skip this Section if you don't have too much time (and I suppose you don't).

User's guide to Class Info

Warning: this document has not yet been reviewed by the DOM gurus, it might contain some errors. Specifically, due to some changes that happened around, April 2002, some things that were not possible before are now possible. I will try to update this guide as soon as possible. Please send any comment to Fabian Guisset.

When should DOMClassInfo be used

  • To add a new interface to an existing DOM object
  • To expose a new DOM object to JavaScript
  • To add a new JS external constructor, like "new Image()"
  • To bypass the default behavior of XPConnect
  • To implement a "replaceable" property
  • To mess with the prototypes of DOM objects

Example of functionality implemented using DOMClassInfo:

  • Constructors of DOM objects in the global scope (e.g. Node)
  • Setting up custom prototypes for those DOM objects
  • new Image(), new Option()
  • window.history[index]
  • document.<formName>

How to add a new interface to an existing DOM object

For this Section, we will use the simple example of the DOMImplementation DOM object. This is a real-world case that was used to solve bug 33871 (the patch is not checked in yet, as of writing this document). The problem is the following: We have to add a new HTMLDOMImplementation interface to the DOMImplementation DOM object. The DOMImplementation object is used when one does, in JS, document.implementation. This object already implements the DOMImplementation interface, but DOM2 HTML says it should also implement the HTMLDOMImplementation interface, so here we go. The C++ implementation is in nsDocument.cpp. The first step is of course to do the C++ implementation of the interface, which is described in the intro to XPCOM document.

Let's assume nsDOMImplementation now implements the nsIDOMHTMLDOMImplementation interface (look in bug 33871 if you want to know how to do that). We want to expose this interface to JavaScript (otherwise only XPCOM callers will be able to access this interface). To do that, we have to add it to the DOMClassInfo of the DOMImplementation DOM object.

Benefits
  • The HTMLDOMImplementation interface will be available from JavaScript.
  • Methods defined on the HTMLDOMImplementation interface will be accessible on the document.implementation object (the main goal) using the automatic interface flattening brought to you by nsDOMClassInfo and XPConnect.
  • document.implementation instanceof HTMLDOMImplementation will work (returns true)
  • HTMLDOMImplementation.prototype will be accessible and modifyable
  • Lots of other stuff you probably don't care about
What there is to do
  1. Include the new interface definition in nsDOMClassInfo.cpp:
    #include "nsIDOMHTMLDOMImplementation.h".
    Put it where you think it fits the most.
  2. Find the code where all the interfaces implemented by the relevant DOM object are implemented. This is in the nsDOMClassInfo::Init() method.
  3. For DOMImplementation, this is around line 1220 (at the time of writing this document):
    1224 DOM_CLASSINFO_MAP_BEGIN(DOMImplementation, nsIDOMDOMImplementation)
    The next line specifies that the DOMImplementation object implements the nsIDOMDOMImplementation interface.
  4. Add the new interface to the DOMClassInfo definition. For us, it is:
    1225 DOM_CLASSINFO_MAP_ENTRY(nsIDOMHTMLDOMImplementation)
  5. Add the new interface to the makefiles, manifests, etc.
  6. Recompile.
  7. Nuke components.reg if you build optimized.
  8. Wonder at the beauty of DOMClassInfo.

How to expose a new DOM object to JavaScript

Let's now go a step further. Not only do we want to add a new interface to an object, but we also want to expose a completely new object to JavaScript. DOMClassInfo does almost everything for you, from prototypes to implementing a default ToString() method on your object.

We will again take the example of the DOMImplementation object. It is accessible using document.implementation. It is defined in the W3C DOM Level 1 Core spec. The requirements include that the global constructor DOMImplementation be accessible, that the ToString() method called on an instance of a DOMImplementation return "DOMImplementation", and that it implements the following methods: hasFeature() (DOM1), createDocumentType() and createDocument() (DOM2).

What there is to do
  1. Implement your object in C++. This is not in the scope of this document. The best thing you can do is probably to copy existing code. A DOM object is a simple XPCOM object with DOMClassInfo. In our example, the implementation class is nsDOMImplementation (in nsDocument.cpp). It implements the nsIDOMDOMImplementation interface (which contains the three methods mentionned above).
  2. Modify the QueryInterface implementation of your XPCOM object to include DOMClassInfo data. Add the following line at the end of the QueryInterface implementation:
    NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO(dom_object_name)
    For the DOMImplementation object, the line would be:
    NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO(DOMImplementation)
    What does it do? It's the QueryInterface implementation for the nsIClassInfo interface, which is requested internally by XPConnect. Basically it will create an instance of the scriptable helper class for this DOM object. More on this subject in the rest of this document.
  3. Add the DOM object DOMClassInfo in the sClassInfoData array (nsDOMClassInfo.cpp):
    NS_DEFINE_CLASSINFO_DATA(dom_object_name, scriptable_helper_class,
    scriptable_flags)
    

    For the DOMImplementation object, the lines would be:

    NS_DEFINE_CLASSINFO_DATA(DOMImplementation, nsDOMGenericSH,
    DOM_DEFAULT_SCRIPTABLE_FLAGS)
    
    The place where you have to add the DOMClassInfo in that array should be obvious. If it is not, ask Johnny Stenback.
  4. Add the DOM object DOMClassInfo in the nsDOMClassInfo::Init() method (nsDOMClassInfo.cpp):
    DOM_CLASSINFO_MAP_BEGIN(dom_object_name, dom_object_main_interface)
    DOM_CLASSINFO_MAP_ENTRY(interface1)
    DOM_CLASSINFO_MAP_ENTRY(interface2)
    ...
    DOM_CLASSINFO_MAP_END
    

    For the DOMImplementation object, the lines would be:

    DOM_CLASSINFO_MAP_BEGIN(DOMImplementation, nsIDOMDOMImplementation)
    DOM_CLASSINFO_MAP_ENTRY(nsIDOMDOMImplementation)
    DOM_CLASSINFO_MAP_END
    
    The interface1, interface2, ... arguments are the name of the interfaces implemented by the DOM object AND exposed to JavaScript. The internal interfaces should NOT be a part of this list.
  5. #include the relevant files to make it build, tweak the makefiles, etc. Make sure it builds on all platforms! :-P
  6. If you used an already existing scriptable helper class, then all you need to do is build, nuke components.reg (if you build optimized) and run. Everything should work well.
  7. If you want to use a new scriptable helper class, you will have to implement it as well.

How to override the default behavior of XPConnect on DOM objects

XPConnect implements default behaviors for XPCOM objects in general, and for DOM objects in particular. DOMClassInfo allows the implementor to override this default behavior using the nsIXPCScriptable interface. Before we begin, please take a look at the nsIXPCScriptable.idl file. It defines a set of constants, called the "scriptable flags", and a set of functions, like NewResolve(), SetProperty(), ... Each flag corresponds to one function. For example, nsIXPCScriptable::WANT_NEWRESOLVE means that we want to implement the NewResolve() function. The important thing to grasp is that each function corresponds to an event in the life of the DOM object. For example, the SetProperty() function is called automatically by XPConnect when, in JS, the client tries to set a property on this DOM object. This is how we can override the default "set this property on this object" XPConnect behavior. For more information about each nsIXPCScriptable function, please see the nsIXPCScriptable documentation.

To illustrate the use of nsIXPCScriptable and scriptable helper functions, we will take the example of the "location" property of the window object. window.location is a DOM object of type "Location". However a common technique is to do window.location = "http://mozilla.org" instead of the correct window.location.href = "http://mozilla.org". So, we have to override the default behavior of "setting the location property on the window object". The default behavior would be that XPConnect expects a nsIDOMLocation object. However it would be passed a JS string. A bad conversion exception would be thrown.

Before we start looking at the implementation of the nsIXPCScriptable interface, the implementor needs the following information:

  • Which DOM object is concerned
  • What action does he want to override
  • What should happen

For our example, it is the window object. The action is setting a property. What should happen is that setting .location should set .location.href. With that information in hand, we can start coding.

What there is to do
  1. Locate the DOM object ClassInfo data in the sClassInfoData array. In our example, it is the Window object. The three parameters passed to the macro, as described in the previous Section, are the DOM object name, the scriptable helper class, and the scriptable flags.
  2. The scriptable flags tell you which nsIXPCScriptable interfaces are implemented by this DOM object. If the flag you need is already there, then go on to the next step. Else, add it to the flag list.
  3. Remember the name of the scriptable helper class for this object. For most objects, it is the nsDOMGenericSH class, which is just a typedef for the nsDOMClassInfo class. If your DOM object does not require any special-casing, then the scriptable helper for your object should be nsDOMGenericSH. If you need special-casing, scroll to the implementation of the helper class.
    In our example, it's the nsWindowSH class.
  4. If the helper class already implements the nsIXPCScriptable function you need, go on to the next step. Else, implement this new method, using the arguments described in the nsIXPCScriptable interface.
  5. Now comes the interesting part. It is unfortunately impossible to describe all the uses of the scriptable helpers, you will have to use your coding skills and/or copy existing code. We will however describe the implementation of our example, the window.location property.
The window.location implementation

Overriding the setter of a property requires two scriptable flags: WANT_NEWRESOLVE and WANT_SETPROPERTY. NewResolve() will define the property on the object using the JS API, the second one will map .location to .location.href. As of writing this document, the code in nsWindowSH::NewResolve() looks like this: (nsDOMClassInfo.cpp)

3553     if (flags & JSRESOLVE_ASSIGNING) {
// Only define the property if we are setting it.
3554       if (str == sLocation_id) {
// Setting the location property.
3555         nsCOMPtr<nsIDOMWindow>
window(do_QueryInterface(native));
3556         NS_ENSURE_TRUE(window, NS_ERROR_UNEXPECTED);
3557 
3558         nsCOMPtr<nsIDOMLocation> location;
3559         rv = window->GetLocation(getter_AddRefs(location));
3560         NS_ENSURE_SUCCESS(rv, rv);
// Use the DOM to get the Location object of the window object.
3561 
3562         jsval v;
3563 
3564         rv = WrapNative(cx, obj, location, NS_GET_IID(nsIDOMLocation),
&v);
// This XPConnect method creates a wrapper for the Location object on the //
Window object.
3565         NS_ENSURE_SUCCESS(rv, rv);
3566 
3567         if (!::JS_DefineUCProperty(cx, obj, ::JS_GetStringChars(str),
3568                                    ::JS_GetStringLength(str), v, nsnull,
3569                                    nsnull, 0)) {
3570           return NS_ERROR_FAILURE;
3571         }
// This JS API call defines the "location" property on the window object, its
// value being the XPConnect wrapper for the Location object.
3572
3573         *objp = obj;
3574 
3575         return NS_OK;
3576       }

This is the first step. It is required to have the getter for .location work as well, but that's another story. The second step is to map .location to .location.href in nsWindowSH::SetProperty()

2894     if (str == sLocation_id) {
// Setting the location property
2895       JSString *val = ::JS_ValueToString(cx, *vp);
2896       NS_ENSURE_TRUE(val, NS_ERROR_UNEXPECTED);
// Convert the value assigned to location (i.e. the url) to a JSString.
2897 
2898       nsCOMPtr<nsISupports> native;
2899       wrapper->GetNative(getter_AddRefs(native));
// Get the pointer to the content object that was wrapped.
2900 
2901       nsCOMPtr<nsIDOMWindow>
window(do_QueryInterface(native));
2902       NS_ENSURE_TRUE(window, NS_ERROR_UNEXPECTED);
// QueryInterface to have a nsIDOMWindow pointer to call
// GetLocation() on it.
2903 
2904       nsCOMPtr<nsIDOMLocation> location;
2905       nsresult rv = window->GetLocation(getter_AddRefs(location));
2906       NS_ENSURE_SUCCESS(rv, rv);
// Get the Location object for this window.
2907 
2908       nsDependentString href(NS_REINTERPRET_CAST(PRUnichar *,
2909                                                  ::JS_GetStringChars(val)),
2910                              ::JS_GetStringLength(val));
// Convert the JSString to a string that can be passed to SetHref()
2911 
2912       rv = location->SetHref(href);
2913       NS_ENSURE_SUCCESS(rv, rv);
// After this, we effectively mapped .location to .location.href
2914 
2915       return WrapNative(cx, obj, location, NS_GET_IID(nsIDOMLocation), vp);
// Create a wrapper for the location object with vp (the url) as value.
2916     }

It's that simple. And the possibilities are endless.

Resources of interest

Scriptable Helper flags

This chapter has not been written yet. If you want to help please contact me!

Security features implementation

This chapter has not been written yet. If you want to help please contact me!

Original Document Information

  • Author(s): Fabian Guisset
  • Last Updated Date: September 27, 2007
  • Copyright Information: Portions of this content are © 1998–2007 by individual mozilla.org contributors; content available under a Creative Commons license | Details.

Document Tags and Contributors

Contributors to this page: Sheppy, jimblandy, kscarfone, Kohei, Yoshino, Mgjbot
Last updated by: kscarfone,