TOC PREV NEXT INDEX


CHAPTER 1 What Is XPCOM?


This is a book about XPCOM. The book is written in the form of a tutorial about creating XPCOM components, but it covers all major aspects, concepts, and terminology of the XPCOM component model along the way.

This chapter starts with a quick tour of XPCOM-an introduction to the basic concepts and technologies in XPCOM and component development. The brief sections in this chapter introduce the concepts at a very high-level, so that we can discuss and use them with more familiarity in the tutorial itself, which describes the creation of a Mozilla component called WebLock.

The XPCOM Solution

The Cross Platform Component Object Module (XPCOM) is a framework which allows developers to break up monolitic software projects into smaller modularized pieces. These pieces, known as components, are then assembled back together at runtime.

The goal of XPCOM is to allow different pieces of software to be developed and built independently of one another. In order to allow interoperability between components within an application, XPCOM separates the implementation of a component from the interface, which we discuss in the "Interfaces" section. But XPCOM also provides several tools and libraries that enable the loading and manipulation of these components, services that help the developer write modular cross-platform code, and versioning support, so that components can be replaced or upgraded without breaking or having to recreate the application. Using XPCOM, developers create components that can be reused in different applications or that can be replaced to change the functionality of existing applications.

XPCOM not only supports component software development, it also provides much of the functionality that a development platform provides, such as:

We will discuss the above items in detail in the coming chapters, but for now, it can be useful to think of XPCOM as a platform for component development, in which features such as those listed above are provided.

Gecko

Although it is in some ways structurally similar to Microsoft COM, XPCOM is designed to be used principally at the application level. The most important use of XPCOM is within Gecko, an open source, standards compliant, embeddable web browser and toolkit for creating web browsers and other applications.

XPCOM is the means of accessing Gecko library functionality and embedding or extending Gecko. This book focuses on the latter-extending Gecko-but the fundamental ideas in the book will be important to developers embedding Gecko as well.

Gecko is used in many internet applications, mostly browsers. The list includes devices such as the Gateway/AOL Instant AOL device and the Nokia Media Terminal. Gecko is also used in the latest Compuserve client, AOL for Mac OS X, Netscape 7, and of course the Mozilla client. At this time, Gecko is the predominant open source web browser.

Components

XPCOM allows you to build a system in which large software projects can be broken up into smaller pieces. These pieces, known as components, are usually delivered in small, reusable binary libraries (a DLL on Windows, for example, or a DSO on Unix), which can include one or more components. When there are two or more related components together in a binary library, the library is referred to as a module.

Breaking software into different components can help make it less difficult to develop and maintain. Beyond this, modular, component-based programming has some well-known advantages, as Table 1 describes:
TABLE 1. Benefits from Modular Code
Benefit Description
Reuse Modular code can be reused in other applications and other contexts
Updates You can update components without having to recompile the whole application
Performance When code is modularized, modules that are not necessary right away can be "lazy loaded", or not loaded at all, which can improve the performance of your application.
Maintenance Even when you are not updating a component, designing your appication in a modular way can make it easier for you to find and maintain the parts of the application that you are interested in.

Mozilla has over four million lines of code, and no single individual understands the entire codebase. The best way to tackle a project of this size is to divide it into smaller, more managable pieces, use a component programming model, and to organize related sets of components into modules. The network library, for example, consists of components for each of the protocols, HTTP, FTP, and others, which are bundled together and linked into a single library. This library is the networking module, also known as "necko."

But it's not always a good idea to divide things up. There are some things in the world that just go together, and others that shouldn't be apart. For example, one author's son will not eat a peanutbutter sandwich if there isn't jam on it, because in his world, peanut butter and jam form an indelible union. Some software is the same. In areas of code that are tightly-coupled-in classes that are only used internally, for example-the expensive work to divide things may not be worth the effort.

The HTTP component in Gecko doesn't expose private classes it uses as separate components. The "stuff" that's internal to the component stays internal, and isn't exposed to XPCOM. In the haste of early Mozilla development, components were created where they were inappropriate, but there's been an ongoing effort to remove XPCOM from places like this.

