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
,JSStringCreateWithCFString
needsJSStringRelease
-
JSObjectCopyPropertyNames
(An array of an object's property names) needsJSPropertyNameArrayRelease
-
JSClassCreate
needsJSClassRelease
- And the Javascript context itself, created with
JSGlobalContextCreate
needsJSGlobalContextRelease
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.