You are currently viewing a snapshot of www.mozilla.org taken on April 21, 2008. Most of this content is highly out of date (some pages haven't been updated since the project began in 1998) and exists for historical purposes only. If there are any pages on this archive site that you think should be added back to www.mozilla.org, please file a bug.



Bi-directional Plugin Scripting

Patrick C. Beard <beard@netscape.com>

July 26, 2002

Introduction
Background
Sample Code

Introduction

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.

Background

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.

Sample Code

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;
}