Wednesday, June 28, 2006

Embedding a C# Class in SpiderMonkey

This is a quick walkthrough of the code from my previous post. The required reading is still Scripting C++ with JavaScript using SpiderMonkey. We are embedding in SpiderMonkey a Customer class, the same which was used in that C++ tutorial.

The following is a new C# version:


public class Customer
{
private int m_age;
private string m_name;

public int GetAge()
{
return m_age;
}

public void SetAge(int newAge)
{
m_age = newAge;
}

public string GetName()
{
return m_name;
}

public void SetName(string newName)
{
m_name = newName;
}
}

The class which defines a Customer in JavaScript is JSCustomer. It contains a Customer as a private member and defines a StructLayout so this object can be entirely marshalled into the unmanaged memory of the engine.


using System;
using System.Runtime.InteropServices;
using SpiderMonkey;

[StructLayout(LayoutKind.Sequential)]
public class JSCustomer
{
private Customer m_pCustomer;

JSCustomer()
{
m_pCustomer = null;
}

void setCustomer(Customer customer)
{
m_pCustomer = customer;
}

Customer getCustomer()
{
return m_pCustomer;
}

The dotnet-spidermonkey binding provides a managed JSClass structure that we use to define our class. The unmanaged C struct that this replaces is documented here in the SpiderMonkey API. The .Net binding also provides delegate types to store in this struct, where function pointers would have been used before.


static JSClass Customer_class = new JSClass("Customer",
JS.JSCLASS_HAS_PRIVATE,
new JS.JS_PropertyDelegate(JS.JS_PropertyStub),
new JS.JS_PropertyDelegate(JS.JS_PropertyStub),
new JS.JS_PropertyDelegate(JSGetProperty),
new JS.JS_PropertyDelegate(JSSetProperty),
new JS.JS_EnumerateDelegate(JS.JS_EnumerateStub),
new JS.JS_ResolveDelegate(JS.JS_ResolveStub),
new JS.JS_ConvertDelegate(JS.JS_ConvertStub),
new JS.JS_FinalizeDelegate(JS.JS_FinalizeStub)
);

The callbacks defined above act as accessors and mutators to any property of the object.

Properties and methods used in this function need to be defined by the folloing data structures:


private enum props : byte
{
name = 0,
age = 1
}

private static JSPropertySpecArray Customer_properties =
new JSPropertySpecArray( new JSPropertySpec[] {
new JSPropertySpec(
"name", (byte)props.name, JS.JSPROP_ENUMERATE, null, null ),
new JSPropertySpec(
"age", (byte)props.age, JS.JSPROP_ENUMERATE, null, null ),
new JSPropertySpec( null, 0, 0, null, null )
});

private static JSFunctionSpecArray Customer_methods =
new JSFunctionSpecArray( new JSFunctionSpec[] {
new JSFunctionSpec( "computeReduction",
new JS.JSNative(computeReduction),
1, 0, 0 ),
new JSFunctionSpec( "", null, 0, 0, 0 )
});

A JSPropertySpec is another managed port of the corresponding SpiderMonkey class, while the JSPropertySpecArray is a new type that aides the marshalling of this structure. The same is true for the JSFunctionSpec and JSFunctionSpecArray types, respectively.

Now we can look at the implementation of the accessor/mutator callbacks (delegates).

       
public static Boolean JSGetProperty(IntPtr cx, IntPtr obj, Int32 id, IntPtr vp )
{
if (JS.JSVAL_IS_INT(id))
{
IntPtr JSCustomerPtr = JS.JS_GetPrivate(cx, obj);
JSCustomer p = (JSCustomer)Marshal.PtrToStructure( JSCustomerPtr, typeof(JSCustomer) );
Customer customer = p.getCustomer();
switch ((props)JS.JSVAL_TO_INT(id))
{
case props.name:
string name = customer.GetName();
vp = JS.JS_NewStringCopyN(cx, name, Convert.ToUInt32(name.Length));
break;
case props.age:
Marshal.WriteInt32(vp, JS.INT_TO_JSVAL(customer.GetAge()));
break;
}
}
return true;
}

public static Boolean JSSetProperty(IntPtr cx, IntPtr obj, Int32 id, IntPtr vp )
{
if (JS.JSVAL_IS_INT(id))
{
IntPtr JSCustomerPtr = JS.JS_GetPrivate(cx, obj);
JSCustomer p = (JSCustomer)Marshal.PtrToStructure( JSCustomerPtr, typeof(JSCustomer) );
Customer customer = p.getCustomer();

int vpjsval = Marshal.ReadInt32(vp);
switch ((props)JS.JSVAL_TO_INT(id))
{
case props.name:
IntPtr strPtr = JS.JS_ValueToString(cx, vpjsval);
string name = JS.JS_GetStringBytes(strPtr);
customer.SetName(name);
break;
case props.age:
int x = JS.JSVAL_TO_INT(vpjsval);
customer.SetAge(x);
break;
}
}
return true;
}

The structure of these functions is the same as those from the SAW article. The differences occur where we must marshal data to and from unmanaged memory. After we obtained the JSCustomerPtr from SpiderMonkey, we must marshal the object out of unmanaged memory and into a managed reference type (JSCustomer). The vp parameter is also an unmanaged pointer to a JSVal, so we need use the call to Marshal.ReadInt32() to pull that data into the managed int type.

Similar considerations are needed for our constructor callback and computeReduction callback:


public static Boolean JSConstructor(IntPtr cx, IntPtr obj, UInt32 argc, Int32[] argv, IntPtr rval)
{
JSCustomer priv = new JSCustomer();
priv.setCustomer(new Customer());
priv.getCustomer().SetName("derek");
IntPtr privPtr = Marshal.AllocHGlobal(Marshal.SizeOf(priv));
Marshal.StructureToPtr(priv, privPtr, false);
JS.JS_SetPrivate(cx, obj, privPtr);
return true;
}

public static void JSDestructor(IntPtr cx, IntPtr obj)
{
IntPtr privPtr = JS.JS_GetPrivate(cx, obj);
Marshal.DestroyStructure(privPtr, typeof(JSCustomer));
}

public static Boolean computeReduction(IntPtr cx, IntPtr obj, UInt32 argc, Int32[] argv, IntPtr rval)
{
IntPtr JSCustomerPtr = JS.JS_GetPrivate(cx, obj);
JSCustomer p = (JSCustomer)Marshal.PtrToStructure(JSCustomerPtr, typeof(JSCustomer));
Customer customer = p.getCustomer();

if ( p.getCustomer().GetAge() < 25 )
Marshal.WriteInt32( rval, JS.INT_TO_JSVAL(10));
else
Marshal.WriteInt32(rval, JS.INT_TO_JSVAL(5));
return true;
}

Marshal.AllocHGlobal and Marshal.StructureToPtr are placing our managed referenced type into unmanaged memory. This enables us to move the entire JSCustomer object into the SpiderMonkey engine as private data.

Finally with all our members defined, we need a function that will actually initialize the class in the engine:


public static IntPtr JSInit(IntPtr cxPtr, IntPtr obj, IntPtr proto)
{
JS.JSNative JSConstructorDelegate = (new JS.JSNative(JSConstructor));
IntPtr Customer_classPtr = Marshal.AllocHGlobal(Marshal.SizeOf(Customer_class));
Marshal.StructureToPtr(Customer_class, Customer_classPtr, false);

IntPtr Customer_methodsPtr = JSHelper.MarshalJSFunctionSpecArray(Customer_methods);
IntPtr Customer_propertiesPtr = JSHelper.MarshalJSPropertySpecArray(Customer_properties);

IntPtr newProtoObjPtr = JS.JS_InitClass(cxPtr, obj, proto, Customer_classPtr,
JSConstructorDelegate, 0,
Customer_propertiesPtr, Customer_methodsPtr,
IntPtr.Zero, IntPtr.Zero);

bool r = JS.JS_DefineProperties(cxPtr, newProtoObjPtr, Customer_propertiesPtr);
return newProtoObjPtr;
}

The marshalling of our JSFunctionSpecArray and JSPropertySpecArray types are not as straightforward as other datatypes, so a helper function handles this. Call the JSInit function after initializing the engine and global object, passing the global object pointer into the obj parameter. The custom object will then be ready for use.

0 Comments:

Links to this post:

Create a Link

<< Home