An Overview of 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 monolithic 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 Interfaces. 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:

  • component management
  • file abstraction
  • object message passing
  • memory management

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:

Benefit Description
재사용 모듈화된 코드는 다른 응용프로그램들과 문맥들에서 재사용 될 수 있다.
갱신 전체 응용프로그램을 재컴파일하지 않고 컴포넌트들을 갱신할 수 있다.
성능 코드가 모듈화되면, 곧바로 필요하지 않은 모듈들은 메모리에 늦게 적재되거나(lazy loaded), 아예 적제되지 않아 응용프로그램의 성능을 향상시키는 효과를 가져올 수 있다.
관리 Even when you are not updating a component, designing your application 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.

인터페이스

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 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.

var el = env.locale; SomeClass Class Initialization

class SomeClass
{
  public:
    // Constructor
    SomeClass();

    // Virtual Destructor
    virtual ~SomeClass();

    // init method
    void Init();

    void DoSomethingUseful();
};

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:

var el = env.locale; Encapsulation of SomeInterface

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

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) 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 runtime. 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.

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. The nsISupports interface is shown below:

var el = env.locale; 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);
};

The various types used in the interface are described in the XPCOM Types section below. A complete (if spare) implementation of the nsISupports interface is shown below:

var el = env.locale; Implementation of nsISupports Interface

// initialize the reference count to 0
Sample::Sample() : 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;
}
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:

var el = env.locale; Simple Class Inheritance

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();
};

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 Simple Class Inheritance, 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.

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 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.

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:

var el = env.locale; Encapsulating the Constructor

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; 
} 

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 Encapsulating the Constructor 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 inheritance.

There are some drawbacks to defining your interface using XPIDL. There is no support for multiple inheritance, 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 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++, or JavaScript (and sometimes Python or Java, depending on the state of the respective bindings), and there are efforts underway to build XPCOM bindings for Ruby and Perl as well.

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 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 the XPCOM API Reference 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 type 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 Forces the method to be resolved internally by the shared library.
NS_EXPORT Forces the method to be exported by the shared library.

Reference Counting

These macros manage 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 a 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.

Document Tags and Contributors

태그:
Contributors to this page: Jevolp, 하양파랑, CN
Last updated by: Jevolp,