Bi-directional Plugin Scripting
July 26, 2002
Introduction
Background
Sample Code
Mozilla has supported plugin scripting since the 0.9.4 milestone, and has been documented by a previous article. However, that article only discusses how to make a plugin callable from JavaScript. This article will show how to call JavaScript from a plugin.
The
traditional plugin API already provides a mechanism by which a plugin
instance can call into JavaScript and access information from the embedding web page.
A plugin instance can simply call NPN_GetURL()
passing in a "javascript:" URL.
Here's a simple example:
NPN_GetURL(instance, "javascript:alert('hello world.')", NULL);
This simply tells JavaScript to display an alert window with the message "hello world." However, this happens asynchronously, i.e. the alert doesn't come up until some time after the call has been issued. This makes showing alert rather inconvenient to use as a debugging tool. In addition, what if what we really want to do is read some value, rather than display a message?
If we are willing to wait for the result to come at some later time, then we have a couple
of options. One is to use NPN_GetURLNotify()
instead, which will call the plugin
back with the result of evaluating the JavaScript URL. Alternatively, since you are presumably
writing a scriptable plugin, we can deliver the results to the scriptable plugin through one
of its methods. For the sake of exposition, let's define a hypothetical scriptable plugin
interface:
interface acmeScriptablePlugin : nsISupports { attribute string location; }
This interface defines an attribute, location, which can be both read and written from script. If the plugin is instantiated on a web page, all by itself, then a simple script that reads and writes this attribute would look like this:
<SCRIPT> var plugin = document.embeds[0]; plugin.location = document.location; // tell the plugin the URL of this document. alert('location = ' + plugin.location); // read back the document's location </SCRIPT>
This is an example of a script calling into a plugin's scriptable interface. Things get more interesting if we define an attribute with a more complex value. Let's define an interface that represents a simple JavaScript object. It has properties which can be read and written:
interface acmeJSObject : nsISupports { string getProperty(in string name); void setProperty(in string name, in string value); }
If we modify our plugin's scriptable interface to use this interface, we can provide a way for the plugin itself to read properties of the web page's document object:
interface acmeScriptablePlugin : nsISupports { attribute acmeJSObject document; }
Here's JavaScript code that would write to the document attribute:
<SCRIPT> var plugin = document.embeds[0]; var docWrapper = { doc : document, getProperty : function(name) { return this.doc[name]; } setPropety : function(name, value) { this.doc[name] = value; } }; plugin.document = docWrapper; // give plugin wrapper to this document. </SCRIPT>
Finally, here's some C++ code reads document.location:
nsIMemory* allocator = GetMemoryAllocator(); acmeJSObject* document = GetJSDocument(); char* location; if (NS_SUCCEEDED(document->GetProperty("location", &location))) { printf("location = %s\n", location); allocator->Free(location); }
How does all of this work? The Mozilla browser contains a bridging layer which wraps JavaScript objects in C++ objects that implement XPCOM interfaces. All that a script object has to do to implement an XPCOM interface is to define function properties that correspond to the IDL declaration of that interface.
Here's a more fully fleshed out example. Here's an interface that will be implemented by JavaScript code that the plugin injects into a web page:
/* acmeIScriptObject.idl */ #include "nsISupports.idl" [scriptable, uuid(f78d64e0-1dd1-11b2-a9b4-ae998c529d3e)] interface acmeIScriptObject : nsISupports { acmeIScriptObject getProperty(in string name); void setProperty(in string name, in acmeIScriptObject value); /** * Conversions. */ string toString(); double toNumber(); /** * Constructors. */ acmeIScriptObject fromString(in string value); acmeIScriptObject fromNumber(in double value); acmeIScriptObject call(in string name, in PRUint32 count, [array, size_is(count)] in acmeIScriptObject argArray); };
Here's a JavaScript implementation of acmeIScriptObject
:
function jsScriptObject(obj) { // implementation detail, to allow unwrapping. this.wrappedJSObject = obj; } jsScriptObject.prototype = { QueryInterface : function(iid) { try { if (iid.equals(Components.interfaces.acmeIScriptObject) || iid.equals(Components.interfaces.nsIClassInfo) || iid.equals(Components.interfaces.nsISecurityCheckedComponent) || iid.equals(Components.interfaces.nsISupports)) { alert("QI good."); return this; } throw Components.results.NS_ERROR_NO_INTERFACE; } catch (se) { // older browsers don't let us use iid.equals, wah. return this; } } // acmeIScriptObject implementation. getProperty : function(name) { return new jsScriptObject(this.wrappedJSObject[name]); } setProperty : function(name, value) { alert('setProperty: name = ' + name + ', value = ' + value.toString() + '\n'); this.wrappedJSObject[name] = value.toString(); } toString : function() { return this.wrappedJSObject.toString(); } toNumber : function() { return this.wrappedJSObject.valueOf(); } fromString : function(value) { return new jsScriptObject(value); } fromNumber : function(value) { return new jsScriptObject(value); } call : function(name, argArray) { // TBD } };
Finally, here's some C++ code that uses the acmeIScriptObject
interface:
NS_IMETHODIMP nsScriptablePeer::SetWindow(acmeIScriptObject *window) { NS_IF_ADDREF(window); NS_IF_RELEASE(mWindow); mWindow = window; acmeIScriptObject* location = nsnull; nsresult rv = window->GetProperty("location", &location); if (NS_SUCCEEDED(rv) && location) { char* locationStr = NULL; rv = location->ToString(&locationStr); if (NS_SUCCEEDED(rv) && locationStr) { NPN_MemFree(locationStr); } NS_RELEASE(location); } acmeIScriptObject* newLocation = nsnull; rv = window->FromString("http://www.mozilla.org", &newLocation); if (NS_SUCCEEDED(rv) && newLocation) { window->SetProperty("location", newLocation); NS_RELEASE(newLocation); } return NS_OK; }