This chapter provides a high-level introduction to XPCOM component technology. XPCOM can be difficult to master, but after reading this chapter, you should have a good sense of what it is and the important part it plays as Mozilla's core technology. You should be able to find and use existing scriptable components in your own applications and create a simple XPCOM component by using JavaScript or C++.
XPCOM permits a reusable code module to be globally accessible to a Mozilla-based application. You do not need to worry about including external source files in your application distribution and you can distribute components by using XPInstall. This type of architecture makes the development of core application services flexible and entirely modular.
The section Section 8.2.1 lets you create an interface from start to finish -- writing the implementation for that interface, compiling it into a type library, registering it with Mozilla, and then testing the new component. One advantage of using XPCOM is that you can create multiple implementations for a single interface; following the JavaScript component section, we will take the same nsISimple interface and implement it in C++ as well.
The section Section 8.2.5 includes some techniques and programming tasks that are particular to C++ components, such as handling return values and generating header files and useful macros. The section Section 8.2.7 introduces the XPCOM bindings for the Python language (pyXPCOM). First, it provides an overview of XPCOM and how it relates to other technologies used in Mozilla.
XPCOM is Mozilla's cross-platform component object model. Although it is similar to Microsoft's COM technology, this chapter points out some important differences.
Essentially, when you program in a component-based environment, you do one of three things: you create a new component using existing components, write a component that implements other components, and establish interdependencies and a service network.
You've already seen components used in this book. In some cases, you may have used the services of Mozilla components without knowing it -- for example, when you created a XUL tree widget in the section Section 3.4.2 in Chapter 3, and used its built-in layout and view capabilities. Some of this functionality is defined in an interface called nsITreeView, which provides specific methods and properties for a XUL tree, persisting its state, row, cell, and column properties, navigation, and other object metadata used in a tree object. Behind the scenes, you'll find an XPCOM-instantiated tree view object where methods and properties associated with the XUL element are accessed via DOM > JavaScript > XPConnect > XPCOM layers.
A component is a reusable or modular piece of code that implements a clearly defined interface. In Mozilla, this code can exist as a singleton service or an object instance. A singleton service is an object instance that is created only once and then used by other code (usually called "callers," "clients," or "consumers"). An object instance is an object that is instantiated once or many times. Components are written as classes that typically have member variables and methods. The basic purpose of a component is to implement a clearly defined set of APIs that exist in a public interface. The interface exists separately so that the implementation is abstracted away, and it can be changed without affecting the interface or breaking binary compatibility. When interfaces are deployed in a production environment, they are frozen, which means they are held in an immutable state -- theoretically for as long as the application exists. While MSCOM provides a component-based programming model on Microsoft platforms, XPCOM provides it on all platforms where Mozilla is available.
Example 8-1 shows how simple using XPCOM components can be. In two lines, an XPConnect-wrapped nsIBookmarksService object is instantiated, and one of its methods is called, providing easy access to this XPCOM component from JavaScript.
As you can see, the assignment of an XPCOM object to the variable bmks takes only a single line. Once you are comfortable using XPCOM from JavaScript, you can use any of Mozilla's scriptable interfaces in your application. Once an object like bmks is created, as in Example 8-1, it can be used to call any method in the nsIBookmarksService interface, of which Flush( ) is an example.
As shown the previous example, the XPCOM object is called and instantiated from script. For an interpreted language like JavaScript to call and instantiate it, a bridge must bind JavaScript types to XPCOM types. These type bindings are part of a technology called XPConnect.
In XPConnect, XPCOM interfaces, classIDs, and progIDs are stored as global JavaScript objects and properties that can be manipulated directly through a top-level object called Components. This object accesses any component that is declared "scriptable" in an XPCOM IDL interface. Through the Components object, you can access and use the services that these interfaces provide. The Component object's top-level properties and methods include:
A method used to match an interface with a desired implementation. The implementation can be in C, C++, JavaScript, Python, and other languages for which appropriate bindings are created. You can have multiple implementations for an interface. Through QueryInterface, you can ask for and assign the desired interface to its implementation. Each XPCOM object needs to implement QueryInterface in order to return an instance of that object's class:
js> var clz = Components.classes['@mozilla.org/file/local;1']; js> var inst = clz.getService( ); js> inst.QueryInterface(C.interfaces.nsILocalFile); [xpconnect wrapped nsILocalFile @ 0x81b7040]
A read-only object array containing all the interfaces declared scriptable in the IDL file. The object name has the same name as the interface it represents.
Components.interfaces.nsILocalFile
The source file for this particular interface, for example, is nsILocalFile.idl. This XPIDL compiler compiles this file to produce a cross-platform binary type library, nsILocalFile.xpt, which contains tokenized IDL in an efficiently parsed form.
A read-only array of all the XPCOM component classes indexed by the ProgID (or human-readable name) of the component class. The classes object has these properties associated with it:
toString Returns the string progID. QueryInterface Used to QI this interface. name Returns the string progid name. number Returns the string components uuid number. valid Boolean verifies if the instance is valid. equals The Boolean used to match identical instances. initialize I don't know what this does. createInstance Will create an instance of the component; you can have many instances. getService Will instantiate the component as a service; you can have only one instance of a service.
The same as classes, except this time the array is indexed by the "canonical" or "well-established" form of their CLSID:
Components.classesByID['{dea98e50-1dd1-11b2-9344-8902b4805a2e}'];
The classesByID object has the same properties object associated with it as the class object. The properties are also used in the same way:
toString QueryInterface name number valid equals initialize createInstance getService
A read-only property that represents a snapshot of the current JavaScript call stack. JavaScript handles each code interpretation one call at a time and then places that code onto a call stack. This property can be used for recondite diagnostic purposes:
js> var C=Components; js> C.stack; JS frame :: typein :: <TOP_LEVEL> :: line 2 js> C.stack; JS frame :: typein :: <TOP_LEVEL> :: line 3
An object array of nserror results:
Components.results.NS_ERROR_FILE_ACCESS_DENIED; 2152857621
A reflection of the XPCOM global native component manager service. Using the component manager is the only way for a component to actually be created. It uses the components factory to create an instance of the class object.
A constructor used for a component written in JavaScript This component needs to register itself with the component manager by using its own nsID (an ID that is not already registered and thus does not appear in Components.classes).
A JavaScript constructor used to create exception objects. When implementing XPCOM interfaces in JavaScript, these exception objects are the preferred types of exceptions. When an XPCOM exception is thrown in your JS code, it takes the form of an Exception object that has properties associated with this object. Exceptions are usually caught in a "catch" block.
A JavaScript constructor object that constructs new instances of XPCOM components:
js> var File=new Components.Constructor( "@mozilla.org/file/local;1", "nsILocalFile", "initWithPath");
The interface nsILocalFile and the method initWithPath are optional. This example creates and initializes the nsILocalFile component.
A function that determines if the results code argument is successful. It takes an argument of nsresult and returns the Boolean values true or false:
js> Components.isSuccessCode(Components.results.NS_OK); true js> Components.isSuccessCode(Components.results.NS_ERROR_FAILURE); false
The methods and properties of the Components object listed above provide the only means to instantiate and access XPCOM objects from JavaScript. They are found often in the Mozilla codebase. In the sections that follow, they will be used frequently.
All XPCOM interfaces are defined with the Interface Definition Language (IDL). IDL provides a language-neutral way to describe the public methods and properties of a component. Mozilla actually uses a modified, cross-platform version of IDL called XPIDL to compile interface source files.
The separation of interface and implementation is a key distinction of COM programming. If the application programming interface (API) is abstracted from the implementation language and then frozen, consumers of that API will receive a guaranteed, established contract with the interface that ensures it will not be changed. This is perhaps the main reason why COM was invented: to maintain compatibility on a binary level so the client code can find and use the library it needs without worrying about linking to it. To make this sort of modularity possible at runtime, IDL interfaces are compiled into binary files called type libraries, which are described later in the section Section 8.1.4.
It is important to understand that most XPCOM components implement at least two interfaces. Like COM, each component needs the QueryInterface, AddRef, and Release functions to be available as an XPCOM object. These methods are derived from a basic interface called nsISupports, which is the XPCOM equivalent to Microsoft COM's IUnknown, shown in Table 8-1.
Table 8-1. The IUnknown interface
Name |
Type |
Description |
Parameters / return value |
---|---|---|---|
AddRef |
ULONG AddRef(void) |
Increments the reference count on the COM object. |
Returns: int, which is the new incremented reference count on the object. This value may be useful for diagnostics or testing. |
QueryInterface |
HRESULT QueryInterface(/* [in] */ REFIID riid, /* [iid_is][out] */ void **ppvObject) |
Retrieves a pointer to the requested interface. |
Parameters: iid, which is an [in] identifier of the requested interface. ppvObject, which is an [out] pointer to the interface pointer identified by iid. If the object does not support this interface, ppvObject is set to NULL. Returns: HRESULT, which is the standard HRESULT value. |
Release |
ULONG Release(void) |
Decrements the reference count on the COM object. |
Returns: int, which is the new decremented reference count on the object. This value may be useful for diagnostics or testing. |
Tables 8-1 and 8-2 illustrate the minor differences between Microsoft's nsIUnknown and Mozilla's nsISupports root interfaces. The usage is covered in detail throughout this chapter.
Table 8-2. The nsISupports interface
Name |
Type |
Description |
Parameters / return value |
---|---|---|---|
AddRef |
NS_IMETHOD_(nsrefcnt) AddRef(void) |
Increases the reference count for this interface. The associated instance will not be deleted unless the reference count is returned to zero. |
Returns: The resulting reference count. |
QueryInterface |
NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) |
A runtime mechanism for interface discovery. |
Parameters: param aIID [in], which is a requested interface IID. param aInstancePtr [out], which is a pointer to an interface pointer that receives the result. Returns: NS_OK if the interface is supported by the associated instance; NS_NOINTERFACE if it is not; and NS_ERROR_INVALID_POINTER if aInstancePtr is NULL. |
Release |
NS_IMETHOD_(nsrefcnt) Release(void) = 0; |
Decreases the reference count for this interface. Generally, if the reference count returns to zero, the associated instance is deleted. |
Returns: The resulting reference count. |
QueryInterface, Addref, and Release are required methods that are implemented by every component. QueryInterface matches a specific interface with its implementation class module. Addref and Release are methods used for reference counting. When an instance of a component is created, one or more pointers may reference that object. For each reference, a count is incremented by one. When a reference is no longer used, Release is called to decrement the count. You must hold a reference count to ensure that no pointers reference an object after it is deleted. When pointers try to access objects that are deleted, the application core dumps. Reference counting can get tricky, which is why smart pointers manage Addref and Release for you, as described in the later section Section 8.2.4.
Defining QueryInterface, Addref, and Release every time an interface is created is clearly not very efficient. Instead, these methods are defined in the base interface called nsISupports. All interfaces inherit from this mother of all interfaces and don't need to redefine these three basic functions.
XPIDL supports the C style syntax preparser directive #include to include other IDL files -- not unlike MSCOM, which uses the import statement. At the top of any IDL file that you create, you need to include nsISupports:
#include "nsISupports.idl" interface nsISimple : nsISupports { readonly attribute string value; };
An IDL compiler is a tool that creates a binary distribution file called a type library from an interface description source file. Since support for many different platforms is a requirement for Mozilla, a modified version of the libIDL compiler from the Gnome project is used. This variant is called the XPIDL compiler and is primarily used to compile Mozilla's own dialect of IDL, conveniently called XPIDL. The XPIDL compiler generates XPCOM interface information, headers for XPCOM objects, and XPT type libraries from which objects may be accessed dynamically through XPConnect. It can also generate HTML files for documentation and Java class stubs. Another feature of the XPIDL compiler is the option to generate C++ code stubs. This feature creates nearly all the declaratory C++ code you need when you start a new project, which makes XPIDL useful as a coding wizard that helps you get started. Code generation is covered later in this chapter in the section Section 8.2.5.
The XPIDL compiler is located in xpcom/typelib/xpidl/ in the Mozilla sources. If you built Mozilla, you can add this directory to your PATH:
$ PATH=$PATH:/usr/src/mozilla/xpcom/typelib/xpidl
Using the compiler is fairly easy. If you use the help command, you can see the usage syntax and other basic information about the compiler:
$ ./xpidl --help Usage: xpidl [-m mode] [-w] [-v] [-I path] [-o basename] filename.idl -a emit annotations to typelib -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) java Generate Java interface (.java)
The key to the component architecture of XPCOM is the presence of binary-independent interface files that are used uniformly across platforms, languages, and programming environments. These interface files are compiled into .xpt files by the XPIDL compiler. The Mozilla components subdirectory is where type libraries and modules are typically stored. If you create a cross-platform type library for your component, you must place it in this directory for it to be accessible to XPCOM.
To create a (.xpt) typelib file, use the flag -m typelib with warning (-w) and verbose (-v) modes turned on. -o is used for the name of the output file and -I is used to specify paths to other IDL files you want to include. To successfully compile your interface, you must always point to the directory where nsISupports is located.
# include path to nsISupports.idl $ $XPIDL_INC = /usr/src/mozilla/xpcom/base #compile nsISimple.idl $ xpidl -m typelib -w -v -I $XPIDL_INC \ > -o nsISimple nsISimple.idl
The file created after compilation is nsISimple.xpt. It provides the necessary type information about your interface at runtime. Typelib files enumerate the methods of interfaces and provide detailed type information for each method parameter.
To simplify the process of dynamically finding, loading, and binding interfaces, all classes and interfaces are assigned IDs. An ID is a unique 128-bit number that is based on universally unique identifiers (UUIDs) generated by various tools such as uuidgen (which we will cover later in this chapter). They are stored in the structure format defined below:
struct nsID { PRUint32 m0; PRUint16 m1, m2; PRUint8 m3[8]; };
To initialize an ID struct, declare it like this:
ID = {0x221ffe10, 0xae3c, 0x11d1, {0xb6, 0x6c, 0x00, 0x80, 0x5f, 0x8a, 0x26, 0x76}};
One thing that gives XPCOM its modularity is the dynamic allocation of objects through the use of unique identifiers at runtime. This system of canonical identifiers is used for interface querying and component instantiation. Having an interface is important because it ensures that an immutable binary holds a semantic contract defined for a specific interface class.
The two types of identifiers used in XPCOM are the contract ID and the class identifier. These identifiers are shuttled to the Component Manager's createInstance( ) or the Service Manager's getService( ) methods in order to instantiate a component.
The program ID (progID), also known as the Contract ID, is a unique human-readable string. Example 8-2 shows various progIDs for different components. This example can be used to instantiate an XPCOM component through the use of a Contract ID.
The other type of identifier is the classID, or CLSID. The interface and implementation are identified by this 128-bit numerical identifier string:
// clsid: {2e23e220-60be-11d3-8c4a-000064657374} var f = Components.classesByID["{2e23e220-60be-11d3-8c4a-000064657374}"];
Using XPConnect, XPCOM interfaces, classIDs, and progIDs are stored as global JavaScript objects and properties and can be manipulated directly through the top-level Components object discussed earlier.
To obtain a UUID on Unix, you can use a command-line program called uuidgen that generates a unique number for you:
$ uuidgen ce32e3ff-36f8-425f-94be-d85b26e634ee
On Windows, a program called guidgen.exe does the same thing and also provides a graphical user interface if you'd rather point and click.
Or you can use one of the special "bots" on IRC at the irc.mozilla.org server.
irc irc.mozilla.org /join #mozilla /msg mozbot uuid
This command makes the bot generate and return a uuid, which you can then copy into your component source code. The information can then be used to uniquely identify your component.
One major goal of XPCOM modularization is the removal of link-time dependencies, or dependencies that arise when you link libraries during compilation. The achievement of this goal allows you to access and use modules at runtime. The trouble then becomes finding those modules and figuring out which of their interfaces you want to use. This problem is solved through the use of the Component Manager.
The Component Manager is a special set of component management classes and implementation classes that reside in object libraries (.dll, .so, .js, .py, etc.). These classes also include factories, which let you create objects without having access to their class declarations. When you bind to objects at runtime, as you do in XPCOM, you need functionality like this to help you discover and use objects without looking at their code. The Component Manager also includes the Component Manager class itself, known as nsComponentManager, which is a mapping of class IDs to factories for the libraries they contain. The Component Manager is responsible for the autoregistration of all new or add-on modules located in the components directory. This autoregistration happens behind the scenes and allows you to use new components as they become available without having to register them yourself.
A component author first creates an interface file that defines all APIs that will be publicly available for a component. The component author then creates an implementation for the methods and attributes in a separate implementation class. For example, an nsILocalFile interface may have an nsLocalFile implementation class. Then a factory or module is needed to abstract the implementation class, and reduce compile and link-time dependencies. It then creates instances of the implementation class through its own implementation of QueryInterface. For example:
// create an instance of the implementation class var f = Components.classes[`@mozilla.org/file/local;1'].createInstance( );
The variable f is assigned an instance of a nsLocalFile implementation class using the nsIFactory method createInstance( ). To match the correct interface (nsILocalFile in this case) to the implementation, you need a class instance to be created before you can call on its member method QueryInterface( ):
// QI for nsILocalFile interface var f = f.QueryInterface(Components.interfaces.nsILocalFile);
Once you do this, the variable f is ready to use the nsILocalFile interface to access the newly created instance of the nsLocalFile class from script.
Simply put, a factory or module is a set of classes used by the Component Manager to register and create an instance of the component's implementation class. A factory can make its way into the Mozilla component repository in several ways. The most direct is through using the Component Manager method RegisterFactory( ), which supports two different registration mechanisms. The first mechanism, which takes a class ID and a pointer to a factory, can be used on factories that are actually linked into the executable. The second, which takes a class ID and the path to a dynamically loadable library, can be used both inside an executable at runtime and externally by using the aPersist flag to tell the repository to store the class ID/library relationship in its permanent store. The Component Manager discovers new factories or modules placed in the components directory and queries those modules for the XPCOM components they provide. The name, contract IDs, and class IDs are placed into a small component registry database for quick retrieval. The factory provides this information through a simple set of APIs required by every XPCOM module. Module creation, covered later in this chapter, describes the process through which all components contain an implementation of a module or factory.
Mozilla is a client application that implements XPCOM, so everything you need to use or build new XPCOM components is already included in the source code and/or the binaries. Whenever you use the JavaScript Components object, as described earlier, you use XPCOM.
If you'd rather not build the entire Mozilla browser and you have no interest in existing Mozilla components or the large footprint that comes with an entire distribution, then standalone XPCOM is for you. To pull the XPCOM source on Unix using Mac OS X or cygwin on Windows, invoke the following commands:
cvs -z 3 co mozilla/client.mk cd mozilla gmake -f client.mk pull_all BUILD_MODULES=xpcom
To build the XPCOM Stand Alone version, type:
configure --enable-modules=xpcom gmake
When you build standalone XPCOM, the directory xpcom/sample contains the source code for a sample application and a nsTestSample component. The sample application built from these sources, also called nsTestSample, is installed in the Mozilla bin directory. libsample.so (Unix), which is the component that the sample application tries to instantiate, should have been installed in bin/components. To run the test that indicates whether standalone XPCOM is installed successfully, change to the mozilla/dist/bin directory and run the following commands:
./run-mozilla.sh ./nsTestSample
You should see the following output. If you do not, there is a problem with the installation of standalone XPCOM:
Type Manifest File: /D/STAND_ALONE_XPCOM/mozilla/dist/bin/components/xpti.dat nsNativeComponentLoader: autoregistering begins. nsNativeComponentLoader: autoregistering succeeded nNCL: registering deferred (0) Inital print: initial value Set value to: XPCOM defies gravity Final print : XPCOM defies gravity Test passed.
Using standalone XPCOM is a powerful way to use the Mozilla framework of cross-platform COM. Even if you're just hacking on Mozilla, standalone XPCOM is a great way to learn about and use XPCOM for application development.