Memory Management for nsIScriptContext

Revision as of 07:08, 14 September 2005 by MarkH (talk | contribs) (Mention XULElement::GetCompiledEventHandler() copies the void *)

Script Object Lifetime Management

This discusses the Script Object lifetimes for all script objects returned by the nsIScriptContext interface. All such objects are returned as a "void *", and the lifetimes of these objects are implicitly tied up in the JS garbage collector and XPCWrappedNative lifetime management. This document attempts to explicitly define lifetime rules such that they can be implemented by any language (well, by JS and Python in the first instance)

Script Globals

A void * is obtained from the language for use as a "global" with an nsIScriptContext. Currently it is assumed the lifetime of the object is the same as the lifetime of the context itself. This model is reasonable and could be retained - however, if an alternate explicit mechanism is used in other places (see below), it may make sense to use that mechanism here.

CompileScript

CompileScript returns a "void *" for the script object. The interface comments for this function say:

   The caller is responsible for GC rooting this object
       

The only caller of this function (nsXULElement) does indeed ensure the void * returned is GC rooted.

As opposed to the event handlers discussed next, there is no "implied reference" for this return object - so the lifetimes of these objects must be explicitly managed.

Event Handlers

Event handlers are a little trickier. There are 3 steps to handling an event:

Compile the script handler

Currently nsJSContext::CompileEventHandler does take the "event target" as a param - however, this is only used to setup principals, and does not otherwise "bind" the compiled function to the event target. (Note: Brendan and I agreed that this param could be dropped from CompileEventHandler() to reinforce this lack of "binding", and the principals thing is apparently no longer necessary. Note however that CompileScript *does* still take an nsIPrincipal param - is that still necessary?)

Due to this lack of binding, the "void *" returned by this function is identical to CompileScript above - the callers must ensure these unbound handlers are rooted if they hold on to them - there is no "implied reference" setup.

Bind the handler

This process binds the compiled event handler to a specific target. In effect, this takes a copy of the compiled handler and "magically" associates it with the event target (see below). After this process, the original compiled event handler is thrown away (and as the caller did not GCRoot the original, it will be disposed of by GC). Critically however, this binding process has created an "implied reference" to the event target - once bound, other assumptions are then made that the event target will not be destroyed.

Call the bound handler

The "void *" passed to CallEventHandler is not the "void *" previously returned directly from CompileEventHandler. Instead, the "magic" association above is reversed, fetching a function object, by name, from the JS wrapper around the original event target. Thus, currently the "void *" used is not supplied directly by nsIScriptContext, so the interface must be abstracted so it is.

Notes:

Theoretically, the above setup should mean that an event handler for a specific object only requires the original event target and event name to fire the event. The "magic" association would ensure that when querying for the event name, the previously cloned function object will be located. However, even though this should be possible, it does not happen for XUL, which stores its XPCWrappedNative pointer in its nsJSEventListener. Thus, XUL is now storing a weak reference to a different interface, not the event itself, in the assumption neither will go away. XBL appears to store the original target.

The nsJSEventListener only keeps a borrowed reference to the target. Even though currently an XPCWrappedNative is stored in place of the original event, it is still a borrowed reference. This borrowed reference itself relies on the "implied reference" model below - without that implied reference, the weak reference to the object in nsJSEventListener would fail. Note also that all of this "WrapNative" work happens *outside* nsIScriptContext, so we need to work out how to abstract it behind that interface.

The "implied reference"

The above relies on an "implied reference" existing on the event target, via the "long-lived XPCWrappedNative" concept supported by XPConnect. This mechanism is used by XPConnect tn ensure the same JS wrapper is always returned for a given DOM nsISupports. While this model makes sense and all other languages would be encouraged to follow something similar, the nsIScriptContext model should not make any such implementation assumptions. Other languages may be able to perform the same magic using a different technique than XPConnect uses, and any "implied references" may not be true. For example, other languages may choose to implement this with weak references to make their own ownership model simpler.

Proposal

Unbound function objects:

Unbound function objects (eg, returned by CompileScript, CompileEventHandler) should be managed by a simple unlock/lock memory management API:

  • All such objects returned by nsIScriptContext are considered "locked". They must remain locked while they are held externally. An 'unlock' function will be provided by the language to unlock the object.
  • It is likely that users of this interface may need to copy the returned object and manage the lifetimes seperately (for example, storing the returned "void *" in 2 unrelated datastructures). For this scenario, an explicit "lock()" call will be provided (meaning 2 unlock calls are now needed to finalize the object). However, as mentioned above, objects will always be returned locked so calling this will be rare. XULElement::GetCompiledEventHandler() is the only identified place that does this.

Bound Event Handlers

There are 2 options here - one relies on reusing the mechanism above for unbound functions, and the other relies on explicit xpcom strong references.

NOTE: I'm still looking at the existing code trying to come up with a concrete proposal.

Have BindCompiledEventHandler() return an out void **

This may be the simplest - this would return a new void **, which must be passed to CallEventHandler(). This new void * is subject to the same rules as unbound handlers, and must be individually unlocked.

Language implementations are then free to have this "void *" indirectly hold a strong reference to the event target (or in the case of JS, need only ensure that the wrapper persists)

Formal binding objects

The process of binding to an event handler can return a new nsISupports object. (As mentioned above, XUL already stores a different nsISupports pointer from the original event target - it just assumes that pointer has the same lifetime). A strong reference to this returned object must be kept and the "binding" is only guaranteed to remain alive as long as the object. Once the binding has been dropped, a new Binding can be created, but it is not guaranteed the returned nsISupports will be the same.

As this is a strong reference, it will be necessary to ensure these references are manually dropped. This means that all nsJSEventListener objects must be explicitly dropped. I believe this already happens now.

Global Script Objects

As mentioned at the start, it may (or may not) make sense to use the same explicit model we use for compiled scripts and event handlers.