Interfaces

It's generally a good idea to break software into components, but how exactly do you do this? The basic idea is to identify the pieces of functionality that are related and understand how they communicate with each other. The communication channels between different component form boundaries between those components, and when those boundaries are formalized they are known as interfaces.

Interfaces aren't a new idea in programming. We've all used interfaces since our first "HelloWorld" program, where the interface was between the code we actually wrote-the application code-and the printing code. The application code used an interface from a library, stdio, to print the "hello world" string out to the screen. The difference here is that a "HelloWorld" application in XPCOM finds this screen-printing functionality at runtime and never has to know about stdio when it's compiled.

Interfaces allow developers to encapsulate the implementation and inner workings of their software, and allow clients to ignore how things are made and just use that software.

Interfaces and Programming by Contract

An interface forms a contractual agreement between components and clients. There is no code that enforces these agreements, but ignoring them can be fatal. In component-based programming, a component guarantees that the interfaces it provides will be immutable-that they will provide the same access to the same methods across different versions of the component-establishing a contract with the software clients that use it. In this respect, interface-based programming is often referred to as programming by contract.


Interfaces and Encapsulation

Between component boundaries, abstraction is crucial for software maintainability and reusability. Consider, for example, a class that isn't well encapsulated. Using a freely available public initialization method, as the example below suggests, can cause problems.

class SomeClass  
{ 
  public: 
    // Constructor 
    SomeClass(); 
 
    // Virtual Destructor 
    virtual ~SomeClass(); 
 
    // init method 
    void Init(); 
 
    void DoSomethingUseful(); 
}; 

Figure 1. SomeClass Class Initialization

For this system to work properly, the client programmer must pay close attention to whatever rules the component programmer has established. This is the contractual agreement of this unencapsulated class: a set of rules that define when each method can be called and what it is expected to do. One rule might specify that DoSomethingUseful may only be called after a call to Init(). The DoSomethingUseful method may do some kind of checking to ensure that the condition-that Init has been called-has been satisfied.

In addition to writing well-commented code that tells the client developer the rules about Init(), the developer can take a couple steps to make this contract even clearer. First, the construction of an object can be encapsulated, and a virtual class provided that defines the DoSomethingUseful method. In this way, construction and initialization can be completely hidden from clients of the class. In this "semi-encapsulated" situation, the only part of the class that is exposed is a well-defined list of callable methods (i.e., the interface). Once the class is encapsulated, the only interface the client will see is this:

class SomeInterface 
{ 
public: 
  virtual void DoSomethingUseful() = 0; 
}; 

Figure 2. Encapsulation of SomeInterface

The implementation can then derive from this class and implement the virtual method. Clients of this code can then use a factory design pattern to create the object (see "Factories" on page 20) and further encapsulate the implementation. In XPCOM, clients are shielded from the inner workings of components in this way and rely on the interface to provide access to the needed functionality.

The nsISupports Base Interface

Two fundamental issues in component and interface-based programming are component lifetime, also called object ownership, and interface querying, or being able to identify which interfaces a component supports at run-time. This section introduces the base interface-the mother of all interfaces in XPCOM-nsISupports, which provides solutions to both of these issues for XPCOM developers.

Object Ownership

In XPCOM, since components may implement any number of different interfaces, interfaces must be reference counted. Components must keep track of how many references to it clients are maintaining and delete themselves when that number reaches zero.

When a component gets created, an integer inside the component tracks this reference count. The reference count is incremented automatically when the client instantiates the component; over the course of the component's life, the reference count goes up and down, always staying above zero. At some point, all clients lose interest in the component, the reference count hits zero, and the component deletes itself.

When clients use interfaces responsibly, this can be a very straightforward process. XPCOM has tools to make it even easier, as we describe later. It can raise some real housekeeping problems when, for example, a client uses an interface and forgets to decrement the reference count. When this happens, interfaces may never be released and will leak memory. The system of reference counting is, like many things in XPCOM, a contract between clients and implementations. It works when people agree to it, but when they don't, things can go wrong. It is the responsibility of the function that creates the interface pointer to add the initial reference, or owning reference, to the count.

