How to use the JavaScriptCore's C API

DEC 3, 2016

Introduction

JavaScriptCore is a framework responsible for interpreting JavaScript code. The framework is available on iOS, OS X, and tvOS. While most people are familiar with the Objective-C API its C API remain poorly documented though its open-source and when you navigate through the source code you will realize that the Objective-C API is just a wrapper around the C interface.

Getting Started

First let’s discuss about some of the component we will use

JSContextGroupRef

JSContextGroupRef is the equivalent to JSVirtualMachine in Objective-C, This is a self-contained environment for JavaScript execution, all code and contexts live here, usually you only need one instance of it.

JSContextRef

JSContextRef is the equivalent to JSContext in Objective-C, this where the global object reside and other execution state get saved.

JSObjectRef

JSObjectRef is the equivalent to JSObject in Objective-C, this represents a JavaScript object.

Now you have a basic understanding of the framework let’s start coding

Calling native code from JavaScript

#include <iostream>
#include <JavaScriptCore/JavaScriptCore.h>

using namespace std;
									
JSValueRef ObjectCallAsFunctionCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
	cout << "Hello World" << endl;
	return JSValueMakeUndefined(ctx);
}

int main(int argc, const char * argv[]) {

	JSContextGroupRef contextGroup = JSContextGroupCreate();
	JSGlobalContextRef globalContext = JSGlobalContextCreateInGroup(contextGroup, nullptr);
	JSObjectRef globalObject = JSContextGetGlobalObject(globalContext);
	
	JSStringRef logFunctionName = JSStringCreateWithUTF8CString("log");
	JSObjectRef functionObject = JSObjectMakeFunctionWithCallback(globalContext, logFunctionName, &ObjectCallAsFunctionCallback);
		
	JSObjectSetProperty(globalContext, globalObject, logFunctionName, functionObject, kJSPropertyAttributeNone, nullptr);
	
	JSStringRef logCallStatement = JSStringCreateWithUTF8CString("log()");
	
	JSEvaluateScript(globalContext, logCallStatement, nullptr, nullptr, 1,nullptr);
	
	/* memory management code to prevent memory leaks */
	
	JSGlobalContextRelease(globalContext);
	JSContextGroupRelease(contextGroup);
	JSStringRelease(logFunctionName);
	JSStringRelease(logCallStatement);
	
	return 0;
}

When the above code is executed, it should print the following result:

Hello World

Lets discus what we did

  • The contextGroup object is the virtual machine and here everything happen.

  • The globalContext object is the execution environment, the first parameter is the virtual machine object and the second parameter is the class of the root object we pass nullptr to use the default class.

  • The globalObject object represent the global JavaScript object the root one so we will be able to define our objects to the JavaScript execution environment.

  • The functionObject object represent the JavaScript function object, we create it by calling JSObjectMakeFunctionWithCallback.

  • We expose our function to JavaScript by defining it to the global object globalObject by calling JSObjectSetProperty, the third parameter is the name of the object we want to define, the fifth parameter is the attributes, kJSPropertyAttributeNone mean that the object have no special attributes, the last parameter is the exception object pointer, we pass nullptr for simplicity, in production code you should pass an object pointer rather than a nullptr to handle errors.

  • And last but not least the callback function ObjectCallAsFunctionCallback all calls from JavaScript to our function will be directed to this function, we return JavaScript object with undefined type to indicate that our function doesn't return any value.

  • Finally we call JSEvaluateScript to execute the JavaScript code.

Extending the native function

We will extend the callback function so it will log the parameters passed to it, the callback function should look like this:

#include <iostream>
#include <JavaScriptCore/JavaScriptCore.h>

using namespace std;

std::string JSStringToStdString(JSStringRef jsString) {
	size_t maxBufferSize = JSStringGetMaximumUTF8CStringSize(jsString);
	char* utf8Buffer = new char[maxBufferSize];
	size_t bytesWritten = JSStringGetUTF8CString(jsString, utf8Buffer, maxBufferSize);
	std::string utf_string = std::string(utf8Buffer, bytesWritten -1);
	delete [] utf8Buffer;
	return utf_string;
}

JSValueRef ObjectCallAsFunctionCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
	for (size_t i=0; i<argumentCount; i++) {
		JSStringRef pathString = JSValueToStringCopy(ctx, arguments[i], nullptr);
		cout << JSStringToStdString(pathString);
	}
	cout << endl;
	return JSValueMakeUndefined(context);
}

If you change the evaluated script to log('string');log(1); it should print the following result:

string
1

Defining native objects to JavaScript

#include <iostream>
#include <JavaScriptCore/JavaScriptCore.h>
#include <sys/stat.h>

using namespace std;

struct FilesystemPrivate {
	string path;
	bool is_directory;
	bool is_file;
	bool is_symlink;
	size_t size;
	bool exists;
};
							
std::string JSStringToStdString(JSStringRef jsString) {
	size_t maxBufferSize = JSStringGetMaximumUTF8CStringSize(jsString);
	char* utf8Buffer = new char[maxBufferSize];
	size_t bytesWritten = JSStringGetUTF8CString(jsString, utf8Buffer, maxBufferSize);
	std::string utf_string = std::string (utf8Buffer, bytesWritten -1); // the last byte is a null \0 which std::string doesn't need.
	delete [] utf8Buffer;
	return utf_string;
}

JSValueRef ObjectCallAsFunctionCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
	for (size_t i=0; i<argumentCount; i++) {
		JSStringRef pathString = JSValueToStringCopy(ctx, arguments[i], nullptr);
		cout << JSStringToStdString(pathString);
	}
	cout << endl ;
	return JSValueMakeUndefined(ctx);
}

void setAttributes(FilesystemPrivate *fs, std::string path) {
	fs->path = path;
	
	struct stat statbuf;
	
	if (lstat(path.c_str(), &statbuf) != -1) {
		switch (statbuf.st_mode & S_IFMT){
			case S_IFREG:
				fs->is_file = true;
				break;
			case S_IFLNK:
				fs->is_symlink = true;
				break;
			case S_IFDIR:
				fs->is_directory = true;
				break;
		}
		fs->size = statbuf.st_size;
		fs->exists = true;
	}else{
		fs->exists = false;
		fs->is_file = false;
		fs->is_directory = false;
		fs->is_symlink = false;
		fs->size = 0;
	}
}

/* callbacks */

void Filesystem_Finalize(JSObjectRef object){
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	delete fs;
}

JSObjectRef Filesystem_CallAsConstructor(JSContextRef ctx, JSObjectRef constructor, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception){
	FilesystemPrivate *fs = new FilesystemPrivate();
	
	JSStringRef pathString = JSValueToStringCopy(ctx, arguments[0], nullptr);
	setAttributes(fs, JSStringToStdString(pathString));
	JSObjectSetPrivate(constructor, static_cast<void*>(fs));
	
	return constructor;
}

/* static values */

JSValueRef Filesystem_getPath(JSContextRef ctx, JSObjectRef object,JSStringRef propertyName, JSValueRef* exception) {
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	JSStringRef pathString = JSStringCreateWithUTF8CString(fs->path.c_str());
	
	return JSValueMakeString(ctx, pathString);
}
	
bool Filesystem_setPath(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception) {
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	JSStringRef pathString = JSValueToStringCopy(ctx, value, nullptr);
	
	setAttributes(fs, JSStringToStdString(pathString));
	
	return true;
}

JSValueRef Filesystem_getType(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception) {
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	JSStringRef pathType;
	
	if (fs->is_file) {
		pathType = JSStringCreateWithUTF8CString("File");
	}else if (fs->is_directory) {
		pathType = JSStringCreateWithUTF8CString("Directory");
	}else if (fs->is_symlink) {
		pathType = JSStringCreateWithUTF8CString("Symlink");
	}else{
		pathType = JSStringCreateWithUTF8CString("Unknown");
	}
	
	return JSValueMakeString(ctx, pathType);
}

JSValueRef Filesystem_getExist(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception) {
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	
	return JSValueMakeBoolean(ctx, fs->exists);
}

JSValueRef Filesystem_getSize(JSContextRef ctx, JSObjectRef object,JSStringRef propertyName, JSValueRef* exception) {
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	
	return JSValueMakeNumber(ctx, static_cast<double>(fs->size));
}

