Skip to content

Instantly share code, notes, and snippets.

@marcoscaceres
Last active March 8, 2021 09:02
Show Gist options
  • Save marcoscaceres/13af01281730b5279ad6 to your computer and use it in GitHub Desktop.
Save marcoscaceres/13af01281730b5279ad6 to your computer and use it in GitHub Desktop.
Implementing a JS-based WebIDL binding in Gecko

DEPRECATED

I think most of this stuff is now depracted. Looks like C++ is the only realistic way to go for implementing WebIDL bindings in Gecko. That's unfortuante, because if you are a JS dev, learning C++ can be challenging (though worth while! it's a fun language.)

Implementing a JS-based WebIDL binding in Gecko

This provides a gentle introduction to implementing a WebIDL interface in JavaScript. Once you are done, you can refer to the MDN Wiki, to add more advanced things.

Before you start

We would encourage you to first build a simple prototype version of your API in JS. Going through the process of creating a WebIDL is fairly straight forward, but if you screw it up you will need to recreate a whole bunch of files, etc. As such, it's much easier to make sure you have your API in a good state before attempting the steps below.

We know you will probably just skip ahead, but you'll soon find yourself here with us telling you, "I told ya so!" :)

Part 1 - Write the WebIDL

I'm assuming you know some WebIDL ... or you are copy/pasting it from a spec.

Steps are as follows:

  1. Create Whatever.webidl in the dom/webidl folder.
  2. Add the Whatever.webidl to dom/webidl/moz.build (do this now before you forget!)

So, let's now write the WebIDL.

Open Whatever.webidl in Sublime (yes, Sublime! not emacs or vim you hipster):

[Constructor(), JSImplementation="@mozilla.org/Whatever;1"]
interface Whatever {
  attribute long value;
  readonly attribute long otherValue;
  void doWhatever();
};

Save it!

The JSImplementation "extended attribute" tells Gecko that you will implement this in JS, and what "contract" to use. We will get to that next!

Note: If you were to compile now, your interface will show up. It just won't do anything... well, it will throw because the interface in not implemented:) You might want mach build now, just to make sure everything is ok so far.

Part 2 - Implementation

Ok, now we need to code the implementation in JS.

Steps:

  1. Generate a UUID, which will identify your components. In the command line, run uuidgen | awk '{print tolower($0)}'.
  2. Create Whatever.js somewhere, like in browser/components/Whatever/. Where you put it depends on your own project.

Add this code in there. You can modify it to suit your needs.

/*global XPCOMUtils*/
/*exported NSGetFactory*/
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

function Whatever() {
  this.value = 111;
  this.invisibleValue = 12345;
}

Whatever.prototype = {
  classDescription: "Defines Whatever objects",
  // Should look like this: "{1ced49ae-2f02-49a4-914e-7da0a371e047}", including the "{" "}"
  classID: Components.ID("{INSERT YOUR NEWLY MINTED UUID HERE}"),
  contractID: "@mozilla.org/Whatever;1",
  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
  doWhatever() {},
  get otherValue() { return this.invisibleValue - 4; },
  __init() {}
};

var NSGetFactory = XPCOMUtils.generateNSGetFactory([Whatever]);

Notable things:

  1. contractID and classID, these are the bits that link together the WebIDL file and the JavaScript implementation. We cover that next when we create a manifest.

Part 3 - Creating the manifest

You now need a .manifest file that links together the WebIDL file, and the implementation.

In browser/components/Whatever, create a file called Whatever.manifest and add the following:

component {f312cb96-21b5-478c-904a-a6da0c2136a1} Whatever.js
contract @mozilla.org/Whatever;1 {f312cb96-21b5-478c-904a-a6da0c2136a1}

Note the UUIDs must match, and they must match the one in the Whatever.js file. The association between the two files is done through the UUID and @mozilla.org/Whatever;1.

You probably noted the ";1". That is just a Mozilla convention for API versioning - but WebAPIs never really change, so it's mostly redundant.

Part 4 - Final moz.build

Ok, now you need to add a moz.build file to browser/components/Whatever.

It should look like this:

EXTRA_COMPONENTS += [
    'Whatever.js',
    'Whatever.manifest',
]

Save it! You are almost done!

Part 5 - Build Gecko!

Ok, now just do the old mach build; mach run. If all went according to plan, will be able to do the following in the JS console:

// creates Whatever { value: 111, otherValue: 12341 }
new Whatever();

Recipes

The above interface is pretty dull. It doesn't actually do anything useful yet. So here is a bunch of tasty things you might now want to do with your newly created interface.

Exposing on window.navigator

If for whatever reason you want to expose the interface on the Navigator object, there is an easy way to do this by using the "NavigatorProperty extended attribute". It's pretty easy, you just add NavigatorProperty="whatevs", like so:

[NavigatorProperty="whatevs", JSImplementation="@mozilla.org/Whatever;1"]
interface Whatever {
  attribute long value;
  readonly attribute long otherValue;
  void doWhatever();
};

Now, "navigator.whatevs" will be an instance of the Whatever object.

// should return { value: 111, otherValue: 12341 }
navigator.whatevs;

Note that you usually never want something exposed on Navigator to also have a constructor. So, if you are doing something like the above, you should probably remove the "Constructor()".

Accessing the caller's window object

