Howto: Build an XPCOM Component in Javascript

Introduction

Here’s my first cut at a “Hello World” tutorial for creating an XPCOM component in Javascript. The tutorials, wiki pages and knowledge-base entries that exist on this subject are mostly from the perspective of a C++ developer, not a front-end developer, and therefore seemed to always leave out basic steps and information. Slay me with corrections in the comments🙂

UPDATE: I’ve moved this tutorial to the Mozilla Developer Center, and made a some of the changes recommended in the comments. Please read the updated version instead of this one.

This is a very utilitarian tutorial (say that 10x fast!): I’m not going to describe how and why XPCOM works the way it does, or what every bit of the example code does. That’s been detailed elsewhere. I’m just going to show you what you need to do to get things *working*, in as few and as simple steps as possible.

Caveat: This was done on a Mac. YMMV with Windows.

Prerequisites

Download and compile Firefox. Haha, ok maybe this tutorial is not as simple as I thought🙂 Seriously though, it takes a while to build, but it’s not hard, and the instructions (at least for Mac) are simple and intuitive. If you don’t want to do this, then you probably don’t need to implement your class as an XPCOM component, and should just stick with plain ol’ Javascript. (There is probably a way to fetch and build only what is needed to compile the typelib, but that’s probably more complicated and less documented than building Firefox. Maybe it’s time to build a web-service for compiling XPCOM typelibs…)

Please read the updated version of this tutorial!

Implementation

This example component will expose a single method, which returns the string “Hello World!”.

Defining the Interface

XPCOM uses a dialect of IDL to define interfaces, called XPIDL. Here’s the XPIDL for our HelloWorld component:

HelloWorld.idl

#include "nsISupports.idl"
[scriptable, uuid(1C0E8D86-B661-40d0-AE3D-CA012FADF170)]
interface nsIHelloWorld: nsISupports {
  string hello();
};

Note that you must generate a new UUID for each XPCOM component that you create. A web-based UUID generator is available here.

Creating the Component

HelloWorld.js

/***********************************************************
constants
***********************************************************/

// reference to the interface defined in nsIHelloWorld.idl
const nsIHelloWorld = Components.interfaces.nsIHelloWorld;

// reference to the required base interface that all components must support
const nsISupports = Components.interfaces.nsISupports;

// guid uniquely identifying our component
const CLASS_ID = Components.ID("{1C0E8D86-B661-40d0-AE3D-CA012FADF170}");

// description
const CLASS_NAME = "My Hello World Javascript XPCOM Component";

// textual unique identifier
const CONTRACT_ID = "@dietrich.ganx4.com/helloworld;1";

/***********************************************************
class definition
***********************************************************/

//class constructor
function HelloWorld() {
};