JSValueRef Filesystem_remove(JSContextRef ctx, JSObjectRef function, JSObjectRef object, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception){
	FilesystemPrivate *fs = static_cast<FilesystemPrivate*>(JSObjectGetPrivate(object));
	remove(fs->path.c_str());
	
	return JSValueMakeUndefined(ctx);
}

JSClassRef FilesystemClass() {
	static JSClassRef filesystem_class;
	if (!filesystem_class) {
		JSClassDefinition classDefinition = kJSClassDefinitionEmpty;
		
		static JSStaticFunction staticFunctions[] = {
			{ "remove", Filesystem_remove, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
			{ 0, 0, 0 }
		};
		
		static JSStaticValue staticValues[] = {
			{ "path", Filesystem_getPath, Filesystem_setPath, kJSPropertyAttributeDontDelete },
			{ "type", Filesystem_getType, 0, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
			{ "exists", Filesystem_getExist, 0, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
			{ "size", Filesystem_getSize, 0, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
			{ 0, 0, 0, 0 }
		};

		classDefinition.className = "Filesystem";
		classDefinition.attributes = kJSClassAttributeNone;
		classDefinition.staticFunctions = staticFunctions;
		classDefinition.staticValues = staticValues;
		classDefinition.finalize = Filesystem_Finalize;
		classDefinition.callAsConstructor = Filesystem_CallAsConstructor;
		
		filesystem_class = JSClassCreate(&classDefinition);
	}
	return filesystem_class;
}

int main(int argc, const char * argv[]) {

	JSContextGroupRef contextGroup = JSContextGroupCreate();
	JSGlobalContextRef globalContext = JSGlobalContextCreateInGroup(contextGroup, nullptr);
	JSObjectRef globalObject = JSContextGetGlobalObject(globalContext);

	JSObjectRef functionObject = JSObjectMakeFunctionWithCallback(globalContext, JSStringCreateWithUTF8CString("log"), ObjectCallAsFunctionCallback);
	JSObjectSetProperty(globalContext, globalObject, JSStringCreateWithUTF8CString("log"), functionObject, kJSPropertyAttributeNone, nullptr);
	
	JSObjectRef filesystemObject = JSObjectMake(globalContext, FilesystemClass(), nullptr);
	JSObjectSetProperty(globalContext, globalObject, JSStringCreateWithUTF8CString("Filesystem"), filesystemObject, kJSPropertyAttributeNone, nullptr);
	
	JSEvaluateScript(globalContext, JSStringCreateWithUTF8CString("var fs = new Filesystem('/Users/{user}/Desktop/file');log(fs.exists);"), nullptr, nullptr, 1, nullptr);
	
	return 0;
}

The above code represent a small implementation of the filesystem, lets start by describing the classDefinition.

  • The staticFunctions array represent the properties of the Filesystem JavaScript class it must be terminated with a null JSStaticValue. We set the kJSPropertyAttributeReadOnly attribute to indicate that this is a read only property, kJSPropertyAttributeDontDelete mean it should not be deleted.

  • The staticValues array represent the values of the class, the first item is the name the second item is the getter method and the third item is the setter and the fourth item is the attributes, when we want to create a read only property we simply don't provide a setter method.

  • The callAsConstructor property is our constructor Filesystem_CallAsConstructor, for simplicity our constructor does not check for the validity of the arguments passed to it and assumes everything is OK, the second parameter represent the object constructed by the base class constructor we allocate a new FilesystemPrivate struct and assign it to our object by calling JSObjectSetPrivate.

  • The FilesystemPrivate struct is the private data we assign to our JavaScript object to keep track of everything.

  • The setAttributes function takes a pointer to a FilesystemPrivate struct and a path and do some validity checks.

  • The FilesystemClass function creates the native JavaScript class representation, we set the initial value of classDefinition to kJSClassDefinitionEmpty to make all values to zeros to prevent any runtime errors.

  • Finally we define our class to JavaScript by calling JSObjectSetProperty.

When i executed the above code on my computer i got the following result, "there was a file named file on my desktop"

true

Summary

JavaScriptCore is a very powerful framework and enables you to create amazing things like game engines scripting or if you want to add automation to your applications or make a hybrid app, you can extend the the Filesystem class for example add a rename function or add methods for viewing permissions etc.