Language agnostic event handling
Language Agnostic Event Handling
This document is a place to dissect the current event handling mechanism and make it language agnostic.
This currently contains notes randomly pulled from Memory Management for nsIScriptContext, and will be fleshed out soon.
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.
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.
Event Handling Options
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.