Monday, September 05, 2005

Sometimes, you just have to throw it all away, and start from scratch.

Unfortunately, my design didn't hold water when it came to two (closely related) things:

  • Stuffing the address of a C++ virtual member function into a JavaScript object, which wants nothing to do with C++ virtual member functions.
  • Difficulty coming up with a scheme for passing "this" to the proxy class directly from JavaScript. This was the drop-dead issue; I might have been able to figure out a way to make the compiler allow me to specify a native function as a pointer to a C++ member, but getting a "this" pointer for the proxy class on the stack when the wrapper function is called would surely have been difficult, if not impossible.
So, it was time to step back, think a bit, and re-design.

It didn't take long to see there was a better way. The new design, instead of generating a C++ proxy class for each component class, generates extern "C" wrapper functions that can be directly added to the JavaScript object via JS_DefineFunction(). These wrapper functions, when called, simply lookup a pointer to the corresponding component class instance, and call it.

Each wrapper function name is generated so that it contains the name of the class, and the name of the member function. This ensures that each function is uniquely named (at least, within the component). As an example, below are the proxy functions that are generated for the HelloClass class directly from the SIL file:

extern "C" JSBool
HelloClass_Get_age(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

extern "C" JSBool
HelloClass_Set_age(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

extern "C" JSBool
HelloClass_Hello(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

The above functions correspond to, and wrap, the Get_age(), Set_age(), and Hello() member functions of the HelloClass class implemented by the component.

Let's now take a look at the function NewHelloClassProxy(), and talk about what it does:

static map<JSObject *, HelloClass *> objHelloClassMap;

extern "C" PRStatus
NewHelloClassProxy(JSObject *obj)
HelloClass *ptr = new HelloClass();
if (!ptr)
return PR_FAILURE;
AddHelloClassClass(obj, ptr);
return PR_SUCCESS;

When the JavaScript application requests a HelloClass object, we create a JavaScript object (as before). We then call the above function, NewHelloClassProxy(), and pass to it the pointer to the JavaScript object that was just created. NewHelloClassProxy() then creates an instance of HelloClass, and adds it to an STL map that holds JavaScript object/HelloClass instance pairs. Given a JavaScript object, we can find the corresponding pointer to a HelloClass instance.

When NewHelloClassProxy() returns, the layout engine stuffs the address of the extern "C" wrapper functions into the JavaScript object. When these functions are invoked by the JavaScript engine, the JavaScript object will be passed as an argument. We can take this JavaScript object, look it up in the map, and then use the resulting HelloClass instance pointer to invoke the wrapped component class instance method.

Here is an example of how the wrapper function HelloClass_get_age() locates the appropriate HelloClass instance, using the JavaScript object it is passed, and invokes the component class instance method:

extern "C" JSBool
HelloClass_Get_age(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
PRInt32 arg_0;
HelloClass *ptr = GetHelloClassClass(obj);
if (!ptr || ptr->Get_age(arg_0) != PR_SUCCESS)
return JS_FALSE;
*rval = JSVAL_TO_INT(arg_0);
return JS_TRUE;

The implementation of GetHelloClassClass() is straightforward; it simply looks up the JavaScript object in the map and returns the HelloClass instance pointer:

static HelloClass *
GetHelloClassClass(JSObject *obj)
return objHelloClassMap[obj];

Ok, so, this should do it. I think that I said this once before, but all that should remain is to wire the above support into the layout code to handle object requests from JavaScript, as descriobed. If all goes well (and it should this time, I hope), I'll be calling C++ from JavaScript soon enough.