XPCOM Part 2: XPCOM component basics Back to top

http://www.ibm.com/developerworks/webservices/library/co-xpcom2.html



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.



Back to top

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);
NSIMETHOD(nsrefcnt) AddRef(void);
NSIMETHOD(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)
NSIMETHODIMP(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)
NSIMETHODIMP(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.