Thursday, August 06, 2009

inheriting XPCOM across languages

I've been working on an Add-On for Firefox 3.* recently, and came across a situation where I wanted to do a little XPCOM component inheritance. Basically, there's an HTTPProtocolHandler in Firefox that is used in a variety of places, mainly in the creation of URIs and connections through HTTP. I wanted to modify the HTTP Protocol Handler so that it would get to "filter" each HTTP URI before a connection is created, and then maybe upgrade it to HTTPS if necessary (ForceTLS: see the AMO listing, my site, and the blog entry).

Anyhow, since there can be only one HTTP protocol handler, I have to somehow modify it, and since it's written in C++, I basically have to write my own from scratch to deploy it in an add-on.

But wait, there's got to be an easy way. Here's a thought: create a really basic component, capture a reference to the existing HTTP protocol handler, register the new one as the HTTP protocol handler, and for all method calls and property accesses on my handler, delegate back to the original protocol handler. In js-pseudocode:

myHandler.aPropertyAccessed = function(propName, context) {
if(typeof this[propName] === 'undefined')
return oldHandler[propName];
return this[propName];
};

myHandler.aFunctionCalled = function(fname, args) {
if(typeof this[fname] === 'function')
return this[fname].apply(fname, args);
return gOldHandler.functionCalled(fname, args, gOldHandler);
}


But of course it's not that easy because there is no propertyGetter or functionCalled general methods (like in python). So instead, I had to take to playing with prototypes, aided of course by my JS guru Ben:

// "@mozilla.org/network/protocol;1?name=http"
var kCID = "{4f47e42e-4d23-4dd3-bfda-eb29255e9ea3}";
var gOldHandler = Components.classesByID[kCID]
.getService(Ci.nsIHttpProtocolHandler);

function MyHandler() {}
MyHandler.prototype = {
//custom methods and overridden stuff here
}
MyHandler.prototype.__proto__ = gOldHandler;


But this didn't work because of XPCOM and QueryInterface: the JS object oldHandler may have other interfaces it supports, but the appropriate methods aren't in the JS instance. So I have to do something a bit more elaborate:


// Given two instances, copy in all properties from "super"
// and create forwarding methods for all functions.
function inheritCurrentInterface(self, super) {
for(let prop in super) {
if(typeof self[prop] === 'undefined')
if(typeof super[prop] === 'function') {
(function(prop) {
self[prop] = function() {
return super[prop].apply(super,arguments);
};
})(prop);
}
else
self[prop] = super[prop];
}
}

function MyHandler() {
//grab initial methods (nsIHttpProtocolhandler)
inheritCurrentInterface(this, gOldHandler);
}

MyHandler.prototype = {
QueryInterface:
function(aIID) {
gOldHandler.QueryInterface(aIID);
inheritCurrentInterface(this, gOldHandler);
return this;
},

newURI:
function(spec, originCharset, baseURI) {
var uri = gOldHandler.newURI.apply(gOldHandler, arguments);
//... do my stuff here ...
return uri;
}
};


Essentially, I have to import the functions and variables from the old HTTP protocol handler, and every time my instance (which is replacing the old protocol handler) is QI'ed, I have to QI the old one and re-import all its properties. This is because the old handler was also an nsIObserver and who knows what else.

I implemented my own newURI method by wrapping the one in the old handler and manipulating the URI that comes out of it. Because this is manually defined, it won't be shadowed by functions imported by the inheritCurrentInterface() calls.

The only lingering XPCOM question I've got is what to do with getInterface(). I think because of the inheritCurrentInterface() implementation, getInterface will get imported with the appropriate functionality when it's needed, but I'm not sure.

So I guess the next step is to try and figure out how to provide a JS library that makes this all a lot easier. I'd like some syntax like:

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

var MyService = XPCOMUtils.extendService(kCOMPONENT_CLASS_ID);
MyService.constructor = function(foo) {
//do something with foo
};
MyService.prototype = {
//override methods here
componentMethod: function(a, b, c) {
super.componentMethod(a, b, c);
},
};

// then the rest of XPCOMUtils init stuff...


That syntax may not be workable, but something like it would be nice.

2 comments:

Anonymous said...

Incidentally, we just implemented something similar for overriding the form fill and auto-complete controller: http://hg.mozilla.org/labs/people/file/tip/components/nsFormFillOverride.js :)

Anonymous said...

Rather cool blog you've got here. Thank you for it. I like such themes and anything connected to them. BTW, why don't you change design :).