// class definition
HelloWorld.prototype = {

  // define the function we want to expose in our interface
  hello: function() {
      return "Hello World!";
  },

  QueryInterface: function(aIID)
  {
    if (!aIID.equals(nsIHelloWorld) &&
        !aIID.equals(nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/***********************************************************
class factory

This object is a member of the global-scope Components.classes.
It is keyed off of the contract ID. Eg:

myHelloWorld = Components.classes["@dietrich.ganx4.com/helloworld;1"].
                          createInstance(Components.interfaces.nsIHelloWorld);

***********************************************************/
var HelloWorldFactory = {
  createInstance: function (aOuter, aIID)
  {
    if (aOuter != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    return (new HelloWorld()).QueryInterface(aIID);
  }
};

/***********************************************************
module definition (xpcom registration)
***********************************************************/
var HelloWorldModule = {
  _firstTime: true,
  registerSelf: function(aCompMgr, aFileSpec, aLocation, aType)
  {
    if (this._firstTime) {
      this._firstTime = false;
      throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
    };
    aCompMgr = aCompMgr.
        QueryInterface(Components.interfaces.nsIComponentRegistrar);
    aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME,
        CONTRACT_ID, aFileSpec, aLocation, aType);
  },

  unregisterSelf: function(aCompMgr, aLocation, aType)
  {
    aCompMgr = aCompMgr.
        QueryInterface(Components.interfaces.nsIComponentRegistrar);
    aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
  },

  getClassObject: function(aCompMgr, aCID, aIID)
  {
    if (!aIID.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

    if (aCID.equals(CLASS_ID))
      return HelloWorldFactory;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  canUnload: function(aCompMgr) { return true; }
};

/***********************************************************
module initialization

When the application registers the component, this function
is called.
***********************************************************/
function NSGetModule(aCompMgr, aFileSpec) { return HelloWorldModule; }

Usage

Compile Typelib

Here {objdir} is the directory specified for the MOZ_OBJDIR option in your .mozconfig file.

  1. Copy HelloWorld.idl to {objdir}/dist/idl/
  2. Copy HelloWorld.js to {objdir}/dist/bin/components/
  3. Change directories to {objdir}/dist/idl/
  4. $../bin/xpidl -m typelib -w -v -e ../bin/components/HelloWorld.xpt HelloWorld.idl

This will create a typelib file at {objdir}/dist/bin/compontents/HelloWorld.xpt.

Installation

For extensions:

  1. Copy HelloWorld.js and HelloWorld.xpt to {extensiondir}/components/
  2. Delete compreg.dat and xpti.dat from your profile directory.
  3. Restart application

For Firefox core, using the application that you just built:

  1. Copy HelloWorld.js and HelloWorld.xpt to the {objdir}/dist/bin/components directory
  2. Delete compreg.dat and xpti.dat from the components directory.
  3. Delete compreg.dat and xpti.dat from your profile directory.
  4. Restart application

Using Your Component

var oMyComponent = Components.classes['@dietrich.ganx4.com/helloworld;1'].
                        createInstance(Components.interfaces.nsIHelloWorld);

alert(oMyComponent.hello());

9 Comments on “Howto: Build an XPCOM Component in Javascript”

  1. Nice to see that people are starting to give javascript example of xpcom. I started to write a tutorial for it over on the mozillazine knowledge base, but never completed it.

    http://kb.mozillazine.org/Implementing_XPCOM_components_in_JavaScript

  2. autonome says:

    Yep, much thanks to you Richard! I filled in some of that example today, and used your example component as the basis of this HelloWorld component🙂

  3. Axel Hecht says:

    May I advocate to put that on devmo? That’s the place to put that stuff.

    About the implementation, there are arguments to not throw in QI, but to use some Components foo. Ask timeless for details, sample is at http://lxr.mozilla.org/mozilla1.8.0/source/calendar/resources/content/calendarService.js#152.

    Ben has some styleguide that we should generally alias Components.classes and Components.interfaces, may be Cc and Ci, but I’m not exactly sure on those.

  4. Neil Deakin says:

    You can get compiled versions of idl tools for windows or linux by downloading the geckosdk at ftp://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/

    Also, you might want to mention that you only need them if you plan to create a new idl file, since many components only need to implement existing interfaces.

  5. To jsem si zase poÄŤetl…

  6. beltzner says:

    Nice work, Dietrich. You’ll put this on MDC, right? I’m sure Deb would love to have this content there!

  7. christian biesinger says:

    ok, I don’t want to edit the wiki right now, so I’ll comment here..

    – I’m not sure you should use a specific UUID in this example. I fear people will copy the specific id. maybe just use all zeroes for the numbers?

    that’s for both the IDL and the class ID in the .js file. btw, why did you use the same one for both?

    – I wouldn’t recommend copying stuff to the IDL. it’s not needed anyway. just leave the IDL in the dir where it is and run xpidl from the sdk, with -I /path/to/sdk/idl

    – this stuff:
    if (this._firstTime) {
    this._firstTime = false;
    throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
    };
    is something that very few components need, not something that should be used in general

  8. christian biesinger says:

    > I wouldn’t recommend copying stuff to the IDL.

    er. I meant “to the SDK directory”

  9. dietrich says:

    Thanks Christian! I’ve updated the wiki with these changes.

    -d