2009 08 12Taming JavascriptCore within and without WebView
On a Mac ? Check out JSCocoa, which bridges Cocoa and Javascript. Its latest version let you manipulate WebViews straight from your JSCocoa context.
Onward !
Starting up
All JavascriptCore code exists in a JavascriptCore context.
// Init JSContextRef ctx = JSGlobalContextCreate(NULL); // Do some work ... // Release JSGlobalContextRelease(ctx);
The simplest call that might possibly work
To create an array, a hash, a new Date object, to test if objects match a condition … JSObjectCallAsFunction is the function to use. It calls a Javascript function with as many arguments as you supply. It can also call anonymous functions. Anonymous functions don't have names, won't clog up your namespace, and will disappear as soon as your destroy all references to them. To get a result, create an anonymous function containing your code, call it, and handle the result. JavascriptCore will cleanup behind you.
// Create a new array
JSStringRef scriptJS = JSStringCreateWithUTF8CString("return new Array");
// Create an anonymous function and call it
JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL);
JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 0, NULL, NULL);
// Release script
JSStringRelease(scriptJS);
result will then contain a new array. Any object creation is better handled through anonymous functions, instead of writing the C code for getting an object reference and calling its constructor.
// Create a hash
JSStringRef scriptJS = JSStringCreateWithUTF8CString("return { hello : 'world', value : 123 }");
// Create a new Date object
JSStringRef scriptJS = JSStringCreateWithUTF8CString("return new Date");
// Even create another anonymous function !
JSStringRef scriptJS = JSStringCreateWithUTF8CString("return function (a, b) { return a+b }");
Arguments
To test if objects match a condition, to build a new object from existing ones, … put your arguments in a C array.
// One argument only : use its address as a one-value C array
// (Test if argument is an array)
JSStringRef scriptJS = JSStringCreateWithUTF8CString("return arguments[0].constructor == Array.prototype.constructor");
JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&jsObject, NULL);
// Multiple arguments
JSValueRef args[] = { arg1, arg2, arg3 };
JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 3, args, NULL);
Your own objects
JavascriptCore lets you define callbacks for when your object is created, destroyed, queried for a property, called as a function or a constructor, and when something wants to set a new property on it.
// Create a class definition and register it
JSClassDefinition myJavascriptClass = kJSClassDefinitionEmpty;
myJavascriptClass.getProperty = myClassGetProperty;
myClass = JSClassCreate(&myJavascriptClass);
static JSValueRef myClassGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception)
{
// Will be called when JavascriptCore requests propertyNameJS on your object
}
Instancing your own objects
Use JSObjectMake and pass your class. Return this object to Javascript and myClassGetProperty will be called when queried for a property.
// Create a Javascript object with a particular class JSObjectRef o = JSObjectMake(ctx, jsCocoaObjectClass, NULL);
Your own data
An object can hold a private pointer. Once again, use JSObjectMake and specify the class and a pointer to your data. You may want to add a finalize callback to be notified when JavascriptCore is destroying the object, as it will be time for cleanup. Here, jsCocoaObjectClass'sfinalize callback would release the ObjC object held as private data.
// Create a Javascript object with a particular class and holding private data MyObject* privateData = [MyObject new]; JSObjectRef o = JSObjectMake(ctx, jsCocoaObjectClass, privateData);
You can also use JSObjectGetPrivate and JSObjectSetPrivate.
Creating Javascript objects
Either call JSObjectCallAsFunction to create objects or use JSValueMake* functions.
// Return a number
return JSValueMakeNumber(ctx, 1.23);
// Return a number
return JSValueMakeNull;
// Return a string
JSStringRef string = JSStringCreateWithUTF8CString("Hello world");
JSValueRef result = JSValueMakeString(ctx, string);
JSStringRelease(string);
return result;
Converting from type to type
Javascript uses a these types : string, number, boolean, null, undefined, object. Convert between these with JSValueTo*. These functions are equal to calling the equivalent type constructor, like var boolResult = Boolean(value) in Javascript.
// Convert to Boolean JSValueRef boolResult = JSValueToBoolean(ctx, value);
Get a native string out of any JavascriptCore object
JSValueToStringCopy will call toString on an object and return a Javascript string that you can then convert to an NSString.
JSStringRef resultStringJS = JSValueToStringCopy(ctx, value, NULL); NSString* resultString = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, resultStringJS); NSLog(@"Javascript value=%@", resultString); // Release string when done JSStringRelease(resultStringJS); // And autorelease our NSString [NSMakeCollectable(resultString) autorelease];
JSStringGetMaximumUTF8CStringSize, JSStringGetUTF8CString will return a UTF8 string.
Garbage collection
JavascriptCore releases objects you've created, so you almost have not to worry about GC. Any result coming from calling JSValueMakeNumber, JSObjectMake is watched by GC and will be collected. Exceptions :
-
JSStringCreateWithUTF8CString,JSStringCreateWithCFStringneedsJSStringRelease -
JSObjectCopyPropertyNames(An array of an object's property names) needsJSPropertyNameArrayRelease -
JSClassCreateneedsJSClassRelease - And the Javascript context itself, created with
JSGlobalContextCreateneedsJSGlobalContextRelease
Protecting objects from Garbage Collection
To prevent JavascriptCore from collecting a Javascript object, call JSValueProtect. When done, call JSValueUnprotect. This works for JSValueRef and JSObjectRef.
This is useful to keep stuff around. In JSCocoa, some ObjC objects can hold a Javascript object, thereby containing an infinite number of variables without needing to declare a method for each. As JavascriptCore has no way of knowing this, JSValueProtect marks that object as protected.
Responding to callbacks
Now that you know how to create data, call functions, convert … you can write the callbacks to your objects.
static JSValueRef myClassGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception)
{
// Get the property name as a NSString
NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS);
[NSMakeCollectable(propertyName) autorelease];
// From there you can return values, functions, objects, or throw an exception
if ([propertyName isEqualToString:@"myNumber"]) return JSValueMakeNumber(ctx, 1.23);
if ([propertyName isEqualToString:@"hello"])
{
JSStringRef string = JSStringCreateWithUTF8CString("world");
JSValueRef result = JSValueMakeString(ctx, string);
JSStringRelease(string);
return result;
}
// Throw an exception
if ([propertyName isEqualToString:@"notYet"])
{
JSStringRef string = JSStringCreateWithUTF8CString("Data not ready");
JSValueRef exceptionString = JSValueMakeString(ctx, string);
JSStringRelease(string);
// Converting the result to an object will let JavascriptCore add source URL and line number to the exception,
// instead of just returning a raw string
*exception = JSValueToObject(ctx, exceptionString, NULL);
return NULL;
}
// JavascriptCore might crash if you return NULL in some functions, so always return a Javascript object
// Here, we return undefined
return JSValueMakeUndefined(ctx);
}
Global scope, like Safari's window
In a Web window, functions and attributes like eval, setInterval, document are global variables. They're part of window, which is the global object : window.document and document retrieve the same object . To get your own global object, use a custom class when creating your context.
// JavascriptCore will call myGlobalClass's getProperty when not finding a variable JSContextRef ctx = JSGlobalContextCreate(myGlobalClass);
Within WebView
WebView holds its own JavascriptCore context and lets you access it with globalContext on a frame. You can then add your custom objects and call custom code through this context.
// Get the window context JSContextRef webViewCtx = [[WebView mainFrame] globalContext]; // Call a js function JSValueRef result = JSObjectCallAsFunction(webViewCtx, ...)
If you want to hold on to objects, you might want to call JSGlobalContextRetain as the WebView could be destroyed before your objects. Call JSGlobalContextRelease when done.
Thanks. Would have been better if you could provide more explanation and complete examples.
Hi, please teach me, how to convert char* - string to JSArray Object.
char* -> JSStringRef . JSSTringRef->JSValueRef . JSValueRef ->JSValueMakeArray. Does not help. becase JSValueMakeArray need jsValue array as 3rd argument. Any help appreciated..
dvbfreaky007@gmail.com
@dvbfreaky : JSObjectMakeArray takes a C array as parameter. Allocate one and put your values in it :
JSValueRef* values = malloc(sizeof(JSValueRef)*itemCount);
array[0] = JSValueMake(...)
array[1] = ....
JSObjectRef jsArray = JSObjectMakeArray(ctx, itemCount, values, NULL);
free(values)
Thanks for this great introduction. There is precious little info on how to use the JavaScriptCore API.