Tuesday, August 6, 2013

exposing XPCOM C++ interface on a DOM

So there was a need for me to write a C++ add-on for Firefox and make it possible to use it's functionality from the webpage Javascript code.
Firefox provides XPCOM object model which can be employed by add-ons. Unfortunate to me, the majority of information is sparse and highly out-dated and this post is dedicated to all this pain developers like me experience trying to understand and assemble these pieces of information together. One should also note, that XPCOM is rapidly changing interface and with time passing, the code from this post can no longer be a working example.

The main problem was the privileges - one can't simply execute XPCOM C++ code from the webpage.
I've tried numerous different approaches which failed to work, unless all the new knowledge in my head produced one idea which appeared to work as I expected.


In few words, the add-on is organized like this. There is your XPCOM C++ component which is registered  in Firefox (you can follow this book, which I found very helpful, though out-dated; also, check example from the source code) and there is a XPCOM Javascript component which is also registered in Firefox, but acts as a wrapper or proxy for your C++ object. At the same moment, Javascript component implements nsIDOMGlobalPropertyInitializer which allows it to be exposed and accessed on a DOM window object like this:
window.yourJSWrapper.doSomething(params);

Now let's run through the code briefly.

XPCOM C++

I'll not dive into details assuming you know basics how to create XPCOM add-ons. We define an interface in .idl file like this:

ndINrtc.idl:
#include "nsISupports.idl"
[scriptable, uuid(86AD66E9-B3F7-4E6A-BEEE-802B1D9FF442)]
interface INrtc : nsISupports
{
  long Test(in long a, in long b);
};
We just defined a new interface INrtc which inherits nsISupports interface and has one method Test with two parameters. We also defined an ID for our component (use uuidgen on MacOS) which is required.
Having that, run header.py for generating header file and typelib.py for generating .xpt interface file. These scripts are part of Gecko SDK. You can also build it by yourself from Mozilla source code. 

ndINrtc.h:
/*
 * DO NOT EDIT.  THIS FILE IS GENERATED FROM /Users/gpeetonn/Documents/code/CCN/ndnrtc/cpp/idl/ndINrtc.idl
 */

#ifndef __gen_ndINrtc_h__
#define __gen_ndINrtc_h__


#ifndef __gen_nsISupports_h__
#include "nsISupports.h"
#endif

/* For IDL files that don't want to include root IDL files. */
#ifndef NS_NO_VTABLE
#define NS_NO_VTABLE
#endif

/* starting interface:    INrtc */
#define INRTC_IID_STR "86ad66e9-b3f7-4e6a-beee-802b1d9ff442"

#define INRTC_IID \
  {0x86ad66e9, 0xb3f7, 0x4e6a, \
    { 0xbe, 0xee, 0x80, 0x2b, 0x1d, 0x9f, 0xf4, 0x42 }}

class NS_NO_VTABLE INrtc : public nsISupports {
 public: 

  NS_DECLARE_STATIC_IID_ACCESSOR(INRTC_IID)

  /* long Test (in long a, in long b); */
  NS_IMETHOD Test(int32_t a, int32_t b, int32_t *_retval) = 0;

};

  NS_DEFINE_STATIC_IID_ACCESSOR(INrtc, INRTC_IID)

/* Use this macro when declaring classes that implement this interface. */
#define NS_DECL_INRTC \
  NS_IMETHOD Test(int32_t a, int32_t b, int32_t *_retval); 

/* Use this macro to declare functions that forward the behavior of this interface to another object. */
#define NS_FORWARD_INRTC(_to) \
  NS_IMETHOD Test(int32_t a, int32_t b, int32_t *_retval) { return _to Test(a, b, _retval); } 

/* Use this macro to declare functions that forward the behavior of this interface to another object in a safe way. */
#define NS_FORWARD_SAFE_INRTC(_to) \
  NS_IMETHOD Test(int32_t a, int32_t b, int32_t *_retval) { return !_to ? NS_ERROR_NULL_POINTER : _to->Test(a, b, _retval); } 

#if 0
/* Use the code below as a template for the implementation class for this interface. */

/* Header file */
class _MYCLASS_ : public INrtc
{
public:
  NS_DECL_ISUPPORTS
  NS_DECL_INRTC

  _MYCLASS_();

private:
  ~_MYCLASS_();

protected:
  /* additional members */
};

/* Implementation file */
NS_IMPL_ISUPPORTS1(_MYCLASS_, INrtc)

_MYCLASS_::_MYCLASS_()
{
  /* member initializers and constructor code */
}

_MYCLASS_::~_MYCLASS_()
{
  /* destructor code */
}

