XPCOM component basics

The idea behind XPCOM as a technology is to provide a modular framework that is platform- and language-neutral. There is very little that a component written in C++ can do that can’t be done in JavaScript or some other scripting language. The mechanism used in XPCOM to accomplish this feat is a type library.

Type library

A type library provides a common data format or interchange mechanism for describing the methods, attributes, parameters, and interfaces of a component. By establishing a common format, the same interfaces can be described across multiple platforms and multiple programming languages. This is useful for supporting a generic marshalling or proxy mechanism. Using the type info, a program can determine every parameter of any given method or attribute on some interface. With this knowledge, it can move data back and forth between the interface and some other environment. That other environment can be a scripting engine or a proxy mechanism for crossing thread, process, or network boundaries. In the case of a scripting engine, this is how a component gets defined in the scripting environment so that scripted code can invoke methods on a component’s interface.
XPConnect is an additional layer built on top of XPCOM that can marshal an XPCOM interface into the JavaScript engine by reading an XPCOM type library file. XPConnect also allows XPCOM components to be written entirely in JavaScript so you can have C++ code call a JS component, or use JS to load and manipulate a compiled C++ component. In addition to JavaScript, the Python language has been added as another scripting alternative using a mechanism similar to XPConnect.

Interface description

The language-neutral way to specify an interface is to use an IDL or interface description language. The tool to create a type library file from an interface description is an IDL compiler. The IDL dialect used in XPCOM is slightly different from those used in OMG CORBA or Microsoft IDL, so a different IDL compiler — the xpidl compiler –– is used. An interesting feature of the xpidl compiler is the option to generate C++ code stubs from an interface definition. This feature has the effect of writing nearly all of the declaratory C++ code when starting a new project. It’s like a coding wizard to help you get started. CORBA and Microsoft IDL compilers offer similar features. Here is a synopsis of running xpidl from a shell prompt.
Listing 1. xpidl from a shell prompt

Usage: xpidl [-m mode] [-w] [-v] [-I path] [-o basename] filename.idl
-w turn on warnings (recommended)
-v verbose mode (NYI)
-I add entry to start of include path for “#include “nsIThing.idl””
-o use basename (e.g. “/tmp/nsIThing”) for output
-m specify output mode:
header        Generate C++ header            (.h)
typelib       Generate XPConnect typelib     (.xpt)
doc           Generate HTML documentation    (.html)

While generating C++ code is a bonus, the real purpose of an IDL compiler is to produce a type library file for each module. C++ code can take advantage of the IDL-generated C++ header files to describe interfaces as virtual methods on a C++ class. Since the interface description (in the form of a C++ header file) is used at compile time, it is referred to as early binding. The type library file offers the same functionality for instances where some piece of code wishes to use a component never seen before, hence no header files are available. It can learn the interfaces after the fact. This is referred to as late binding.
The XPIDL syntax for specifying an interface is; the interface keyword followed by the interface name, a colon, the name of a base interface (usually nsISupports), an open curly brace, a list of attributes and methods that each end in a semicolon, and a closing curly brace and semicolon. Attributes are declared using the attribute keyword. The parameters to methods can be declared as input or output parameters by prefixing them with the in or out keywords. Listing 2 shows a sample interface used to describe the computer’s screen.
Listing 2. Sample interface

#include “nsISupports.idl”
[scriptable, uuid(f728830e-1dd1-11b2-9598-fb9f414f2465)]

interface nsIScreen  : nsISupports
{
void GetRect(out long left, out long top, out long width, out long height);
void GetAvailRect(out long left, out long top, out long width, out long height);
readonly attribute long pixelDepth;
readonly attribute long colorDepth;
};

Examining the above interface description we can see that the interface is named nsIScreen, and that it has two methods (GetRect and GetAvailRect), and two attributes (pixelDepth and colorDepth). Just ahead of the interface keyword is a clause bound inside a pair of square brackets. This clause is an optional part of the interface description and supplies some useful metadata. The scriptable keyword tags the interface as a candidate for marshalling in JavaScript and other scripting languages. The uuid keyword specifies the interface’s UUID or interface ID. nsIScreen’s base interface is nsISupports (appears just after the colon) which means that whatever methods and attributes described in nsISupports are also found in nsIScreen (more on what these are later). Attributes are distinguished from methods by the attribute keyword. In this case, both attributes can be examined but not set; the clue being the readonly keyword. (See the Resources section for a link to a detailed description of xpidl syntax.)