Pointers in XPCOM

In XPCOM, pointers refer to interface pointers. The difference is a subtle one, since interface pointers and regular pointers are both just address in memory. But an interface pointer is known to implement the nsISupports base interface, and so can be used to call methods such as AddRef, Release, or QueryInterface.

nsISupports, shown below, supplies the basic functionality for dealing with interface discovery and reference counting. The members of this interface, QueryInterface, AddRef, and Release, provide the basic means for getting the right interface from an object, incrementing the reference count, and releasing objects once they are not being used, respectively. Figure 3 shows the nsISupports interface.

class Sample: public nsISupports {   
private:   
  nsrefcnt mRefCnt;   
public:   
  Sample();   
  virtual ~Sample();   
 
  NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult);   
  NS_IMETHOD_(nsrefcnt) AddRef(void);   
  NS_IMETHOD_(nsrefcnt) Release(void);   
 
}; 

Figure 3. The nsISupports Interface

The various types used in this figure are described in the "XPCOM Types" section below. Figure 4 shows a complete (if spare) implementation of the nsISupports interface.

Sample::Sample()   
{   
  // initialize the reference count to 0 
  mRefCnt = 0;   
}   
Sample::~Sample()   
{   
}   
 
// typical, generic implementation of QI 
NS_IMETHODIMP Sample::QueryInterface(const nsIID &aIID,   
                                  void **aResult)   
{   
  if (aResult == NULL) {   
    return NS_ERROR_NULL_POINTER;   
  }  
  *aResult = NULL;   
  if (aIID.Equals(kISupportsIID)) {   
    *aResult = (void *) this;   
  }  
  if (*aResult != NULL) {   
    return NS_ERROR_NO_INTERFACE;   
  }   
  // add a reference 
  AddRef();   
  return NS_OK;   
}   
 
NS_IMETHODIMP_(nsrefcnt) Sample::AddRef()   
{   
  return ++mRefCnt;   
}   
 
NS_IMETHODIMP_(nsrefcnt) Sample::Release()   
{   
  if (--mRefCnt == 0) {   
    delete this;   
    return 0;   
  }   
  // optional: return the reference count 
  return mRefCnt;   
} 

Figure 4. Implementation of nsISupports Interface
Object Interface Discovery

Inheritance is another very important topic in object oriented programming. Inheritance is the means through which one class is derived from another. When a class inherits from another class, the inheriting class may override the default behaviors of the base class without having to copy all of that class's code, in effect creating a more specific class, as in the following example:

class Shape 
{  
private: 
  int m_x; 
  int m_y; 
 
public: 
  virtual void Draw() = 0; 
  Shape(); 
  virtual ~Shape(); 
}; 
 
 
class Circle : public Shape 
{ 
private: 
   int m_radius; 
public: 
   virtual Draw(); 
   Circle(int x, int y, int radius); 
   virtual ~Circle(); 
}; 

Figure 5. Simple Class Inheritance

Circle is a derived class of Shape. A Circle is a Shape, in other words, but a Shape is not necessarily a Circle. In this case, Shape is the base class and Circle is a subclass of Shape.

In XPCOM, all classes derive from the nsISupports interface, so all objects are nsISupports but they are also other, more specific classes, which you need to be able to find out about at runtime. In Figure 5 above, for example, you'd like to be able ask the Shape if it's a Circle and to be able to use it like a circle if it is. In XPCOM, this is what the QueryInterface feature of the nsISupports interface is for: it allows clients to find and access different interfaces based on their needs.

In C++, you can use a fairly advanced feature known as a dynamic_cast<>, which throws an exception if the Shape object is not able to be cast to a Circle. But enabling exceptions and RTTI may not be an option because of performance overhead and compatibility on many platforms, so XPCOM does things differently.

"Exceptions" in XPCOM

C++ exceptions are not supported directly by XPCOM. Instead all exceptions must be handled within a given component, before crossing interface boundaries. In XPCOM, all interface methods should return an nsresult error value (see the XPCOM API in Appendix B for a listing of these error codes). These error code results become the "exceptions" that XPCOM handles.

