Level: Introductory Rick Parrish (rfmobile@swbell.net), Independent consultant, Freelance
01 Mar 2001 In the last article in this series we took an overview look at XPCOM technology. This time, we'll delve into type libraries, the xpidl compiler, and interface discovery. 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:
@<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;1 |
-
@mozilla.org/file/local;1 |
-
-
@mozilla.org/filelocator;1 |
-
@mozilla.org/filepicker;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.
Conclusion
The material so far should serve as an aid when browsing through the
mozilla source code. We've zoomed in and taken a microscopic view of how a
component dispenses interface pointers and how it performs lifetime
management. Looking ahead, we'll discuss the higher-level facilities
available for managing components. Our next step is to take a tour of the
development tools and other requirements to actually build the mozilla
browser and the entire XPCOM framework underneath it. The main benefit will be the creation of an XPCOM development environment.
Resources -
XPIDL, a tool
for generating XPCOM interface information, based on XPIDL interface
description files
- This is Part 2 of our continuing five-part series on XPCOM. Other parts are:
- These files include macros for testing, debugging and
implementation:
- Netscape's JavaScript is a superset
of the ECMA-262 Revision 3 (ECMAScript) standard scripting language
-
Standard ECMA-262 ECMAScript Language Specification
- The home of the Python programming language
About the author  | |  | Rick Parrish has held an interest in computers since high school and in electronics even longer. He originally pursued an education in electrical engineering but discovered that software, unlike hardware, did not require smelly vats of ferric chloride or run the risk of burnt fingers just to perform a design change. Rick has been programming in C/C++ for A LONG TIME(tm) but has also done heaps of work in VB, Delphi(Pascal), and a handful of assembly languages. He still manages to squeeze in a project or two that requires hot solder. His current opinions are that while Windows 2000 is neat-o, Ogg Vorbis is cool, and the Linux 2.4 kernel with IPTables definitely rocks! He is
currently interested in starting an open source project to develop tools for software modeling design. Rick can be reached at rfmobile@swbell.net.
|
Rate this page
|