/* long Test (in long a, in long b); */
NS_IMETHODIMP _MYCLASS_::Test(int32_t a, int32_t b, int32_t *_retval)
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

/* End of implementation class template. */
#endif


#endif /* __gen_ndINrtc_h__ */
Well, this file is full of different XPCOM-specific macro and you've noticed
#if 0 directive after which template code for our implementation appears. So here they are:

ndNrtc.h:
#include "ndINrtc.h"

// These macros are used in ndnNrtModule.cpp
#define NRTC_CLASSNAME  "NDN WebRTC Main Class"
#define NRTC_CONTRACTID "@named-data.net/ndnrtc;1"
// "CD232E0F-A777-41A3-BB19-CF415B98088E"
#define NRTC_CID \
  {0xcd232e0f, 0xa777, 0x41a3, \
    { 0xbb, 0x19, 0xcf, 0x41, 0x5b, 0x98, 0x08, 0x8e }}
  
/** 
 * Class description goes here
 */  
class ndNrtc : public INrtc
{
public:
  NS_DECL_ISUPPORTS
  NS_DECL_INRTC

  ndNrtc();

private:
  ~ndNrtc();

protected:
  /* additional members */
};
ndNrtc.cpp:
#include "ndNrtc.h"
NS_IMPL_ISUPPORTS1(ndNrtc, INrtc)

ndNrtc::ndNrtc()
{
  /* member initializers and constructor code */
}

ndNrtc::~ndNrtc()
{
  /* destructor code */
}

/* long Add (in long a, in long b); */
NS_IMETHODIMP ndNrtc::Test(int32_t a, int32_t b, int32_t *_retval)
{
    *_retval = a+b;
    return NS_OK;
}
So, what does it do is just adding two numbers. There are couple of macro in header and implementation files, all of them are XPCOM-specific and help to implement necessary functions of nsISupports interface.
Finally, you also need to implement module class for your component which will be responsible for creating new instances.

ndNrtcModule:
#include "mozilla/ModuleUtils.h"
#include "nsIClassInfoImpl.h"
#include "ndNrtc.h"

NS_GENERIC_FACTORY_CONSTRUCTOR(ndNrtc)
NS_DEFINE_NAMED_CID(NRTC_CID);

static const mozilla::Module::CIDEntry kNrtcCIDs[] = {
    { &kNRTC_CID, false, NULL, ndNrtcConstructor},
    { NULL }
};

static const mozilla::Module::ContractIDEntry kNrtcContracts[] = {
    { NRTC_CONTRACTID, &kNRTC_CID },
    { NULL }
};

static const mozilla::Module::CategoryEntry kNrtcCategories[] = {
    { NULL }
};

static const mozilla::Module kNrtcModule = {
    mozilla::Module::kVersion,
    kNrtcCIDs,
    kNrtcContracts,
    kNrtcCategories
};

NSMODULE_DEFN(ndNrtcModule) = &kNrtcModule;
XPCOM book is out-dated on this info, but you can refer to source code which is more up-to-date.
Now you can build your code into dynamic library (.dylib for MacOS).
In order to check your component is working, you need to archive .xpt and library file into .xpi package, which is recognized by Firefox as an add-on. But, before doing this, you need two additional files: 
  • chrome.manifest which tells Firefox what's inside your add-on
  • install.rdf  which tells Firefox general information about your add-on

