ScriptableObject

From MozillaWiki
Jump to: navigation, search

nsIScriptableObject

A need has been identified for an XPCOM interface to support general purpose "scriptable objects". "scriptable objects" are objects with methods and properties that change at runtime. For this reason, the use of normal XPCOM interfaces is not suitable.

nsIScriptableObject is designed to solve this problem. It is an interface that allows arbitrary property references to be made on an object. The operations allowed are:

  • Call a method on an object (including calling the object itself - ie, an nsIScriptableObject is itself 'callable')
  • Set and get the value of a property on the object.
  • Create or delete properties on the object.
  • Enumerate the properties of the object.

Not all implementations of the interface will support all operations, but the interface itself must be able to describe the above semantics.

Script languages may provide special support for this interface. For example, given an nsIScriptableObject object, script languages should be able to dynamically translate "x = object.foo" into the necessary nsIScriptableObject calls to lookup and fetch the attribute 'foo' on the interface.

nsIScriptableObject design

We will shamelessly borrow the MS COM IDispatchEx interface as a model. IDispatchEx is designed for this purpose and provides a number of enhancements to the older IDispatch interfaces. For convenience, this document often refers to simply IDispatch, but this should be read as meaning IDispatchEx.

It is worth discussing the role of IDispatchEx with MS's JScript and IE. JScript has native support for working with IDispatchEx as a namespace - ie, property references, assignments and method calls etc make IDispatchEx calls. GetNameSpaceParent is used to manage scoping. IE provides the IDispatchEx implementation. As IE initializes script engines (ie, script languages), it provides an IDispatchEx implementation for each "named object" - such as 'window'. As all script languages share this common IDispatchEx instance, all languages work with the same namespace - a property set in one language is reflected in another.

A couple of excellent resources on this topic are at http://blogs.msdn.com/ericlippert/archive/2004/10/07/239289.aspx and http://blogs.msdn.com/ericlippert/archive/2004/09/20/231852.aspx

More information can be found at the new nsIScriptable interface description.

Language Helpers

Use Cases

existing dialog widget

Consider the dialog widget, which allows you to have an "ondialogaccept" event handler. This widget is implemented in JS. See dialog.xul, http://lxr.mozilla.org/seamonkey/source/xpfe/global/resources/content/bindings/dialog.xml#304

In summary, a dialog widget's "ondialogaccept" handler is called using the following paraphrased code:

 // fetch the 'ondialogaccept' or 'ondialogcancel' attribute
 var handler = this.getAttribute("ondialog"+aDlgType);
 // compile code returning a function object
 var fn = new Function("event", handler);
 // call the function.
 var returned = fn(event);
 

The challenge is to allow this JS implemented widget to work in a language agnostic fashion - so that if the dialog node has a different script-type set, the JS implementation can call the other language.

This would be achieved by:

 [scriptable, uuid(...)]
 interface nsIDOMScriptableObjectHelper: nsISupports
  {
    // Compile and call an "event handler" for the specified language, returning
    // the result. |aGlobal| must be the global window in which the
    // returned function will execute when called. Generally you pass |window|.
    // This is basically a "scriptable" version of
    // nsIScriptContext::CompileEventHandler/CallEventHandler - note that
    // a number of things conspire against this interface exposing compile and
    // call distinctly:
    // * Ideally, compile could return an nsIScriptableObject which could be 'called'
    //   using generic nsIScriptableObject semantics - but nsIScriptContext
    //   semantics require the global object is passed to the call.
    //   It might be possible to pollute nsIScriptableObject to work around this, but
    //   that's messy, and doesn't work in the general case for Python.
    // * BindCompiledEventHandler and GetBoundEventHandler must be used to
    //   ensure the correct 'binding' of the handler with the target.
    // * The 'handler' to the call step must be an nsISupports or similar, and
    //   converting this to the 'void *' for the language, and managing the
    //   lifetime of that void * are complicated hoops, especially given the above.
    nsIVariant callEventHandler(in nsISupports aGlobal,
                                in PRUint32 aScriptTypeID, 
                                in DOMString aFuncName,
                                in PRUint32 argCount,
                                [array,size_is(argCount)] in string argNames,
                                [array,size_is(argCount)] in nsIVariant argValues,
                                in DOMString aBody,
                                in AUTF8String aURL,
                                in PRUint32 aLineNumber);
    // Execute arbitrary code.
    void execute(in nsISupports aGlobal,
                 in PRUint32 aScriptTypeID,
                 in DOMString aCode);
    // Evaluate an arbitrary expression.
    nsIScriptableObject evaluate(in nsISupports aGlobal,
                                 in PRUint32 aScriptTypeID,
                                 in DOMString aCode);

    // Takes a script-type ID (which is presumably the ID for
    // a language other than the one being executed), and nsISupports 
    // |aGlobal| (which must be the global object for the currently
    // executing language) and returns an nsIScriptableObject, which is 
    // the global scope object for the requested language.
    nsIScriptableObject getLanguageGlobal(in nsISupports aGlobal,
                                          in PRUint32 aScriptTypeID);
 };

And the JS code implementing dialog.xml would change to:

  var handler_name = "ondialog"+aDlgType;
  var handler_code = this.getAttribute(handler_name);
  if (handler_code != "") {
    // Execute the handler via the 'Script Object Helper' so all
    // script languages are supported.
    var helper = Components.classes[
                 '@mozilla.org/dom/scriptable-object-helper;1'].getService();
    // Determine the default 'script type ID' specified for the target.
    var stid = event.target.scriptTypeID;
    // Apparently impossible to determine URL and lineno?
    // Make the call.
    var returned = helper.callEventHandler(
                                    window, stid,
                                    handler_name,
                                    1, // args count
                                    ['event'], // arg names
                                    [event], // args
                                    handler_code,
                                    "<dialog event handler>", // URL
                                    0); // linenumber

Contrived dialog widget

Below is a contrived example that demonstrates getLanguageGlobal. Let's assume the design of the dialog widget was such that it simply assumed a global function called "ondialogaccept" in the window namespace.

For example, let's assume a dialog widget insisted you structure your dialog like:

 <script>
   function ondialogaccept(event) { ... do something .." }
 </script>
 <dialog>

This could be implemented now in a JavaScript specific way simply by calling the function 'ondialogaccept' by name, in the assumption a <script> block created such a function.

Assuming nsIScriptableObject, this could be implemented in a language agnostic fashion as follows:

 var global = s.getLanguageGlobal(lang_id, window)
 var func = global.ondialogaccept
 var ret = func(event)

The first line returns an nsIScriptableObject object. The JS support for this interface would then allow 'namespace.ondialogaccept' to invoke the necessary nsIScriptableObject methods to lookup the attribute. The attribute would be looked up as a simple property reference, and as a function object would be found, |func| would itself be an nsIScriptableObject. |func(event)| would then automatically call nsIScriptableObject::invoke with a DISPID of DISPID_DEFAULT.

For the sake of completeness, the above could also obviously be written as:

 var global = s.getLanguageGlobal(lang_id, window)
 var ret = global.ondialogaccept(event)
   

In this case the use of nsIScriptableObject may be slightly different -- the initial invoke for 'ondialogaccept' may be done as a method call, assuming the language is capable of detecting that situation.