Hack 82. Add a New XPCOM Component
Extend Firefox's component library with a new object that's usable in the chrome. Firefox comes preinstalled with over a thousand XPCOM objects (classes and interfaces). Most of these objects provide fundamental services, such as access to files, networks, and data. Nearly all of these objects are written in C++ and are provided in compiled libraries. This hack shows how to add new components written in JavaScript. Typically, new objects provide application-level abstractions on top of the existing libraries, but in theory, they can do anything. We'll use as an example the creation of a simple persistence classone that dumps a simple string of information to a disk-based file. We'll put all the required logic into a single script named nsStringSave.js, which we'll put in the components directory in the install area. We'll use limited error checking, which could easily be done more robustly. The whole component creation process comes down to dancing to the beat of the XPIDL (Cross Platform Interface Definition Language) drum. Mozilla interfaces are specified in XPIDL. They're part of the Mozilla source code; you can also download them in convenient form from http://www.nigelmcfarlane.com/books/radmoz_support.html. 7.9.1. Naming the New ComponentComponents and interfaces must be defined by both names and numbers. We're creating one new component and one new interface, so that's two names and two numbers. To get the name, we make it up, using some existing conventions. To get the number (called an ID), we run uuidgen or guidgen from the command line or /msg mozbot uuid on the #mozilla channel of irc.mozilla.org. On Windows, you can get uuidgen with free Microsoft compiler products [Hack #93] . Interface names are defined in XPIDL files, so here's such a file named nsIStringSave.idl: #include "nsISupports.idl" [scriptable, uuid(A35ED730-4817-4BE3-943E-75E9124FC4D7)] interface nsIStringSave : nsISupports { void set_file(in string pathname); void dump(in string str); }; All components should implement the nsISupports interface, which is the fundamental interface. We generate a type library from this file. The original IDL serves as documentation only. To make this library, you need the tool xpidl. It's available in nightly Mozilla builds [Hack #92] . Use it to generate a nsIStringSave.xpt file. On UNIX/Linux/Macintosh: xpidl -m typelib nsIStringSave.idl On Windows: Xpidl.exe -m typelib nsIStringSave.idl Put this .xpt file in the components directory. Component names are defined along with the componentin this case, done as follows in JavaScript: const SSComponentName = "@mozilla.org/example/string-save;1"; const SSClassID = Components.ID('{a81d9b27-6adc-47c7-a780-dced1e1e4cb3}'); Again, uuidgen is used to create a unique identifier. We'll put these statements in a script shortly. Naming of the new component is now complete. 7.9.2. Creating and Implementing a ModuleModules are the ultimate containers for XPCOM components. Each module holds zero or more components. Modules themselves implement the nsIModule interface. Here's our new module in JavaScript: const SSComponentName = "@mozilla.org/example/string-save;1"; const SSClassID = Components.ID('{a81d9b27-6adc-47c7-a780-dced1e1e4cb3}'); var nsStringSaveModule = { getClassObject : function (mgr, cid, iid, ) { if ( !cid.equals(SSClassID) ) throw "unknown"; var component = (new nsStringSave( )).QueryInterface(iid); component._init( ); // the module knows its components return component; }, registerSelf : function ( ) { /* contstructor - does nothing here */ }, unregisterSelf : function ( ) { /* destructor - does nothing here */}, canUnload : function ( ) { return false; /* can't unload */ } }; The getClassObject() method checks its arguments and returns an instance of the only component that it knows about. The module object does nothing by itself; an equivalent of C's main() is required. For module definition scripts, Firefox expects to find the special hook function named NSGetModule(), so we add that as well: function NSGetModule( ) { return nsStringSaveModule; } 7.9.3. Creating and Implementing a ComponentThe getClassObject() method in the previous section creates an object that's an instance of the component. It uses the following custom JavaScript object constructor to do so. Append this lot to the file containing the module code: var Cc = Components.classes; // shorthand var Ci = Components.interfaces; // shorthand function nsStringSave ( ) {}; nsStringSave.prototype = { // --- "private" data --- _file : null, _stream : null, _init : function ( ) { _file = Cc["@mozilla.org/local/file;1"].createInstance(Ci.nsILocalFile); _stream = Cc["@mozilla.org/network/file-output-stream;1"]; _stream = _stream.createInstance(Ci.nsIFileOutputStream); }, // --- nsIStringSave --- set_file : function (path) { _file.initWithPath(path); _stream.close( ); _stream.init(_file, 0x0A, 0644, true); }, dump : function (str) { _stream.write(str, str.length); }, // --- nsISupports --- QueryInterface : function (iid) { if ( !iid.equals(Components.interfaces.nsISupports) ) throw "unknown"; if ( !iid.equals(Components.interfaces.nsIStringSave) ) throw "unknown"; return this; } }; There's a bit of complexity at work here, both in the use of JavaScript prototypes and in the opening and administering of file objects. The important bit is that the object constructor constructs an object that implements the two required interfaces: nsISupports and nsIStringSave. 7.9.4. Installing the ComponentThe nsIStringSave.js file, once complete, should be put in the components directory. After that, we have to fool Firefox into thinking that the install area's been patched with new files (in truth, it has). Once that misdirection is achieved, Firefox will do extra work the next time it starts up. It will do a big audit of all the stuff it has to work with, and that includes discovering all XPCOM modules and components.
To flag that the audit should occur, move to the user profile area. Find the file named compatibility.ini. Edit it with a text editor and decrement the Build ID date by one day. Save the file in place. When Firefox next starts up, it will do its big audit because it thinks that it has been patched (the build date has gone up). It will find the nsStringSave.xpt file and also the new NSGetModule() call. From those things and others, it will write out a file called compreg.dat. Each time Firefox starts up subsequently, it will rely on the compreg.dat file for an accurate list of available objects. When getting ready for the audit, it is therefore critical that you back up this file, in case there are programming errors in your new modules, components, or interfaces. Those errors will cause the regenerated compreg.dat file to be flawed, and in that case, you'll need the old one to go back to. 7.9.5. Running the ComponentFrom any secure scripting environment (in the chrome, a signed web page, or using the xpcshell testing tool) this is all the code you need to use your new component: var saver = Components.classes["@mozilla.org/example/string-saver;1"]; saver = saver.createInstance(Components.interfaces.nsIStringSaver); saver.set_file("C:\tmp\test.dat"); saver.dump("A test string of no particular import"); xpcshell is a small, non-GUI JavaScript interpreter available in nightly builds that can manipulate XPCOM objects. |