In order to access the caller's window object, you need to tell the implementation to use the nsIDOMGlobalPropertyInitializer as part of the QueryInterface property. Yes, what da what? :)

Let me just show you:

"use strict";
const {interfaces: Ci, utils: Cu} = Components;
{
  // ... other props would be here ... see step 2
  QueryInterface: XPCOMUtils.generateQI(
    [Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer]
  ),
  // By adding nsIDOMGlobalPropertyInitializer, init will get called with the window!
  init(contentWindow) {
    this._contentWindow = contentWindow;
  },
}

So, by adding nsIDOMGlobalPropertyInitializer to your implementation, Gecko will try to call an init() method on your implementation and pass it a reference to the Window object. Use it wisely, Padawan.

Implementing a constructor

TWB

"ChromeConstructor" (chrome only contructor)

standard "Contructor" (content)

Implementing event-emitting object

Implementing events doesn't require you to create JS-based implementation (yay!). There is some handy stuff built into Gecko that will automagically wire up events for you. This is great, as it greatly reduces the amount of work you have to do.

You need to do the follow, tho (full step below):

  1. Specify that your interface extents EvenTarget + add an WebIDL attribute to your interface.
  2. Specify your event, and its initialization dictionary in a seperate file (e.g., MozThingChangedEvent.webild).
  3. Add your event to the GENERATED_EVENTS_WEBIDL_FILES section of the dom/webidl/moz.build file. You don't need to add it to the WEBIDL_FILES section.
  4. Create an instance, and fire the event.

Extending EventTarget

Extending EventTarget is straight forward. Here is an example of the MozContentSearch extending EventTarget. This interface receives "enginechange" events. In addition to extending EvenTarget it is good form to also provide an EventHandler attribute for the correspoding event. You can add multiple attributes, depeding on what events your interface will handle.

interface MozContentSearch : EventTarget {
  attribute EventHandler onenginechange;
}

Specifying the event

In a seperate file, you now need to define the actual event. In the example below, we define "MozSearchEngineChangeEvent". Here is an example of an event that we fire when a user changes their default search engine.

[Constructor(DOMString type, SearchEngineChangeEventInit eventInitDict)]
interface MozSearchEngineChangeEvent : Event {
  readonly attribute MozSearchEngineDetails engine;
};

dictionary SearchEngineChangeEventInit : EventInit {
  required MozSearchEngineDetails engine;
};

Add event to moz.build under GENERATED_EVENTS_WEBIDL_FILES

** Note: This is limited to events that don't have methods. **

As already stated, you need to add your event to the GENERATED_EVENTS_WEBIDL_FILES section of the dom/webidl/moz.build file. You don't need to add it to the WEBIDL_FILES section.

When you mach build, the WebIDL elves will wire everything up for you (and get angry if you screwed something up).

Creating an instance, firing the event

If you made it this far, you are doing great! Now it's time to fire your events. It's super easy, all you need to do is contruct the your event, and call this.__DOM_IMPL__.dispatchEvent(event).

For the EventHandler attributes, you just need to add getters and setters, as show below. Just adapt them to use the name of your event.

Below is an example that shows be basic things you need to fire an event. We are again using MozSearchEngineChange as an example. You can modify the code to meet you needs.

MozContentSearch.prototype = { 
  // Other props.... 
  QueryInterface: XPCOMUtils.generateQI(
    [Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer]
  ),

  init(contentWindow) {
    this._win = contentWindow;
  }
  _fireEngineChangeEvent() {
    // We make sure we create things in the correct security context
    const engine = new this._win.MozSearchEngineDetails({/*init stuff*/})
    const event = new this._win.MozSearchEngineChangeEvent("enginechange", {engine});
    // We now fire the event!
    this.__DOM_IMPL__.dispatchEvent(event);
  },
  
  // Add the attribute getters and setters
  get onenginechange() {
    return this.__DOM_IMPL__.getEventHandler("onenginechange");
  },

  set oneginechange(handler) {
    this.__DOM_IMPL__.setEventHandler("onenginechange", handler);
  },
}

In a web page, you would then use the common addEventListener and EventHandler attribute to recieve events:

function handleEvent(e){
  console.log(e);
}
navigator.whatevs.addEventListener("enginechange", handleEvent);
navigator.whatevs.onenginechange = handleEvent;

Optionality in events

TBW.

Returning promises

TBW.

Implementing map-like and set-like objects

TBW.

Implementing multiple interfaces at once

TBW.

Working with e10s by sending messages

TBW. Needs to cover getting a message manager.

{
  init(contentWindow) {
    this._contentWindow = contentWindow;
    this._MessageManager = contentWindow
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIDocShell)
      .sameTypeRootTreeItem
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIContentFrameMessageManager);
  },
}

More advanced things

You are probably ready to return to the MDN Wiki.

Please consider contributing to this guide with useful recipies for adding JS implemented WebIDL functionality.

Copy link

ghost commented Apr 25, 2019

In Part2, XPCOMUtils.generateQI has to be changed to ChromeUtils.generateQI

@sidvishnoi
Copy link

NavigatorProperty extended attribute is no longer supported with https://bugzilla.mozilla.org/show_bug.cgi?id=921496.
To expose something on navigator, use WebIDL + C++ (example: https://hg.mozilla.org/integration/autoland/rev/ca578c57a4f0).

@marcoscaceres
Copy link
Author

Thanks. I've marked the document as deprecated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment