Modularization Techniques (Rough Draft)
Created Feb 25, 1998 by Will ScullinFeedback to bug 149248
Contents
- Introduction
- The Basics
- A Simple Example
- Moving to a Dynamically Loaded Library
- Reference Counting Basics
- (Soon To Be) Frequently Asked Questions
- Links
- Revision History
Introduction
The purpose of this document is provide all the information you need to create a new Mozilla Module or break existing code into a module. The mechanism we're using is based on the principles laid down by COM, so pretty much anything you know about COM can be applied here, and any reference on COM can provide you with more interesting and complex examples than the ones provided here.
The Basics
Interfaces
The basic building blocks of modules are C++ pure virtual interfaces. A pure virtual interface is simply a class where every method is defined as pure virtual, that is:
virtual int foo(int bar) = 0;
Pure virtual interfaces provide an easy mechanism for passing function tables between modules that may reside in separate, possibly dynamically loaded, libraries. Each interface is assigned a unique Interface Identifier, or IID.
nsISupports
The key interface in our model is the nsISupports
interface, our
equivalent to COM's IUnknown
interface. nsISupports
provides
two key features, interface interrogation and reference counting. Interface
interrogation is a simple, uniform mechanism for determining which interfaces
a object supports, and for hiding the the mechanics of how the object was
implemented.
Interface interrogation is performed using the QueryInterface()
method. The caller passes in an ID and a pointer to a address to place
the resulting interface. If the query is successful, QueryInterface()
will return NS_OK
. If the object does not support the given interface,
it will return NS_NOINTERFACE
.
Reference counting is performed using the AddRef()
and Release()
methods. An objects reference count generally starts at zero. AddRef()
increments that reference count, and Release()
decrements it.
If a call to Release()
causes the reference count to hit zero,
the object will generally free itself. A successful QueryInterface()
will call AddRef()
on the requested interface before returning.
Both AddRef()
and Release()
return the resulting reference
count.
The convenience macros NS_ADDREF()
and NS_RELEASE()
are preferred over calling AddRef
and Release
directly. In debug builds, these macros provide useful reference counting logs. Use them wherever possible.
/* * The nsISupports interface */ class nsISupports { public: NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult) = 0; NS_IMETHOD_(nsrefcnt) AddRef(void) = 0; NS_IMETHOD_(nsrefcnt) Release(void) = 0; };
The NS_IMETHOD
and NS_IMETHOD_(type)
macros are
basically shorthand for virtual nsresult
and virtual type
.
On Windows they expand to virtual nsresult __stdcall
and virtual
type
__stdcall
for COM compatibility reasons. You don't have to use them
in your interfaces unless you're concerned with COM compatibility.
All Mozilla interfaces inherit from nsISupports
. Inheriting
from nsISupports
allows any interface to be interrogated about
other interfaces that its instance may support, and insures that reference
counting facilities are always available. The IID for nsISupports
is defined as NS_ISUPPORTS_IID
.
QueryInterface()
has several important characteristics that
must be maintained. If you perform a QueryInterface()
on interface
A and obtain interface B, you must be able to perform a QueryInterface()
B and obtain interface A. If interfaces A and B are both implemented by
the same instance, performing a QueryInterface()
for nsISupports
on either should return the exact same interface. This means that even
though interface B inherits from nsISupports, performing a QueryInterface()
on it may not return the same interface. This important behavior is the
only reliable mechanism for determining whether interfaces A and B are
implemented by the same object.For simple objects, maintaining these behaviors
is easy. Aggregation, as we will see later, can complicate things.
On the other hand, objects are allowed a certain degree of flexibility
in their implementations of AddRef()
and Release()
. They
can maintain a single reference count for the entire object, or individual
reference counts for each interface. A static object would chose to ignore
reference counts altogether. However, a poor implementation of these functions
can have negative results, such as memory leaks or inadvertent access of
freed objects.
Factories
Factories are special classes dedicated to creating instances of classes.
A Foo class will typically have a FooFactory associated with it. The nsIFactory
interface is the equivalent of COM's IClassFactory
.
/* * The nsIFactory interface */ class nsIFactory: public nsISupports { public: NS_IMETHOD CreateInstance(nsISupports *aOuter, const nsIID &aIID, void **aResult) = 0; NS_IMETHOD LockFactory(PRBool aLock) = 0;
The reason for using factories is that it provides a mechanism for creating an object without having access to the class declaration for that object. Calling new Foo() requires that at compile time you have access to the class declaration of Foo(). A factory allows an implementor to hide both the class declaration and creation details of an object, an extremely important step for allowing maximum flexibility in the implementation of a class and reducing compile time dependencies. It can even be used to eliminate all link time dependencies on the class and its factory entirely.
The Component Manager
One of the major goals of our modularization is to remove link time dependencies.
So how do you find a module if you've never linked with it? We've created
something called the nsComponentManager. The nsComponentManager
is simply a mapping
of class IDs to factories and their containing libraries.
class nsComponentManager { public: // Finds a factory for a specific class ID static nsresult FindFactory(const nsCID &aClass, nsIFactory **aFactory); // Creates a class instance for a specific class ID static nsresult CreateInstance(const nsCID &aClass, const nsIID &aIID, nsISupports *aDelegate, void **aResult); // Manually registry a factory for a class static nsresult RegisterFactory(const nsCID &aClass, nsIFactory *aFactory, PRBool aReplace); // Manually registry a dynamically loaded factory for a class static nsresult RegisterFactory(const nsCID &aClass, const char *aLibrary, PRBool aReplace, PRBool aPersist); // Manually unregister a factory for a class static nsresult UnregisterFactory(const nsCID &aClass, nsIFactory *aFactory); // Manually unregister a dynamically loaded factory for a class static nsresult UnregisterFactory(const nsCID &aClass, const char *aLibrary); // Unload dynamically loaded factories that are not in use static nsresult FreeLibraries(); };
There are several ways a factory can make its way into the repository.
The most direct is through RegisterFactory()
. RegisterFactory()
supports two different registration mechanisms. The first takes a class
ID and a pointer to a factory. This mechanism can be used on factories
that are linked into the executable. The second takes a class ID and the
path to a dynamically loadable library. This mechanism can be used both
inside an executable at run-time and externally using the aPersist
flag to tell the repository to store the class ID/library relationship
in its permenant store.
About nsIIDs and nsCIDs
To simplify the process of dynamically finding, loading and binding interfaces, all classes and interfaces are assigned unique IDs. The IDs are unique 128 bit numbers that are based on UUIDs. For those who like gory details, their structure is this:
struct nsID { PRUint32 m0; PRUint16 m1, m2; PRUint8 m3[8]; };
Frequently you see them represented as strings, like this:
{221ffe10-ae3c-11d1-b66c-00805f8a2676}
To initialize an ID struct you declare them like this:
ID = {0x221ffe10, 0xae3c, 0x11d1, {0xb6, 0x6c, 0x00, 0x80, 0x5f, 0x8a, 0x26, 0x76}};
Why the b66c
couplet gets broken up and grouped with the last
set of bytes is probably a footnote somewhere. On Windows you can use the
programs uuidgen
and guidgen
, which ship with Visual
C++, to generate IDs.
A Simple Example
It is recommended that you use XPIDL to define your interfaces. This sample code should be updated to reflect this, but it gives you a good basic understanding of COM from the C++ perspective.
File nsISample.h
nsISample.h
defines an extremely simple interface and its interface ID
(IID). The important things to notice are that the interface inherits from
nsISupports, and all member functions are pure virtual methods.
#include "nsISupports.h" // {57ecad90-ae1a-11d1-b66c-00805f8a2676} #define NS_ISAMPLE_IID \ {0x57ecad90, 0xae1a, 0x11d1, \ {0xb6, 0x6c, 0x00, 0x80, 0x5f, 0x8a, 0x26, 0x76}} /* * nsISample Interface declaration */ class nsISample: public nsISupports { public: NS_IMETHOD Hello() = 0; };
File nsSample.h
nsSample.h
defines the class ID (CID) for our sample class. Note that
one interface can have a number of classes that implement it, so there
is not necessarily a one-to-one mapping from IIDs to CIDs. It also defines
the function for retrieving our class factory. Notice it does not contain
a class declaration.
#include "nsIFactory.h" // {d3944dd0-ae1a-11d1-b66c-00805f8a2676} #define NS_SAMPLE_CID \ {0xd3944dd0, 0xae1a, 0x11d1, \ {0xb6, 0x6c, 0x00, 0x80, 0x5f, 0x8a, 0x26, 0x76}} extern nsresult GetSampleFactory(nsIFactory **aResult);
File nsSample.cpp
nsSample.cpp
contains both the declaration and implementation of our
sample class, and the declaration and implementation of our class factory.
#include "nsISample.h" #include "nsSample.h" static NS_DEFINE_IID(kISupportsIID, NS_ISUPPORTS_IID); static NS_DEFINE_IID(kIFactoryIID, NS_IFACTORY_IID); static NS_DEFINE_IID(kISampleIID, NS_ISAMPLE_IID); static NS_DEFINE_CID(kISampleCID, NS_ISAMPLE_CID); /* * nsSampleClass Declaration */ class nsSample: public nsISample { private: nsrefcnt mRefCnt; public: // Constructor and Destuctor nsSample(); ~nsSample(); // nsISupports methods NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult); NS_IMETHOD_(nsrefcnt) AddRef(void); NS_IMETHOD_(nsrefcnt) Release(void); // nsISample method NS_IMETHOD Hello(); }; /* * nsSampleFactory Declaration */ class nsSampleFactory: public nsIFactory { private: nsrefcnt mRefCnt; public: nsSampleFactory(); ~nsSampleFactory(); // nsISupports methods NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult); NS_IMETHOD_(nsrefcnt) AddRef(void); NS_IMETHOD_(nsrefcnt) Release(void); // nsIFactory methods NS_IMETHOD CreateInstance(nsISupports *aOuter, const nsIID &aIID, void **aResult); NS_IMETHOD_(void) LockFactory(PRBool aLock); }; /* * nsSample Implementation */ nsSample::nsSample() { mRefCnt = 0; } nsSample::~nsSample() { assert(mRefCnt == 0); } NS_IMETHOD nsSample::QueryInterface(const nsIID &aIID, void **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } // Always NULL result, in case of failure *aResult = NULL; if (aIID.Equals(kISupportsIID)) { *aResult = (void *) this; } else if (aIID.Equals(kISampleIID)) { *aResult = (void *) this; } if (aResult != NULL) { return NS_ERROR_NO_INTERFACE; } AddRef(); return NS_OK; } nsRefCount nsSample::AddRef() { return ++mRefCnt; } nsRefCount nsSample::Release() { if (--mRefCnt == 0) { delete this; return 0; // Don't access mRefCnt after deleting! } return mRefCnt; } /* * nsSampleFactory Implementation */ nsSampleFactory::nsSampleFactory() { mRefCnt = 0; } nsSampleFactory::~nsSampleFactory() { assert(mRefCnt == 0); } NS_IMETHODIMP nsSampleFactory::QueryInterface(const nsIID &aIID, void **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } // Always NULL result, in case of failure *aResult = NULL; if (aIID.Equals(kISupportsIID)) { *aResult = (void *) this; } else if (aIID.Equals(kIFactoryIID)) { *aResult = (void *) this; } if (*aResult == NULL) { return NS_ERROR_NO_INTERFACE; } AddRef(); // Increase reference count for caller return NS_OK; } NS_IMETHODIMP(nsRefCount) nsSampleFactory::AddRef() { return ++mRefCnt; } NS_IMETHODIMP(nsRefCount) nsSampleFactory::Release() { if (--mRefCnt == 0) { delete this; return 0; // Don't access mRefCnt after deleting! } return mRefCnt; } NS_IMETHODIMP nsSampleFactory::CreateInstance(nsISupports *aOuter, const nsIID &aIID, void **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } *aResult = NULL; nsISupports inst = new nsSample(); if (inst == NULL) { return NS_ERROR_OUT_OF_MEMORY; } nsresult res = inst->QueryInterface(aIID, aResult); if (res != NS_OK) { // We didn't get the right interface, so clean up delete inst; } return res; } void nsSampleFactory::LockFactory(PRBool aLock) { // Not implemented in simplest case. } nsresult GetSampleFactory(nsIFactory **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } *aResult = NULL; nsISupports inst = new nsSampleFactory(); if (inst == NULL) { return NS_ERROR_OUT_OF_MEMORY; } nsresult res = inst->QueryInterface(kIFactoryIID, aResult); if (res != NS_OK) { // We didn't get the right interface, so clean up delete inst; } return res; }
File main.cpp
main.cpp
is a simple program that creates an instance of our sample
class and disposes of it. Because it obtains the class factory directly,
it doesn't use the CID for class.
#include "nsISample.h" #include "nsSample.h" static NS_DEFINE_IID(kISampleIID, NS_ISAMPLE_IID); int main(int argc, char *argv[]) { nsIFactory *factory; GetSampleFactory(&factory); nsISample *sample; nsresult res = factory->CreateInstance(NULL, kISampleIID, (void **) &sample); if (res == NS_OK) { sample->Hello(); NS_RELEASE(sample); } return 0; }
Moving to a Dynamically Linked Library
Implementing a DLL
Once you've set a factory, moving it to a DLL is a relatively trivial thing. A DLL that contains a factory need to define one or two exported functions:
// Returns the factory associated with the given class ID extern "C" NS_EXPORT nsresult NSGetFactory(const nsCID &aCID, nsIFactory **aFactory); // Returns whether the DLL can be unloaded now. extern "C" NS_EXPORT PRBool NSCanUnload();
The implementation of NSGetFactory()
in the simplest case is nearly
identical to that of GetSampleFactory()
in our previous example.
You only need to verify that the class ID passed in is the correct one
for the factory you implement. If your DLL contains multiple factories,
you'll need to add conditional code to determine which one to return.
NSCanUnload()
is an optional, but useful function. If implemented,
it allows the NSRepository to free up memory by unloading DLLs it is no
longer using when FreeLibraries()
is called. The implementation
takes into consideration two things when deciding whether or not a DLL
can be unloaded: Whether any of its factories are currently in use, and
whether anyone has locked the server. If NSCanUnload()
is not
implemented, the DLL will not be unloaded.
The following example turns nsSample.cpp
into a file that can
be compiled into a DLL. The differences are marked with strong emphasis.
There really aren't that many.
File nsSample3.cpp
#include <iostream.h> #include "pratom.h" #include "nsRepository.h" #include "nsISample.h" #include "nsSample.h" static NS_DEFINE_IID(kISupportsIID, NS_ISUPPORTS_IID); static NS_DEFINE_IID(kIFactoryIID, NS_IFACTORY_IID); static NS_DEFINE_IID(kISampleIID, NS_ISAMPLE_IID); static NS_DEFINE_CID(kSampleCID, NS_SAMPLE_CID); /* * Globals */ static PRInt32 gLockCnt = 0; static PRInt32 gInstanceCnt = 0; /* * nsSampleClass Declaration */ class nsSample: public nsISample { private: nsrefcnt mRefCnt; public: // Constructor and Destuctor nsSample(); ~nsSample(); // nsISupports methods NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult); NS_IMETHOD_(nsrefcnt) AddRef(void); NS_IMETHOD_(nsrefcnt) Release(void); // nsISample method NS_IMETHOD Hello(); }; /* * nsSampleFactory Declaration */ class nsSampleFactory: public nsIFactory { private: nsrefcnt mRefCnt; public: nsSampleFactory(); ~nsSampleFactory(); // nsISupports methods NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult); NS_IMETHOD_(nsrefcnt) AddRef(void); NS_IMETHOD_(nsrefcnt) Release(void); // nsIFactory methods NS_IMETHOD CreateInstance(nsISupports *aOuter, const nsIID &aIID, void **aResult); NS_IMETHOD_(void) LockFactory(PRBool aLock); }; /* * nsSample Implemtation */ nsSample::nsSample() { mRefCnt = 0; PR_AtomicIncrement(&gInstanceCnt); } nsSample::~nsSample() { // assert(mRefCnt == 0); PR_AtomicDecrement(&gInstanceCnt); } NS_IMETHODIMP nsSample::Hello() { cout << "Hello, world\n"; return NS_OK; } NS_IMETHODIMP nsSample::QueryInterface(const nsIID &aIID, void **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } // Always NULL result, in case of failure *aResult = NULL; if (aIID.Equals(kISupportsIID)) { *aResult = (void *) this; } else if (aIID.Equals(kISampleIID)) { *aResult = (void *) this; } if (aResult != NULL) { return NS_NOINTERFACE; } AddRef(); return NS_OK; } NS_IMETHODIMP nsSample::AddRef() { return ++mRefCnt; } NS_IMETHODIMP nsSample::Release() { if (--mRefCnt == 0) { delete this; return 0; // Don't access mRefCnt after deleting! } return mRefCnt; } /* * nsSampleFactory Implementation */ nsSampleFactory::nsSampleFactory() { mRefCnt = 0; PR_AtomicIncrement(&gInstanceCnt); } nsSampleFactory::~nsSampleFactory() { // assert(mRefCnt == 0); PR_AtomicDecrement(&gInstanceCnt); } NS_IMETHODIMP nsSampleFactory::QueryInterface(const nsIID &aIID, void **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } // Always NULL result, in case of failure *aResult = NULL; if (aIID.Equals(kISupportsIID)) { *aResult = (void *) this; } else if (aIID.Equals(kIFactoryIID)) { *aResult = (void *) this; } if (*aResult == NULL) { return NS_NOINTERFACE; } AddRef(); // Increase reference count for caller return NS_OK; } NS_IMETHODIMP_(nsrefcnt) nsSampleFactory::AddRef() { return ++mRefCnt; } NS_IMETHODIMP_(nsrefcnt) nsSampleFactory::Release() { if (--mRefCnt == 0) { delete this; return 0; // Don't access mRefCnt after deleting! } return mRefCnt; } NS_IMETHODIMP nsSampleFactory::CreateInstance(nsISupports *aOuter, const nsIID &aIID, void **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } *aResult = NULL; nsISupports *inst = new nsSample(); if (inst == NULL) { return NS_ERROR_OUT_OF_MEMORY; } nsresult res = inst->QueryInterface(aIID, aResult); if (res != NS_OK) { // We didn't get the right interface, so clean up delete inst; } return res; } /* * Exported functions */ void nsSampleFactory::LockFactory(PRBool aLock) { if (aLock) { PR_AtomicIncrement(&gLockCnt); } else { PR_AtomicDecrement(&gLockCnt); } } extern "C" NS_EXPORT nsresult NSGetFactory(const nsCID &aCID, nsIFactory **aResult) { if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } *aResult = NULL; nsISupports *inst; if (aCID.Equals(kSampleCID)) { inst = new nsSampleFactory(); } else { return NS_ERROR_ILLEGAL_VALUE; } if (inst == NULL) { return NS_ERROR_OUT_OF_MEMORY; } nsresult res = inst->QueryInterface(kIFactoryIID, (void **) aResult); if (res != NS_OK) { // We didn't get the right interface, so clean up delete inst; } return res; } extern "C" NS_EXPORT PRBool NSCanUnload() { return PRBool(gInstanceCnt == 0 && gLockCnt == 0); }
Now, instead of directly calling the factory, we call NSRepository::CreateInstance()
.
We rely on the factory registering itself somehow.
File: main2.cpp
#include "nsRepository.h" #include "nsISample.h" #include "nsSample.h" static NS_DEFINE_IID(kISampleIID, NS_ISAMPLE_IID); static NS_DEFINE_CID(kSampleCID, NS_SAMPLE_CID); int main(int argc, char *argv[]) { nsISample *sample; nsresult res = NSRepository::CreateInstance(kSampleCID, NULL, kISampleIID, (void **) &sample); if (res == NS_OK) { sample->Hello(); NS_RELEASE(sample); } return 0; }
Registering a DLL
This is currently being hashed out. You can currently manually register
a DLL using the NSRepository's RegisterFactory()
method (For an
example of this, see nsSample2.cpp
).
A DLL can export two additional functions for self registration and unregistration.
extern "C" NS_EXPORT nsresult NSRegisterSelf(const char *path); extern "C" NS_EXPORT nsresult NSUnregisterSelf(const char *path);
This allows a DLL to register and unregister all its factories. A simple program called RegFactory.exe (on Windows) or regfactory (on UNIX) can be used to register self-registering DLLs.
Reference Counting Basics
Reference counting is a critical part of modularity picture. There are a number of basic reference counting rules to remember. Here's a quick summary.
Out Parameters
Functions that return a new interface should call AddRef() on that interface before returning it.
nsresult GetFoo(IFoo **aFooRes) { if (aFooRes == NULL) { return NS_ERROR_NULL_POINTER; } *aFooRes = mFoo; NS_ADDREF(*aFooRes); return NS_OK; }
Remember that this applies to the interfaces returned by QueryInterface()
,
CreateInstance()
and NS_NewX()
, and you must call
Release()
on them when you are done to avoid memory leaks.
In Parameters and Local Pointers
Interfaces that are passed in to a function and local copies of that interface
pointer are assumed to be in the lifetime of the calling function, and
do not need to have AddRef()
called.
nsresult TweekFoo(IFoo *aFoo1, IFoo *aFoo2) { IFoo local = aFoo; if (aFoo->Bar() == NS_OK) { local = aFoo2; } return local->Boff(); }
In-Out Parameters
In-Out parameters are used as both In parameters and Out parameters. If
a function changes the value of an interface In-Out parameter, it should
call Release()
on the interface passed in and AddRef()
on the interface passed out.
nsresult RefreshFoo(IFoo **aFoo) { if (aFoo == NULL || *aFoo == NULL) { return NS_ERROR_NULL_PARAMETER; } if ((*aFoo)->Stale()) { NS_RELEASE(*aFoo); *aFoo = mFoo; NS_ADDREF(*aFoo); } return NS_OK; }
Global and Member Variables
Both global and member variables have lifetimes that can be changed by
any number of functions. Therefore one should call AddRef()
on
any global or member variable that is being passed to a function, and call
Release()
afterward.
NS_ADDREF(mFoo); TweekFoo(mFoo); NS_RELEASE(mFoo);
(Soon to be) Frequently Asked Questions
- Why are we mimicking COM? Doesn't COM suck?
- You're probably basing this opinion on your experiences with or stories you've heard about OLE. A really important thing to remember is that COM is not OLE. OLE was built one top of COM, but it's not a shining example of COM. COM is simply a mechanism for laying out and using interfaces, the important components we've pretty much described here. OLE (actually OLE 2) was one of the first efforts to use COM.
- Why C++?
- C++ provides the easiest mechanism for implementing interfaces. You can manually assemble interfaces using function tables and macros, but you'd be simply doing by hand what a C++ compiler can do for you automatically.
- Can I use C?
- You can use C everywhere except your interface. There are mechanisms for declaring interfaces in C, but they're pretty gruesome and compiler dependent, and we're trying to make this as light weight as possible.
- Why not COM?
- The only platform for which COM support is currently widely available is Windows. Microsoft ships a COM extension for the Macintosh, but it's generally only installed with Internet Explorer or Microsoft Office. UNIX support for COM is scarce.
- Why not COM on Windows?
- Because it's not a cross platform solution, and that's what we need. We're going to make every effort to make our interfaces compatible with COM on platforms that support it, so it may not matter. But no promises, yet.
- What are the major differences?
-
Instead of Microsoft's MIDL compiler, we are using a CORBA-compliant IDL compiler, XPIDL. It outputs NSPR types when generating C++ headers. It also generates typelibraries that are not compatible with Microsoft's .TLB format. XPCOM uses these typelibraries to allow other languages, such as JavaScript, to implement and call XPCOM objects. We also do cross-thread proxying calls using the typelib and NSPR's event queues.
Microsoft provides an extensive support infrastructure for COM. This technology is built into Windows, but not most other platforms. The technology can be licensed from Microsoft, but for obvious reasons we are not going to be doing that. In house equivalents to the important elements of this technology will be developed as needed.
Links
Revision History
- Feb 25, 1998, Created
- Oct 19, 1998, Dusted off momentarily
- Oct 10, 1999, Added comments about XPIDL, language-independantness