Interface discovery

XPCOM uses an interface-based approach to handling components. Client code is forced to interact with a component strictly through the interfaces provided by that component. Most components support two or more interfaces so the interface dispensing mechanism (that’s the QueryInterface method — more on this in a moment) has to provide some facilities for managing interfaces, particularly:
·    Determining what interfaces are supported by a component
·    Switching from one interface to another (and back again)
I’ll group these two items together and call them interface discovery. A core requirement for an XPCOM component is that it support a standard interface to handle interface discovery, and that this standard interface must be the base interface from which any other XPCOM interface extends to provide additional methods and functionality. That standard interface is named nsISupports and appears in simplified IDL in Listing 3.
Listing 3. Standard interface, nslSupports

interface nsISupports
{
void QueryInterface(in nsIIDRef uuid, out nsQIResult result);
nsrefcnt AddRef();
nsrefcnt Release();
};

The first method, QueryInterface, is the one that actually takes care of interface discovery. The other two methods, AddRef and Release, provide for lifetime management of a component (how long a component should exist) through reference counting.
The first parameter to QueryInterface is a reference to a UUID or universally unique ID number that is 128 bits long (16 bytes). As an example, here is the interface ID for nsISupports: 00000000-0000-0000-c000-000000000046.
UUIDs are commonly written using hexadecimal digits in a hyphenated form. This ID number specifies an interface that may or may not be supported by the component being queried. The component may either return an error result code or set the second parameter to the address of the requested interface and return a success result code. XPCOM software designers are expected to exercise care when creating new interfaces to make sure that any new interfaces are assigned unique interface IDs.
Listing 4 contains a JavaScript sample that uses QueryInterface to switch among different interfaces on the same instance of some component.
Listing 4. Switching interfaces

// first, we create an instance of something…
var file = components.classes[“@mozilla.org/file/local;1”].createInstance();
// second, we specify which interface we actually want to use.
file = file.QueryInterface(Components.interfaces.nsIFile);
// do something generic with the nsIFile interface here.
file.create(NORMAL_FILE_TYPE, 0377);
var size = file.fileSize;
// later on, we check to see if an extended interface is supported.
var local = file.QueryInterface(Components.interfaces.nsILocalFile);
if (local)
{
// do something specific to the nsILocalFile interface…
local.initWithPath(‘/usr/tmp/scratch.txt’);

// suppose we’re now in some scope where the file variable is no longer
// visible to use but we want to call some function that absolutely
// insists on only accepting an nsIFile and not an nsILocalFile.
// no problem, just QI over to the other interface like so …

var insists = local.QueryInterface(Components.interfaces.nsIFile);
if (insists)
{
// at this point we can call our hypothetical function
// to do some generic file processing…
hypothetical(insists);
}
}

nsISupports

Component creation

