Working with Multiple Versions of Interfaces

The Problem

In this short note we illustrate how one can update an XPCOM module in order for it to work in both Firefox 2 and Firefox 3, even if the interfaces have changed in the interim.

In the extension that prompted this note, I needed to obtain the HWND of the document (yes its on Windows) in order to identify each particular extension instance. To do this I used the accessibility framework:

HWND getHwnd(nsIDOMNode *node){
  HWND self = NULL;
  nsresult rv;
  nsCOMPtr<nsIAccessibleRetrieval> refp;
  refp = do_CreateInstance( "@mozilla.org/accessibleRetrieval;1", &rv);
  if (NS_FAILED(rv)){ return self; } //line 6.
  nsCOMPtr<nsIAccessible> accnode;
  rv = refp->GetAccessibleFor(node, getter_AddRefs(accnode));
  if(NS_FAILED(rv)){ return self; }
  void *wh = NULL;
  nsCOMPtr<nsIAccessibleDocument> accdocnode;
  accdocnode = do_QueryInterface(accnode, &rv);
  if(NS_FAILED(rv)){ return self; }
  rv = accdocnode->GetWindowHandle(&wh);
  if(NS_SUCCEEDED(rv)){ self = static_cast<HWND>(wh); }
  return self;
}

This approach worked, as is, for versions as early as Firefox 1.5. The problem arises when one tries to run an extension built with the latest SDK in an older version of Firefox, say Firefox 2. What happens is that the call to do_CreateInstance fails with nsresult NS_ERROR_NO_INTERFACE. This is because the call to

do_CreateInstance(aCID, aOuter, error);

will eventually evolve into a request for an object supporting the interface with IID NS_GET_IID(nsIAccessibleRetrieval). Unfortunately we compiled this in the latest SDK, and so this magic number happens to be:

"244e4c67-a1d3-44f2-9cab-cdaa31b68046"

whereas, inside Firefox 2, the IID it happens to know about is:

"663ca4a8-d219-4000-925d-d8f66406b626".


and this explains our NS_ERROR_NO_INTERFACE.

Fortunately there is a plan of action that we can follow to rectify this. We build construct our XPCOM component so that first tries to get the interface by it's new IID, then if that fails, attempts plan B. Plan B is simply trying to get the interface by it's older IID.

For expository purposes we will do this in two stages; buoyed by the fact that it is the actual route we took.

The Optimist's Solution

The first thing we do is replace line 6 above by our plan B:

  if (NS_FAILED(rv)){ return getHwndB(node); } //new line 6.

and then implement plan B. We first dredge out the old interface identifiers from our yea olde Firefox 1.5 SDK:

static const nsIID IAR_IID_OLD =
  { 0x663ca4a8, 0xd219, 0x4000, { 0x92, 0x5d, 0xd8, 0xf6, 0x64, 0x06, 0xb6, 0x26 }};

static const nsIID IAD_IID_OLD =
  {0x8781fc88, 0x355f, 0x4439, { 0x88, 0x1f, 0x65, 0x04, 0xa0, 0xa1, 0xce, 0xb6 }};

then follow the recipe.

HWND getHwndB(nsIDOMNode *node){
  HWND self = NULL;
  nsresult rv;
  nsCOMPtr<nsIComponentManager> compMgr;
  rv = NS_GetComponentManager(getter_AddRefs(compMgr));
  if (NS_FAILED(rv)){ return self; }
  nsCOMPtr<nsIAccessibleRetrieval> refp;
  rv = compMgr->CreateInstanceByContractID(accRetCID, 0, IAR_IID_OLD, getter_AddRefs(refp));
  if (NS_FAILED(rv)){ return  self; }
  nsCOMPtr<nsIAccessible> accnode;
  rv = refp->GetAccessibleFor(node, getter_AddRefs(accnode));
  if(NS_FAILED(rv)){ return self; }
  void *wh = NULL;
  nsCOMPtr<nsIAccessibleDocument> accdocnode;
  rv = accnode->QueryInterface(IAD_IID_OLD, getter_AddRefs(accdocnode));
  if(NS_FAILED(rv)){ return self; }
  rv = accdocnode->GetWindowHandle(&wh);
  if(NS_SUCCEEDED(rv)){ self = static_cast<HWND>(wh); }
  return self;
}


There is good news and bad news. First the good news. Plan B inside Firefox 2 happily runs to completion. All the XPCOM calls succeed, or at least think they succeed. The bad news is that the thing we get back is not a valid HWND.

The problem now is not just interface identifier mismatch, but class declaration mismatch. GetWindowHandle is the tenth method declared in the nsIAccessibleDocument.h that Firefox 2 was built with, but actually the eighth method in the SDK that I used to build my extension (and hence XPCOM component). Since these classes don't use vtables this means I'm probably, no I can be more positive, definitely calling the wrong method. If I were a betting man, I'd hedge a bet on GetAccessibleInParentChain, that being the tenth method in the interface declaration in the new SDK.

In actual fact we were lucky we actually got as far as we did, since the very first interface we made use of has changed substantially. If there is any truth to the story I weave here, then this is because GetAccessibleFor is still the very first method declared in nsIAccessibleRetrieval.h. So if you are lucky enough with your API's this technique may work without further ado. We are not so lucky.


The Realist's Solution

To make sure we call the right methods of the right interfaces we need to have two versions of both the nsIAccessibleDocument and nsIAccessibleRetrieval interfaces at our fingertips. I called my old ones: nsIAccessibleRetrieval_old.h and nsIAccessibleDocument_old.h.

Mine date back to August of 2006, which is when I first built the lizard. To get these four files to co-exist together peacefully I had to resort to some preprocessor magic and an ugly hack. Maybe the ugly hack can be replaced by even more preprocessor magic, but not today. Lets do the magic first, then describe the ugly hack.

To include both retrieval interfaces (and remember the old IID without having to cut and paste) I followed the kind advice of Mike Shaver and did:

#define nsIAccessibleRetrieval nsIAccessibleRetrieval_old
#include "accessibility/nsIAccessibleRetrieval_old.h"
static const nsIID NS_IACCESSIBLERETRIEVAL_IID_OLD = NS_IACCESSIBLERETRIEVAL_IID;
#undef nsIAccessibleRetrieval
#undef __gen_nsIAccessibleRetrieval_h__
#include "accessibility/nsIAccessibleRetrieval.h"

and following the identical principle for the document interface:

#define nsIAccessibleDocument nsIAccessibleDocument_old
#include "accessibility/nsIAccessibleDocument_old.h"
static const nsIID NS_IACCESSIBLEDOCUMENT_IID_OLD = NS_IACCESSIBLEDOCUMENT_IID;
#undef nsIAccessibleDocument
#undef __gen_nsIAccessibleDocument_h__
#include "accessibility/nsIAccessibleDocument.h"

I even silenced my friend the compiler by enclosing both incantations within a compiler pragma:

#pragma warning(push)
#pragma warning(disable : 4005)
...
#pragma warning(pop)

So now I have to own up to the ugly hack. I did have to delve into my old versions and change:

   NS_DEFINE_STATIC_IID_ACCESSOR(...)

to

   NS_DECLARE_STATIC_IID_ACCESSOR(...)

This ugliness aside, my plan B routine now looks like:

HWND getHwndB(nsIDOMNode *node){
  HWND self = NULL;
  nsresult rv;
  nsCOMPtr<nsIComponentManager> compMgr;
  rv = NS_GetComponentManager(getter_AddRefs(compMgr));
  if (NS_FAILED(rv)){ return self; }
  nsCOMPtr<nsIAccessibleRetrieval_old> refp; //N.B. _old
  rv = compMgr->CreateInstanceByContractID(accRetCID, 0,
                                           NS_IACCESSIBLERETRIEVAL_IID_OLD,
                                           getter_AddRefs(refp));
  if (NS_FAILED(rv)){ return  self; }
  nsCOMPtr<nsIAccessible> accnode;
  rv = refp->GetAccessibleFor(node, getter_AddRefs(accnode));
  if(NS_FAILED(rv)){ return self; }
  void *wh = NULL;
  nsCOMPtr<nsIAccessibleDocument_old> accdocnode; //N.B. _old
  rv = accnode->QueryInterface(NS_IACCESSIBLEDOCUMENT_IID_OLD,
                               getter_AddRefs(accdocnode));
  if(NS_FAILED(rv)){ return self; }
  rv = accdocnode->GetWindowHandle(&wh);
  if(NS_SUCCEEDED(rv)){ self = static_cast<HWND>(wh); }
  return self;
}