XPConnect interface plans
This is a quick and dirty overview of some of the interfaces and the internal mechanisms I am working on for XPConnect to support COM identity and maximize sharing of objects. Much handwaving remains...
Note that what is elsewhere called JS2CPPProxy
is here called
WrappedNative
and what is elsewhere called CPP2JSProxy
is
here called WrappedJS
.
The InterfaceInfo subsystem
[Scott Furman is looking closer at representing interface data on his own. These are just my ideas]
nsIInterfaceInfoFactory
This interface will encapsulate information files. It can return an nsIInterfaceInfo object for any given UIID. It should provide services (on its own or through some helper class?) to read and write InterfaceInfo files. A single instance of this factory, and the InterfaceInfo objects it dispenses, could be shared at runtime by various client modules.
nsIInterfaceInfo
This interface will encapsulate one XPCOM interface - i.e. one UIID. It can return information about that interface including the name, the superinterface, the number of methods, an information object about each method, and information about constants defined by the interface. It should be able to hide superinterface boundaries. i.e. when asked for method info objects it should return those of the superinterface chain as if they were part of this interface. We may have methods in this object to return these 'flattened' sets of methods and constants, and alternate methods to return only those of the directly represented interface.
nsIMethodInfo
This interface will encapsulate one method. It will have methods to get the name, the number of params, and each param.
nsIParamInfo
This interface will encapsulate one paramerter. It will have methods to get the type and modifiers (e.g. in, out, in/out, is_iid reference, etc). Optionally we could expose the param name. We might decide to not expose nsIMethodInfo as an object and instead just have nsIMethodInfo return an encoded string or array of bytes representing the params?
The InterfaceInfo system will be used by XPConnect, but it ought to stand on its own. The XPIDL compiler ought to use it to write InterfaceInfo files. Other tools should be able to use it to read, modify, merge. and split those files. It should not depend either on XPIDL or XPConnect if we can avoid those dependencies.
The XPConnect Runtime
nsIXPConnectRuntime (implemented by nsXPConnectRuntime)
nsXPConnectRuntime objects will be instantiated one to one with nsIJSRuntime; i.e. there will be a factory which takes an nsIJSRuntime as a param to create instances of nsXPConnectRuntime. nsIXPConnectRuntime is used to bootstrap XPCOM into the JS environment and to supply wrappers used for JS calling XPCOM and XPCOM calling JS.
Bootstrapping mainly consists of reflecting the runtime registry and some other 'well known' (potentially application specific) objects into JavaScript. This allows JavaScript to access and instantiate XPCOM objects and also to register JavaScript objects with the shared registry to be accessed by arbitrary XPCOM objects.
We should develop a standard namespace in JS for access to these well known objects: perhaps an object named 'xpcom' to which these well known objects can be attached as properties. We may add default code to make this easy. But, in fact, this can be done easily with normal jsapi calls.
The key methods of nsIXPConnectRuntime are those that do the wrapping. XPCOM Interfaces and JS objects passed across existing XPConnect wrappers will be automatically wrapped. However, nsIXPConnectRuntime will provide explicit interfaces for bootstrapping and for XPCOM code that wants to explicitly construct graphs of reflected XPCOM interfaces in a JavaScript context.
The primary interfaces of nsIXPConnectRuntime are:
NS_IMETHOD WrapNative(nsIJSContext* aJSContext, nsISupports* aCOMObj, REFNSIID aIID, nsICCWrappedNative** aWrapper) = 0; NS_IMETHOD WrapJS(nsIJSContext* aJSContext, nsIJSObject* aJSObj, REFNSIID aIID, nsICCWrappedJS** aWrapper) = 0;
The wrappers are XPCOM objects and are reference counted. The wrappers constructed (or found) by WrappedJS expose a GetJSObject method to allow callers to construct relationships between the wrapper and other JSObjects using the jsapi.
Both kinds of wrappers follow similar rules...
I'm planning a 'fully-factored' class graph to implement this system. We need to construct distinct wrapper instances for each interface of a given object, however, many different objects may expose the same interface. I'll factor out those common parts into objects which can be shared by separate wrappers.
In order to correctly support COM identity rules the various wrappers for the various interfaces of a given wrapped object will be linked in a list. The root object of the linked list of wrappers will be the wrapper representing nsISupports. Often that wrapper is also the same wrapper that supports some other interface. In fact, most typically, a given wrapped object will only be wrapped for nsISupports and one 'interesting' interface. And most objects will return the same pointer when QueryInterface calls are made asking for nsISupports and an 'interesting' interface. In these most common cases we need only generate one wrapper.
Each wrapper will have a pointer to the 'root' wrapper and to a 'next' wrapper. The root wrapper's 'root' pointer will point to itself (and thus we can see that it is the root). A hash table will be maintained that allows us to get at the root wrapper for any wrapped object. From there we can walk the links looking for a wrapper that 'does' the interface desired. It will sometimes be necessary to do a QueryInterface of 'nsISupports' on the interface we want to wrap in order to get the 'right' key for the hash table lookup. We can find or add the appropriate wrapper as necessary (sometimes having to create a new root wrapper for an object if none existed before).
The wrappers are linked together but are individually reference counted. However, they are not individually freed. Rather, they are freed en mass when all become unreachable. For WrappedNative wrappers 'unreachability' is a cross between reference counts going to zero and the JSObject finalizer having been called. The JSObject components of these wrappers will be rooted in JavaScript's gc system while XPCOM holds references and unrooted when the refcount goes to zero. So, the freeing happens only when JavaScript's gc decides to finalize the wrapper.
QueryInterface for for wrapped JavaScript objects...
The WrappedJS wrappers allow JavaScript objects to be reflected into XPCOM. In order to succeed, the JavaScript object must implement the methods declared by the given interface. In many real world uses of XPCOM a given object may be expected to implement more than one interface. For instance, an object claiming to be a a specific kind of widget might need to implement the interface of that specific kind of widget and also the common nsIWidget interface.
To support this idiom I think we should support QueryInterface of wrapped objects implemented in JavaScript. We want it to be easy to implement XPCOM interfaces in JavaScript, so we don't want to require that all wrapped JavaScript objects support QueryInterface. So...
If a wrapped JavaScript object has no QueryInterface method then we assume that the object only supports the interface for which it was wrapped. Any XPCOM caller of QueryInterface on that wrapped object will receive that same wrapper when asking for either that interface or for nsISupports and will receive errors when other interfaces are requested.
If the wrapped JavaScript object does have a QueryInterface method then we call it. We will have a means of representing UUIDs in JavaScript to pass to the QueryInterface call. The object can return itself or some other object when asked for a given interface (or it can fail - return null or possible we'll use exception handling?). Being able to return some other object allows it to implement the multiple interfaces through delegation or whatever means it chooses and to avoid problems when more than one interface uses a method with the same name but different meaning. In order to make the wrapping work right it is required that any object that does implement QueryInterface must also 'do the right thing' when asked for the nsISupports (i.e. the root) interface. The same is true of any objects it returns from a call to QueryInterface. And all objects thus returned as representing different interfaces of the same 'virtual' object must return the same object when asked for the nsISupports object. This allows us to support COM identity rules everywhere.
Factoring...
For each instance of the WrappedNative wrapper we need to build a JSObject with the appropriate method stubs, getters/setters, and constant properties. I propose that to simplify this we lazily build a JSClass for each kind of interface (one per InterfaceInfo) and share that JSClass between WrappedNative instances. These can not be directly shared across JSRuntimes, so they will be per nsIXPConnectRuntime. So, for each nsXPConnectRuntime we'll maintain a hashtable keyed by UIID that returns an object that contains both an InterfaceInfo reference (which can be shared across JSRuntimes) and a JSClass. We build these as needed. They can be found/constructed whenever a new WrappedNative wrapper is constructed.
[10 Dec - jband -
Scott Furman pointed out that just sharing a
JSClass would not have the intended effect because property set calls cause new
properties to be set on the object instances rather than calling the class's
setter as I intended. He suggested building these Wrapper objects using the
newer ObjectOps callback system in the jsapi to allow more direct per object
handling. LiveConnect uses this strategy. I'll look closer at that code.
Nevertheless, a 'per class' shared object can handle this aspect for the various
Wrapper instances used for a given interface.]
The nsXPConnectRuntime will also hold one hashtable each to map XPCOM objects to WrappedNative wrappers and to map JSObjects to WrappedJS wrappers. In both cases the wrapped objects will be the 'root' object as returned by a QueryInterface for nsISupports and the wrapper will be the appropriate 'root' wrapper (from which we can find wrappers for other interfaces on the object as explained previously). We can support an xpcom.equals method that takes two objects (in JavaScript) and determins if they represent the same underlying object. This can work whether the objects are JavaScript which happen to have WrappedJS wrappers or the JSObject object exposed into JavaScript of WrappedNative wrappers. Either way xpcom.equals can do the grunt work to check for COM identity.
changes...
10 Dec 1998 - jband -
changed "WrapJS" to "WrappedJS"10 Dec 1998 - jband -
changed "WrapNative" to "WrappedNative"10 Dec 1998 - jband -
Added note about use of ObjectOps rather than JSClass11 Jan 1999 - jband -
changed 'comconnect' references to be 'XPConnect'