Did you notice that funny looking string in the JavaScript example above – the one that reads “@mozilla.org/file/local;1”? That’s called a contract ID. Explicitly creating a component requires one of two forms of identification as a means of specifying to the component manager which component to create. One form is the component’s class ID which is just a 128 bit number. The other form is a contract ID that is really just a text string. Either is sufficient for requesting a component from the component manager. The intent of a contract ID is to promise a set of behavior and related interfaces to clients wishing to use the component. The recommended format of a contract ID is a one-line string as follows <!– /* Font Definitions */ @font-face {font-family:Wingdings; panose-1:5 0 0 0 0 0 0 0 0 0; mso-font-charset:2; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:0 268435456 0 0 -2147483648 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:””; margin:0in; margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:12.0pt; font-family:”Times New Roman”; mso-fareast-font-family:”Times New Roman”;} p {mso-margin-top-alt:auto; margin-right:0in; mso-margin-bottom-alt:auto; margin-left:0in; mso-pagination:widow-orphan; font-size:12.0pt; font-family:”Times New Roman”; mso-fareast-font-family:”Times New Roman”;} pre {margin:0in; margin-bottom:.0001pt; mso-pagination:widow-orphan; tab-stops:45.8pt 91.6pt 137.4pt 183.2pt 229.0pt 274.8pt 320.6pt 366.4pt 412.2pt 458.0pt 503.8pt 549.6pt 595.4pt 641.2pt 687.0pt 732.8pt; font-size:10.0pt; font-family:”Courier New”; mso-fareast-font-family:”Times New Roman”;} @page Section1 {size:8.5in 11.0in; margin:1.0in 1.25in 1.0in 1.25in; mso-header-margin:.5in; mso-footer-margin:.5in; mso-paper-source:0;} div.Section1 {page:Section1;} /* List Definitions */ @list l0 {mso-list-id:824278441; mso-list-template-ids:429948326;} @list l0:level1 {mso-level-number-format:bullet; mso-level-text:; mso-level-tab-stop:.5in; mso-level-number-position:left; text-indent:-.25in; mso-ansi-font-size:10.0pt; font-family:Symbol;} ol {margin-bottom:0in;} ul {margin-bottom:0in;} –>
/* Style Definitions */
table.MsoNormalTable
{mso-style-name:”Table Normal”;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-parent:””;
mso-padding-alt:0in 5.4pt 0in 5.4pt;
mso-para-margin:0in;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.0pt;
font-family:”Times New Roman”;
mso-ansi-language:#0400;
mso-fareast-language:#0400;
mso-bidi-language:#0400;}

@<internetdomain>/module[/submodule[...]];<version>[?<name>=<value>[&<name>=<value>[...]]]

The square brackets above [like this] imply something optional. Here are some examples:

    @mozilla.org/file/directory_service;
      @mozilla.org/file/local;1
        @mozilla.org/file;1
          @mozilla.org/filelocator;1
            @mozilla.org/filepicker;1
              @mozilla.org/filespec;1

              Each example includes a version number of one. A subsequent contract ID with a version of two does not necessarily imply backward compatibility. A contract ID with a different version number may include other contracts’ promised behaviors and interfaces as part of its own promised behaviors and interfaces.

              Lifetime management

              Components must keep count of how many outstanding interfaces have been issued. It would be a very bad thing for a component to be destroyed while some other piece of code is attempting to make use of one of its interfaces. When an object dispenses another copy of an interface, it increments its internal reference count. When an object’s interface is released, its reference count decrements. When an object’s reference count drops to zero, it destroys itself. That’s reference counting in a nutshell.
              Generally, the QueryInterface method performs an implicit AddRef on the component being queried when returning a valid interface pointer. When a piece of client code is done using the interface, it calls the Release method to indicate to the component that it is done with that interface. This is an important burden on all XPCOM client software: for every QueryInterface or AddRef on a component there must also be a Release. A large portion of XPCOM bugs can be traced to either a missing or an extra Release on a component.

              Macros and smart pointers

              To combat this type of error, XPCOM includes some C++ templates that allow you to declare a smart interface pointer. The templates give you a “set and forget” pointer. Set the pointer to an interface and it will remember to release the interface for you. Set the pointer to another interface and it will release the previous one. Sounds simple enough. You declare your smart pointer within the scope needed and assign it to an interface. You use the smart pointer as you would a plain vanilla interface pointer. When the smart pointer loses scope, its built-in destructor will call the Release method for you.
              Taking a look at just about any Mozilla code that uses nsCOMPtr or nsIPtr you will see something like the code in Listing 5.
              Listing 5. Mozilla code using nsCOMPtr or nslPtr

              nsresult nsExample::DoSomething(void)
              {
              nsresult rv;
              nsCOMPtr<nsIManager> pManager;
              *aResult = nsnull;
              pManager = do_GetService(“Some contract ID goes here”);
              if (pManager == nsnull)
              return NS_ERROR_NOT_AVAILABLE;
              rv = pManager->ManageSomething(); // do some more work here …
              return rv;
              }

              In Listing 5, a smart pointer to the fictional nsIManager interface is declared with the name pManager (see the line that starts with “nsCOMPtr ..”). The smart pointer is assigned to some service. After testing that a valid pointer was indeed returned, the code above dereferences the pointer to call the ManageSomething() method. When the above function returns, the pManager smart pointer will be destroyed — but not before calling Release on the interface pointer held inside.
              XPCOM expedites a lot of the declaratory grunt work demanded by C++ through the use of a family of C macros. Most interfaces return a nsresult. In most cases, the magic value to check for in an nsresult is NS_OK. (For an exhaustive list of nsresult values take a look at nsError.h.)
              The XPCOM include files nsCom.h, nsDebug.h, nsError.h, nsIServiceManager.h and nsISupportsUtils.h provide some additional macros for testing, debugging and implementation.
              When you browse the header files of various C++ XPCOM components you’ll see NS_DECL_ISUPPORTS as part of the class definition. This macro provides the definitions for the nsISupports interface.
              Listing 6. Definitions for nsISupports

              public:
              NS_IMETHOD QueryInterface(REFNSIID aIID void** aInstancePtr);
              NS_IMETHOD_(nsrefcnt) AddRef(void);
              NS_IMETHOD_(nsrefcnt) Release(void);
              nsrefcnt mRefCnt;

              When you browse a component’s corresponding implementation file, you’ll see another mysterious one-line macro named NS_IMPL_ISUPPORTS1 (or similar). This macro provides the actual implementation of the nsISupports interface. The digit “1” at the end of the macro denotes the number of interfaces (besides nsISupports) that the component implements. If a class implemented two interfaces it could use NS_IMPL_ISUPPORTS2.
              Remember the mRefCnt data member above — we’ll be making reference to it again shortly.
              Here’s how the NS_IMPL_ISUPPORTS1 macro is defined in nsISupportsUtils.h:
              Listing 7. NS_IMPL_ISUPPORTS1

              #define NS_IMPL_ISUPPORTS1(_class, _interface)
              NS_IMPL_ADDREF(_class)
              NS_IMPL_RELEASE(_class)
              NS_IMPL_QUERY_INTERFACE1(_class, _interface)

              As you can see, it’s just defined in terms of three other macros. Digging further, we start with the definition for NS_IMPL_ADDREF:
              Listing 8. NS_IMPL_ADDREF

              #define NS_IMPL_ADDREF(_class)
              NS_IMETHODIMP_(nsrefcnt) _class::AddRef(void)
              {
              NS_PRECONDITION(PRInt32(mRefCnt) >= 0, “illegal refcnt”);
              NS_ASSERT_OWNINGTHREAD(_class);
              ++mRefCnt;
              NS_LOG_ADDREF(this, mRefCnt, #_class, sizeof(*this));
              return mRefCnt;
              }

              Finally some real code to look at! Out of five lines of code, three of them are debugging macros that we can safely ignore. The last line of code is a return statement. Any code calling AddRef is supposed to discard the value returned, so we can ignore the return statement.
              The one line of code of interest to us is the ++mRefCnt statement. All it does is increment a counter so every time we call AddRef on some interface, all we are doing (in all likelihood) is causing that component to increment some internal counter. Next, let’s peek at the NS_IMPL_RELEASE macro:
              Listing 9. NS_IMPL_RELEASE macro

              #define NS_IMPL_RELEASE(_class)
              NS_IMETHODIMP_(nsrefcnt) _class::Release(void)
              {
              NS_PRECONDITION(0 != mRefCnt, “dup release”);
              NS_ASSERT_OWNINGTHREAD(_class);
              –mRefCnt;
              NS_LOG_RELEASE(this, mRefCnt, #_class);
              if (mRefCnt == 0) {
              mRefCnt = 1; /* stabilize */
              NS_DELETEXPCOM(this);
              return 0;
              }
              return mRefCnt;
              }

              Again, we’ve got three statements involving debugging macros that we can safely ignore, along with two return statements that we can ignore for reasons explained above.
              The two statements we care about are –mRefCnt, which decrements the object’s counter and if (mRefCnt == 0), which tests to see if the counter has reached a value of zero. The next couple of lines tell us that the object will delete itself when this internal counter reaches zero.
              In summary, AddRef increments the counter, Release decrements the counter — and when the number of calls to AddRef equal the number of calls to Release, the net reference count becomes zero and the component destroys itself. This whole reference-counting idea is starting to look fairly straightforward. Next we’ve got NS_IMPL_QUERY_INTERFACE1 defined in Listing 10.
              Listing 10. NS_IMPL_QUERY_INTERFACE1

              #define NS_IMPL_QUERY_INTERFACE1(_class, _i1)
              NS_INTERFACE_MAP_BEGIN(_class)
              NS_INTERFACE_MAP_ENTRY(_i1)
              NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, _i1)
              NS_INTERFACE_MAP_END

              Drat! More macros to look up. By now, all of the MFC coders are chuckling because they’ve seen this kind of macro indirection nonsense before. These macros are building an interface map for the component so their order and position is important to creating a QueryInterface implementation. Undaunted by this indirection, we plow ahead and look at NS_INTERFACE_MAP_BEGIN. It turns out that it’s just an alias for NS_IMPL_QUERY_HEAD which expands into the code in Listing 11.
              Listing 11. NS_INTERFACE_MAP_BEGIN

              #define NS_IMPL_QUERY_HEAD(_class)                                       \
              NS_IMETHODIMP _class::QueryInterface(REFNSIID aIID, void** aInstancePtr) \
              {                                                                        \
              NS_ASSERTION(aInstancePtr, “QueryInterface requires a non-NULL destination!”); \
              if ( !aInstancePtr )                                                   \
              return NS_ERROR_NULL_POINTER;                                        \
              nsISupports* foundInterface;

              The code in this macro doesn’t really do any work. It just provides the function’s declaratory preamble and some lightweight error checking in the form of a test for a null return pointer. There isn’t even a closing curly brace to complete the function so it’s logical to suspect this macro is intended to be followed by other macros that fill in rest of the code. This next macro does some of that work. NS_INTERFACE_MAP_ENTRY is just an alias for NS_IMPL_QUERY_BODY.
              Listing 12. NS_IMPL_QUERY_BODY

              #define NS_IMPL_QUERY_BODY(_interface)
              if ( aIID.Equals(NS_GET_IID(_interface)) )
              foundInterface = NS_STATIC_CAST(_interface*, this);
              else

              This is the critical snippet of code that does the matching for our interface map. Because of the way the if/else statements are structured, we can stack multiple NS_IMPL_QUERY_BODY macros in succession to build an interface map that will answer to any number of interface IDs. The next macro, NS_INTERFACE_MAP_ENTRY_AMBIGUOUS, is just an alias for NS_IMPL_QUERY_BODY_AMBIGUOUS.
              Listing 13. NS_IMPL_QUERY_BODY_AMBIGUOUS

              #define NS_IMPL_QUERY_BODY_AMBIGUOUS(_interface, _implClass)             \
              if ( aIID.Equals(NS_GET_IID(_interface)) )                             \
              foundInterface = NS_STATIC_CAST(_interface*, NS_STATIC_CAST(_implClass*, this)); \
              else

              NS_IMPL_QUERY_BODY_AMBIGUOUS looks like it is doing the same work as NS_IMPL_QUERY_BODY — which it is. The only extra work being done here is avoiding a compiler error when trying to return an interface pointer for nsISupports when there are two or more supported interfaces that derive from nsISupports. Any one of them is also a valid nsISupports interface pointer — so the dilemma for the C++ compiler is to choose which one. One requirement placed on an XPCOM interface-dispensing mechanism is that it always return the same pointer for the same interface ID — so this macro also helps to comply with this rule by specifying which nsISupports-derived interface pointer gets to be used as the nsISupports interface pointer. As Listing 14 illustrates, NS_INTERFACE_MAP_END is just an alias for NS_IMPL_QUERY_TAIL_GUTS.
              Listing 14. NS_IMPL_QUERY_TAIL_GUTS

              #define NS_IMPL_QUERY_TAIL_GUTS
              foundInterface = 0;
              nsresult status;
              if ( !foundInterface )
              status = NS_NOINTERFACE;
              else
              {
              NS_ADDREF(foundInterface);
              status = NS_OK;
              }
              *aInstancePtr = foundInterface;
              return status;
              }

              At long last, we get to the end of the implementation of QueryInterface. This last snippet of code returns an error code of NS_NOINTERFACE if the caller’s interface ID does not match any of the IDs in its map. If the caller’s interface ID matches one of the object’s supported interfaces, the code calls the object’s AddRef method and returns a pointer to the interface along with a result code of NS_OK.
              The code we’ve just gone through is a stock implementation of nsISupports for a component with a single interface. The stock implementations for supporting multiple interfaces are similar. Actual components may be written using one of the stock implementations or they may provide their own. In most cases, they will use the macros found in nsISupportsUtils.h. By now you should see why it is so important to be able to dissect C style macros — particularly those with nested definitions — if you want to be able to read and understand the mozilla/XPCOM code base.

              Leave a comment