chrome.manifest:
resource ndnrtc chrome/content/
content ndnrtc chrome/content/
interfaces components/ndINrtc.xpt
binary-component components/libndnrtc.dylib ABI=Darwin_x86_64-gcc3
Frist line tells Firefox where to look for files in case of "resource://" URI scheme (we'll see later). Second line tells Firefox where to look for the content of add-on so it can be accessible as "chrome://ndnrtc/content/<filename>".
Finally, interface file is declared in "interfaces" line and binary library in "binary-component".

install.rdf:
<?xml version="1.0"?>

<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
    <em:id>ndnrtc@named-data.net</em:id>
    <em:version>0.1</em:version>
    <em:unpack>true</em:unpack>    
    <em:type>2</em:type>
   
    <!-- Target Application this extension can install into, 
         with minimum and maximum supported versions. --> 
    <em:targetApplication>
      <Description>
        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
        <em:minVersion>4.5</em:minVersion>
        <em:maxVersion>99.*</em:maxVersion>
      </Description>
    </em:targetApplication>
   
    <!-- Front End MetaData -->
    <em:name>NDN RTC</em:name>
    <em:description>NDN WebRTC add-on</em:description>
    <em:creator>Peter Gusev</em:creator>
    <em:homepageURL>https://named-data.net</em:homepageURL>
  </Description>      
</RDF>
Here one should note "<em:unpack>" node which should be enabled for binary components.
Now you can compress everything into .xpi add-on package (which is actually a zip archive with changed extension). I've followed this structure for archive internals:

  • chrome/
    • content/ -- place any add-on resources here
  • components/ -- place binary components and interface files here
    • libnrtc.dylib
    • ndINrtc.xpt
  • install.rdf
  • chrome.manifest
Archive it, rename it and open it with Firefox. It should ask for restart after installing the component. You can ensure it's installed by examining XPCOM regisrty - I used XPCOM Viewer addon for that.  
But once installed, how to use our component from a webpage? For now, you can't access it =( You can access it through the web console though. Open a new empty tab, open Javascript console and type:
Components.classes["@named-data.net/ndnrtc;1"].createInstance().QueryInterface(Components.interfaces.INrtc).Test(1,2)
which is equivalent for Javascript code:
var xpcomHelper = Components.classes["@named-data.net/ndnrtc;1"].createInstance();
var obj = xpcomHelper.QueryInterface(Components.interfaces.INrtc);
obj.Test(1,2);
The code above cannot be executed on a webpage due to security reason. So let's put it inside the add-on javascript code!

XPCOM Javascript wrapper

Javascript code is quite simple:
wrapper.js:
let Cu = Components.utils;
let Ci = Components.interfaces;
let Cc = Components.classes;
let Cr = Components.results;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");

function NDNWebRTC() {}

NDNWebRTC.prototype = {
    
classID: Components.ID("{81BAE33D-C6B4-4BAC-A612-996C8BCC26C3}"),
    
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
    
init: function(aWindow) {
    let self = this;
    let api = {
    Test: self.Test.bind(self),
  
    __exposedProps__: {
        Test: "r"
        }
    };
    return api;
},
 
Test: function(a, b){
    var ndnrtc = ndnrtc = Cc["@named-data.net/ndnrtc;1"].createInstance();
    
    ndnrtc = ndnrtc.QueryInterface(Ci.INrtc);
    
    return ndnrtc.Test(a,b);
}
};

var NSGetFactory = XPCOMUtils.generateNSGetFactory([NDNWebRTC]);
As you can see, we implement nsIDOMGlobalPropertyInitializer interface which actually allows exposing Javascript object interface on the DOM of a webpage. Exposed functions are defined in __exposedProps__ dictionary with specifies rights (read, write, read/write). Look here for more information. We create an instance of our C++ XPCOM object by querying interface like this:
var ndnrtc = ndnrtc = Cc["@named-data.net/ndnrtc;1"].createInstance();
ndnrtc = ndnrtc.QueryInterface(Ci.INrtc);
Now you can access any method defined in your .idl file and return results on upper level.

Finally, we should explain Firefox, which attribute it should add to the DOM. We do this in additional manifest file:
wrapper.manifest:
component {81BAE33D-C6B4-4BAC-A612-996C8BCC26C3} wrapper.js
contract @named-data.net/ndnwrapper;1 {81BAE33D-C6B4-4BAC-A612-996C8BCC26C3}
category JavaScript-global-property nrtcObject @named-data.net/ndnwrapper;1
Here we ask Firefox to add global property nrtcObject to DOM and it can be accessible in this manner from webpage Javascript code: window.nrtcObject.Test(1,2); 


Assembling together

In order to reveal new manifest for Firefox, add new line to chrome.manifest file, so it looks like this:
resource ndnrtc chrome/content/
content ndnrtc chrome/content/
manifest components/wrapper.manifest
interfaces components/ndINrtc.xpt
binary-component components/libndnrtc.dylib ABI=Darwin_x86_64-gcc3
Finally, add Javascript files to your add-on and place them inside "components/" directory. The whole final structure looks like this:
  • chrome/
    • content/ -- place any add-on resources here
  • components/ -- place binary components and interface files here
    • libnrtc.dylib
    • ndINrtc.xpt
    • wrapper.js
    • wrapper.manifest
  • install.rdf
  • chrome.manifest
After installing, you'll be able to access your C++ code from the web-page by calling window.nrtcObject's methods.

Hope this was helpful! 

1 comment:

  1. i tried this example but on windows vc++ getting error

    Error 1 error MIDL2025: syntax error : expecting an interface name or DispatchInterfaceName or CoclassName or ModuleName or LibraryName or ContractName or a type specification near "%" c:\...\inrtc\xulrunner-sdk\idl\nsrootidl.idl 10 1 npRtc


    ReplyDelete