Instead of leveraging C++ RTTI, XPCOM uses the special QueryInterface method that casts the object to the right interface if that interface is supported.

Every interface is assigned an identifier that gets generated from a tool commonly named "uuidgen". This universally unique identifier (UUID) is a unique, 128 bit number. Used in the context of an interface (as opposed to a component, which is what the contract ID is for), this number is called an IID.

When a client wants to discover if an object supports a given interface, the client passes the IID assigned to that interface into the QueryInterface method of that object. If the object supports the requested interface, it adds a reference to itself and passes back a pointer to that interface. If the object does not support the interface an error is returned.

class nsISupports { 
 	public:  
		long QueryInterface(const nsIID & uuid, 
					void **result) = 0; 
		long AddRef(void) = 0; 
		long Release(void) = 0; 
}; 

The first parameter of QueryInterface is a reference to a class named nsIID, which is a basic encapsulation of the IID. Of the three methods on the nsIID class, Equals, Parse, and ToString, Equals is by far the most important, because it is used to compare two nsIIDs in this interface querying process.

When you implement the nsIID class (and you'll see in the chapter "Tutorial: Using XPCOM Utilities To Make Things Easier" how macros can make this process much easier), you must make sure the class methods return a valid result when the client calls QueryInterface with the nsISupports IID. QueryInterface should support all interfaces that the component supports.

In implementations of QueryInterface, the IID argument is checked against the nsIID class. If there is a match, the object's this pointer is cast to void, the reference count is incremented, and the interface returned to the caller. If there isn't a match, the class returns an error and sets the out value to null.

In the example above, it's easy enough to use a C-style cast. But casting can become more involved where you must first cast void then to the requested type, because you must return the interface pointer in the vtable corresponding to the requested interface. Casting can become a problem when there is an ambiguous inheritance hierarchy.

XPCOM Identifiers

In addition to the IID interface identifier discussed in the previous section, XPCOM uses two other very important identifiers to distinguish classes and components.

CID

A CID is a 128 bit number that uniquely identifies a class or component in much the same way that an IID uniquely identifies an interface. The CID for nsISupports looks like this:

00000000-0000-0000-c000-000000000046 

The length of a CID can make it cumbersome to deal with in the code, so very often you see #defines for CIDs and other identifiers being used, as in this example:

#define SAMPLE_CID \ 
{ 0x777f7150, 0x4a2b, 0x4301, \ 
{ 0xad, 0x10, 0x5e, 0xab, 0x25, 0xb3, 0x22, 0xaa}} 

You also see NS_DEFINE_CID used a lot. This simple macro declares a constant with the value of the CID:

static NS_DEFINE_CID(kWebShellCID, NS_WEB_SHELL_CID); 

A CID is sometimes also referred to as a class identifier. If the class to which a CID refers implements more than one interface, that CID guarantees that the class implements that whole set of interfaces when it's published or frozen.

Contract ID

A contract ID is a human readable string used to access a component. A CID or a contract ID may be used to get a component from the component manager. This is the contract ID for the LDAP Operation component:

"@mozilla.org/network/ldap-operation;1" 

The format of the contract ID is the domain of the component, the module, the component name, and the version number, separated by slashes.

Like a CID, a contract ID refers to an implementation rather than an interface, as an IID does. But a contract ID is not bound to any specific implementation, as the CID is, and is thus more general. Instead, a contract ID only specifies a given set of interfaces that it wants implemented, and any number of different CIDs may step in and fill that request. This difference between a contract ID and a CID is what makes it possible to override components.

XPCOM Identifier Classes

The nsIID class is actually a typedef for the nsID class. The other typedefs of nsID, CID and IID, refer to specific implementations of a concrete class and to a specific interface, respectively. The nsID class provides methods like Equals for comparing identifiers in the code. See "Identifiers in XPCOM" on page 60 for more discussion of the nsID classes.

Factories

Once code is broken up into components, client code typically uses the new constructor to instantiate objects for use:

SomeClass* component = new SomeClass(); 

This pattern requires that the client know something about the component, however-how big it is at the very least. The factory design pattern can be used to encapsulate object construction. The goal of factories is create objects without exposing clients to the implementations and initializations of those objects. In the SomeClass example, the construction and initialization of SomeClass, which implements the SomeInterface abstract class, is contained within the New_SomeInterface function, which follows the factory design pattern:

int New_SomeInterface(SomeInterface** ret)  
{ 
  // create the object 
  SomeClass* out = new SomeClass(); 
  if (!out) return -1; 
   
  // init the object 
  if (out->Init() == FALSE) 
  { 
    delete out; 
    return -1; 
  } 
 
  // cast to the interface 
  *ret = static_cast<SomeInterface*>(out); 
  return 0; 
} 

Figure 6. Encapsulating the Constructor

The factory is the class that actually manages the creation of separate instances of a component for use. In XPCOM, factories are implementations of the nsIFactory interface, and they use a factory design pattern like the example above to abstract and encapsulate object construction and initialization.

The example in Figure 6 above is a simple and stateless version of factories, but real world programming isn't usually so simple, and in general factories need to store state. At a minimum, the factory needs to preserve information about what objects it has created. When a factory manages instances of a class built in a dynamic shared library, for example, it needs to know when it can unload the library. When the factory preserves state, you can ask if there are outstanding references and find out if the factory created any objects.

Another state that a factory can save is whether or not an object is a singleton. For example, if a factory creates an object that is supposed to be a singleton, then subsequent calls to the factory for the object should return the same object. Though there are tools and better ways to handle singletons (which we'll discuss when we talk about the nsIServiceManager), a developer may want to use this information to ensure that only one singleton object can exist despite what the callers do.

The requirements of a factory class can be handled in a strictly functional way, with state being held by global variables, but there are benefits to using classes for factories. When you use a class to implement the functionality of a factory, for example, you derive from the nsISupports interface, which allows you to manage the lifetime of the factory objects themselves. This is important when you want to group sets of factories together and determine if they can be unloaded. Another benefit of using the nsISupports interface is that you can support other interfaces as they are introduced. As we'll show when we discuss nsIClassInfo, some factories support querying information about the underlying implementation, such as what language the object is written in, interfaces that the object supports, etc. This kind of "future-proofing" is a key advantage that comes along with deriving from nsISupports.

XPIDL and Type Libraries

An easy and powerful way to define an interface-indeed, a requirement for defining interfaces in a cross-platform, language neutral development environment-is to use an interface definition language (IDL). XPCOM uses its own variant of the CORBA OMG Interface Definition Language (IDL) called XPIDL, which allows you to specify methods, attributes and constants of a given interface, and also to define interface inheritence.

There are some drawbacks to defining your interface using XPIDL. There is no support for multiple inheritence, for one thing. If you define a new interface, it cannot derive from more than one interface. Another limitation of interfaces in XPIDL is that method names must be unique. You can not have two methods with the same name that take different parameters, and the workaround-having multiple function names-isn't pretty:

void FooWithInt(in int x); 
void FooWithString(in string x); 
void FooWithURI(in nsIURI x); 

However, these shortcomings pale in comparison to the functionality gained by using XPIDL. XPIDL allows you to generate type libraries, or typelibs, which are files with the extension .xpt. The type library is a binary representation of an interface or interfaces. It provides programmatic control and access of the interface, which is crucial for interfaces used in the non C++ world. When components are accessed from other languages, as they can be in XPCOM, they use the binary type library to access the interface, learn what methods it supports, and call those methods. This aspect of XPCOM is called XPConnect. XPConnect is the layer of XPCOM that provides access to XPCOM components from languages such as JavaScript. See "Connecting to Components from the Interface" on page 30 for more information about XPConnect.

When a component is accessible from a language other than C++, such as JavaScript, its interface is said to be "reflected" into that language. Every reflected interface must have a corresponding type library. Currently you can write components in C, C++, JavaScript, or Python, and there are efforts underway to build XPCOM bindings for Ruby and Perl as well.

Writing Components in Other Languages

Though you do not have access to some of the tools that XPCOM provides for C++ developers (such as macros, templates, smart pointers, and others) when you create components in other languages, you may be so comfortable with the language itself that you can eschew C++ altogether and build, for example, Python-based XPCOM components that can be used from JavaScript or vice versa. See the "References" section in Appendix C for more information about Python and other languages for which support has been added in XPCOM.

All of the public interfaces in XPCOM are defined using the XPIDL syntax. Type libraries and C++ header files are generated from these IDL files, and the tool that generates these files is called the xpidl compiler. The section "Defining the Weblock Interface in XPIDL" on page 103 describes the XPIDL syntax in detail.

XPCOM Services

When clients use components, they typically instantiate a new object each time they need the functionality the component provides. This is the case when, for example, clients deal with files: each separate file is represented by a different object, and several file objects may be being used at any one time.

But there is also a kind of object known as a service, of which there is always only one copy (though there may be many services running at any one time). Each time a client wants to access the functionality provided by a service, they talk to the same instance of that service. When a user looks up a phone number in a company database, for example, probably that database is being represented by an "object" that is the same for all co-workers. If it weren't, the application would need to keep two copies of a large database in memory, for one thing, and there might also be inconsistencies between records as the copies diverged.

Providing this single point of access to functionality is what the singleton design pattern is for, and what services do in an application (and in a development environment like XPCOM).

In XPCOM, in addition to the component support and management, there are a number of services that help the developer write cross platform components. These services include a cross platform file abstraction which provides uniform and powerful access to files, directory services which maintain the location of application- and system-specific locations, memory management to ensure everyone uses the same memory allocator, and an event notification system that allows passing of simple messages. The tutorial will show each of these component and services in use, and Appendix B has a complete interface listing of these areas.

XPCOM Types

There are many XPCOM declared types and simple macros that we will use in the following samples. Most of these types are simple mappings. The most common types are described in the following sections:

Method Types

The following are a set of types for ensuring correct calling convention and return time of XPCOM methods.

NS_IMETHOD 
Method declaration return type. XPCOM method declarations should use this as their return type.
NS_IMETHODIMP 
Method Implementation return type. XPCOM method implementations should use this as their return time.
NS_IMETHODIMP_(type) 
Special case implementation return type. Some methods such as AddRef and Release do not return the default return type. This exception is regrettable, but required for COM compliance.
NS_IMPORT 
Forced the method to be resolved internally by the shared library.
NS_EXPORT 
Forces the method to be exported by the shared library.

Reference Counting

Set of macros for managing reference counting.

NS_ADDREF 
Calls AddRef on an nsISupports object
NS_IF_ADDREF 
Same as above but checks for null before calling AddRef
NS_RELEASE 
Calls Release on an nsISupports object
NS_IF_RELEASE 
Same as above but check for null before calling Release

Status Codes

These macros test status codes

NS_FAILED 
Return true if the passed status code was a failure.
NS_SUCCEEDED 
Returns true is the passed status code was a success.

Variable mappings

nsrefcnt 
Default reference count type. Maps to an 32 bit integer.
nsresult 
Default error type. Maps to a 32 bit integer.
nsnull 
Default null value.

Common XPCOM Error Codes

NS_ERROR_NOT_INITIALIZED 
Returned when an instance is not initialized.
NS_ERROR_ALREADY_INITIALIZED 
Returned when an instance is already initialized
NS_ERROR_NOT_IMPLEMENTED 
Returned by an unimplemented method
NS_ERROR_NO_INTERFACE    
Returned when a given interface is not supported.
NS_ERROR_NULL_POINTER    
Returned when a valid pointer is found to be nsnull.
NS_ERROR_FAILURE        
Returned when a method fails. Generic error case.
NS_ERROR_UNEXPECTED      
Returned when an unexpected error occurs.
NS_ERROR_OUT_OF_MEMORY  
Returned when a memory allocation fails.
NS_ERROR_FACTORY_NOT_REGISTERED  
Returned when a requested class is not registered.


Copyright (c) 2003 by Doug Turner and Ian Oeschger. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.02 or later. Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
TOC PREV NEXT INDEX