Parmanoir

Taming JavascriptCore within and without WebView

Here's a little tutorial for JavascriptCore, the Javascript library used by Safari, the iPhone, Konqueror, and more. JavascriptCore lets you execute any kind of Javascript while allowing you to mix in your own objects, callbacks, and data.

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 needs JSStringRelease
  • JSObjectCopyPropertyNames (An array of an object's property names) needs JSPropertyNameArrayRelease
  • JSClassCreate needs JSClassRelease
  • And the Javascript context itself, created with JSGlobalContextCreate needs JSGlobalContextRelease

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.

And this concludes the JavascriptCore tour. Have a look at the JSCocoa source for some code.

Philip Taylor
2010 02 25

Thanks for this great introduction. There is precious little info on how to use the JavaScriptCore API.

John
2010 05 27

Thanks. Would have been better if you could provide more explanation and complete examples.

dvbfreaky
2011 01 07

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

Patrick Geiller
2011 01 07

@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)


Follow me on Twitter
Planet Cocoa
Cocoa.fr

2011 02 22Distance field
2010 07 202Binding through NSApp
2010 05 122Forwarding invocations
2010 02 272Core Image black fringes
2010 02 21Quickest Way to Shell
2010 02 08Who's calling ?
2009 09 2138 ways to use Blocks in Snow Leopard
2009 08 182Bracket Mess
2009 08 124Taming JavascriptCore within and without WebView
2009 04 15Debugging with Activity Monitor
2009 03 25How Core Image Color Tracking works
2009 03 1510Custom NSThemeFrame
2009 03 10Which framework is running ?
2009 03 074CoreUI can paint pretty big
2009 02 18Localization with functions
2009 01 30Did you forget to nest alloc and init?
2009 01 16JSCocoa on the iPhone
2009 01 11Mixing WebView and JavascriptCore
2009 01 09Badge overflow
2009 01 09Find your Garbage Collection leaks with Instruments

Powered by MediaWiki