This extension for Mac OS X serves as a demonstration of how to use js-ctypes to call Mac OS X Carbon, Core Foundation, and other system frameworks from an extension written entirely in JavaScript.
Note: This extension uses Carbon routines, which can no longer be used in Firefox add-ons now that Firefox is a 64-bit application.
You can download an installable version of this extension on AMO.
Once installed, when you right-click on an image, you'll see among the options in the contextual menu an option to "Add Image to iPhoto". Choose it, and iPhoto will start up (if it's not already running) and import the image.
The first thing we have to do is declare the Mac OS X APIs we'll be using. This extension uses a number of methods and data types, as well as constants, from three system frameworks.
Since a lot of this stuff is repetitive, we'll only look at selected parts of the code to get an idea how things work. You can download the extension and poke through the code inside it if you'd like to see all of it.
For the sake of organization, I chose to implement each system framework (and, mind you, I only declare the APIs I actually use, not all of them) as a JavaScript object containing all the types and methods that framework's API.
ctypes.voidptr_t
instead of a typed pointer. That's not really the ideal way to do things but saved some time for this simple example. Some of this may change as I refine the example in the future; I'll update the article if and when that happens.There are a few global data types used by all of our frameworks. These are declared near the top of the code:
const OSStatus = ctypes.int32_t; const CFIndex = ctypes.long; const OptionBits = ctypes.uint32_t;
OSStatus
CFIndex
CoreFoundation
object, and probably would have if I were being more formal, but opted against it for clarity's sake.OptionBits
The majority of the system routines we'll be using come from Core Foundation. Among these are routines for managing CFString
, CFURL
, and CFArray
objects, among others. These are core system data formats that are used by other frameworks, and we'll be making use of them.
The Core Foundation API is implemented by the CoreFoundation
object, which consists of two methods to initialize and shut down the library, a reference to the library, and all the types and methods declared to support Core Foundation.
The init()
method, which sets everything up, looks like this:
init: function() { this.lib = ctypes.open("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"); // declaring all the APIs goes here }
While the Core Foundation system framework itself doesn't need to be shut down, we do need to close the library we opened using the js-ctypes API; that's where the shutdown()
method comes in:
shutdown: function() { this.lib.close(); }
Let's take a look at a few of the key APIs we declare for Core Foundation, to see how it's done.
CFRange
is a structure that identifies a range; that is, it identifies an offset to an item in a list and a number of items. In C, the declaration looks like this:
typedef struct { CFIndex location; CFIndex length; } CFRange;
To declare this for use with js-ctypes, we use the following code:
this.CFRange = new ctypes.StructType("CFRange", [ {'location': ctypes.int32_t}, {'length': ctypes.int32_t}]);
This defines CoreFoundation.CFRange
to represent this data type, comprised of two 32-bit integer fields called location
and length
.
All Core Foundation data types are based upon a core CFType
data type. Basic CFType
routines handle memory management, dumping CFType
objects to the console, comparing CFType
values, and so forth. We'll be using a number of these methods, but for brevity's sake, since these are generally simple declarations, let's look at only the CFRelease()
and CFRetain()
declarations.
In C, these are declared thusly:
void CFRelease(CFTypeRef cf); void CFRetain(CFTypeRef cf);
In JavaScript, this translates to:
this.CFRelease = this.lib.declare("CFRelease", ctypes.default_abi, ctypes.void_t, ctypes.voidptr_t); // input: object to release this.CFRetain = this.lib.declare("CFRetain", ctypes.default_abi, ctypes.void_t, ctypes.voidptr_t); // input: object to retain
These methods are used to manage the reference counting for Core Foundation objects.
A CFString
is an opaque data type that contains a string. The string can be stored in any of a number of encodings, so you use assorted functions that know how to cope with different encodings to set and get values of CFString
s, as well as to perform typical string operations.
The first declaration to be done here is to actually declare the CFStringRef
data type; this is an opaque pointer to a CFString
object.
this.CFStringRef = new ctypes.PointerType("CFStringRef");
Now that we've declared the core type, we can declare the methods we use that work with CFString
objects. Let's take a look at one of these.
this.CFStringCreateWithCharacters = this.lib.declare("CFStringCreateWithCharacters", ctypes.default_abi, this.CFStringRef, // returns a new CFStringRef ctypes.voidptr_t, // allocator ctypes.jschar.ptr, // pointer to the Unicode string ctypes.int32_t); // length of the string
CFStringCreateWithCharacters()
is used to create a new CFString
object using a Unicode string as the source string, which is copied into the new CFString
object. It returns a CFStringRef
, which is a pointer to the new string, and accepts, as input, three parameters: an allocator, which is a pointer to a routine that will allocate the memory to contain the new object (we use the ctypes.voidptr_t
type for this), a pointer to the Unicode string to copy into the new string object (ctypes.jschar.ptr
), and the length of the Unicode string in characters.
The CFURL
type is used to describe a URL. It differs from a string in that it offers URL-specific methods for managing the content, and includes methods for converting between URLs and file system routine data formats such as FSRef
and Unix pathnames. We use a few of these routines because the Launch Services routine we'll be using to launch iPhoto and pass it the image to import uses CFURL
for the file references.
Let's take a look at two of the routines declared here:
this.CFURLCreateFromFileSystemRepresentation = this.lib.declare("CFURLCreateFromFileSystemRepresentation", ctypes.default_abi, this.CFURLRef, // returns ctypes.voidptr_t, // input: allocator ctypes.unsigned_char.ptr, // input: pointer to string CFIndex, // input: string length ctypes.bool) // input: isDirectory
This method is used to convert a Unix pathname into an URL. The interesting things to note about the declaration of CoreFoundation.CFURLCreateFromFileSystemRepresentation()
are:
CFURLRef
, which is an opaque pointer similar to the CFStringRef
we noted above.ctypes.unsigned_char.ptr
, which is a pointer to an unsigned character. "File system representation" strings on Mac OS X are in UTF-8 format.CFIndex
type here to specify the length of the string.this.CFURLGetFSRef = this.lib.declare("CFURLGetFSRef", ctypes.default_abi, ctypes.bool, // Returns a bool this.CFURLRef, // input: URL to convert ctypes.voidptr_t); // input: Pointer to FSRef to fill
The CoreFoundation.CFURLGetFSRef()
method is used to fill out a Carbon FSRef
structure to describe the location of a file represented by a CFURL
object. The main reason I include this here is because of the last parameter, which should be a pointer to an FSRef
, but that's not declared until we get around to declaring the Carbon API, and I think that's worth noting.
The CFArray
type is used to create arrays of objects; the objects in the array can be of any type, thanks to a set of callbacks you can provide to handle managing their memory and performing operations such as comparisons. The most interesting thing we'll look at here is how to reference the system-provided default callback record, which is exported by Core Foundation under the name kCFTypeArrayCallBacks
.
In C, the callback structure, and the predefined callback record, look like this:
typedef const void * (*CFArrayRetainCallBack)(CFAllocatorRef allocator, const void *value); typedef void (*CFArrayReleaseCallBack)(CFAllocatorRef allocator, const void *value); typedef CFStringRef (*CFArrayCopyDescriptionCallBack)(const void *value); typedef Boolean (*CFArrayEqualCallBack)(const void *value1, const void *value2); typedef struct { CFIndex version; CFArrayRetainCallBack retain; CFArrayReleaseCallBack release; CFArrayCopyDescriptionCallBack copyDescription; CFArrayEqualCallBack equal; } CFArrayCallBacks; CF_EXPORT const CFArrayCallBacks kCFTypeArrayCallBacks;
The kCFTypeArrayCallBacks
constant refers to a predefined callback structure referencing callback routines for managing arrays whose values are all CFType
-based objects, such as CFURL
, which is what we'll be using.
We need to be able to reference that predefined structure from our code. To do that, we first need to declare the CFArrayCallBacks
structure:
this.CFArrayCallBacks = new ctypes.StructType("CFArrayCallBacks", [ {'version': CFIndex}, {'retain': ctypes.voidptr_t}, {'release': ctypes.voidptr_t}, {'copyDescription': ctypes.voidptr_t}, {'equal': ctypes.voidptr_t} ]);
Having done this, we can then import the kCFTypeArrayCallBacks
structure. This is done using the js-ctypes library object's declare()
method, just like importing a function:
this.kCFTypeArrayCallBacks = this.lib.declare("kCFTypeArrayCallBacks", this.CFArrayCallBacks);
One thing about Core Foundation types that is interesting is the use of regular and mutable versions of the same data types. For example, the CFArray
type describes an array, but CFArray
objects can't be changed once they've been created.
However, obviously there are cases in which you'll want to be able to manipulate the contents of an array by adding and removing items, sorting them, and so forth. That's where the CFMutableArray
type comes into play. All CFArray
functions accept CFMutableArray
objects, so you can use CFMutableArray
with any routine that accepts a CFArray
as input, but CFMutableArray
supports additional functions that let you change the contents of the array.
There's nothing particularly interesting about how we declare this API, but it will be noteworthy when we look at how we use CFMutableArray
objects with methods that accept a CFArray
as input, so I introduce this concept here.
The Carbon API is the core operating system API derived from the classic Mac operating system. We actually aren't using any Carbon methods, but we are using one Carbon data type, the previously mentioned FSRef
structure. FSRef
is an opaque object describing a file.
In C, the FSRef
is declared thusly:
struct FSRef { UInt8 hidden[80]; /* private to File Manager; •• need symbolic constant */ }; typedef struct FSRef FSRef;
We declare it using js-ctypes like this:
this.struct_FSRef = new ctypes.StructType("FSRef", [ {"hidden": ctypes.char.array(80)}]);
The Carbon library init()
and shutdown()
routines are otherwise similar to how we do things for Core Foundation.
The Application Services framework consists of a number of different APIs that provide special services to applications. The Application Services API we'll be using is the Launch Services API, which is used to launch applications and open files in default (or specific, in our case) applications.
The function we'll be using is LSOpenURLsWithRole()
, whose declaration looks like this:
this.LSOpenURLsWithRole = this.lib.declare("LSOpenURLsWithRole", ctypes.default_abi, // ABI type OSStatus, // Returns OSStatus CoreFoundation.CFArrayRef, // Array of files to open in the app OptionBits, // Roles mask ctypes.voidptr_t, // inAEParam this.struct_LSApplicationParameters.ptr, // description of the app to launch ctypes.voidptr_t, // PSN array pointer CFIndex); // max PSN count
This function returns an OSStatus
indicating the result of the launch attempt, and accepts these parameters:
CFArrayRef
providing a list of CFURL objects for the files to open in the application.OptionBits
value providing a bit field of special options.voidptr_t
here.LSApplicationParameters
structure that describes what application to launchCFIndex
indicating the size of the array specified by the previous parameter.The LSApplicationParameters
structure is declared like this:
this.struct_LSApplicationParameters = new ctypes.StructType('LSApplicationParameters', [ {'version': CFIndex}, {'flags': OptionBits}, {'application': ctypes.voidptr_t}, // FSRef of application to launch {'asyncLaunchRefCon': ctypes.voidptr_t}, {'environment': ctypes.voidptr_t}, // CFDictionaryRef {'argv': ctypes.voidptr_t}, // CFArrayRef of args {'initialEvent': ctypes.voidptr_t}]); // AppleEvent *
Most of these fields, we won't be using. We'll get a look at how we use this shortly.
There are also a few constants used for the flags
field in the LSApplicationParameters
structure:
this.kLSRolesNone = 1; this.kLSRolesViewer = 2; this.kLSRolesEditor = 4; this.kLSRolesAll = 0xffffffff;
Now that the Mac OS X APIs we'll be using have been declared, we can write the core of the extension itself. This is done in the iPhoto
object in the extension's code.
On startup, we find the content area's context menu and add an event listener to it that will be called when the context menu is displayed. We'll use our handler for this event to add the "Add Image to iPhoto" option if the user has right-clicked on an image.
if (document.getElementById("contentAreaContextMenu")) { document.getElementById("contentAreaContextMenu").addEventListener("popupshowing", iPhoto.onPopup, false); }
When the user right-clicks an image, our handler gets called:
onPopup: function() { var node = iPhoto.getCurrentNode(); var item = document.getElementById("add-to-iphoto_menuitem"); if (item) { item.hidden = (node == null); // Hide it if we're not on an image } }
This code finds the image node the user right-clicked in by calling our getCurrentNode()
method, then sets the state of the "Add Image to iPhoto" menu item based on whether or not an image node was found.
The code to identify the node looks like this:
getCurrentNode: function() { var node = document.popupNode; // If no node, just return null now if (node == undefined || !node) { return null; } // Is it an image node? var elemName = node.localName.toUpperCase(); if (elemName == "IMG") { return node; } // Nope, return null return null; }
This starts by getting the node the popup was opened from. If this is null or undefined, we immediately return null, indicating there is no node associated with the context menu.
Otherwise, we fetch the name of the element and look to see if it's an <img>
element. If so, we return that node; otherwise, we return null.
The important thing to take away from this is that this method returns either null or the image node the user right-clicked on. If they right-clicked anything other than an image, it returns null.
When the user chooses to add the image to iPhoto, the add()
method is executed.
add: function() { var node = iPhoto.getCurrentNode(); if (node) { var src = node.getAttribute("src"); // Get the URL of the image if (src && src != "") { iPhoto.addImageByURL(src); } } }
This fetches the node representing the image the user wants to add, and, if it's an image, fetches the image's URL from its src
attribute, then passes it into our addImageByURL()
method, which will do all the heavy lifting.
The addImageByURL()
method handles actually retrieving the image and adding it to iPhoto. Let's take a look at its code, then explore how it works. This is where all our js-ctypes usage occurs.
addImageByURL: function(src) { CoreFoundation.init(); Carbon.init(); AppServices.init(); // Download the image var filePath = this.downloadImage(src); var mutableArray = CoreFoundation.CFArrayCreateMutable(null, 1, CoreFoundation.kCFTypeArrayCallBacks.address()); if (mutableArray) { var url = CoreFoundation.CFURLCreateFromFileSystemRepresentation(null, filePath, filePath.length, false); CoreFoundation.CFArrayAppendValue(mutableArray, url); CoreFoundation.CFRelease(url); // Call Launch Services to open iPhoto and deliver the image var ref = new Carbon.struct_FSRef; var appParams = AppServices.struct_LSApplicationParameters(0, 1, ref.address(), null, null, null, null); var appstr = "file:///Applications/iPhoto.app"; var appstrCF = CoreFoundation.CFStringCreateWithCharacters(null, appstr, appstr.length); var appurl = CoreFoundation.CFURLCreateWithString(null, appstrCF, null); CoreFoundation.CFRelease(appstrCF); var b = CoreFoundation.CFURLGetFSRef(appurl, ref.address()); if (!b) { var stringsBundle = document.getElementByID("string-bundle"); alert(stringsBundle.getString('alert_download_error_string')); } else { var array = ctypes.cast(mutableArray, CoreFoundation.CFArrayRef); AppServices.LSOpenURLsWithRole(array, 0, null, appParams.address(), null, 0); } CoreFoundation.CFRelease(appurl); // Clean up CoreFoundation.CFRelease(array); } AppServices.shutdown(); Carbon.shutdown(); CoreFoundation.shutdown(); }
This code begins by initializing all the system frameworks we're using, by calling the init()
methods on the CoreFoundation
, Carbon
, and AppServices
objects.
Then the downloadImage()
method is used to actually download the image to a temporary file. Once we have the file, we start making use of our native APIs.
The first step is to construct an array of URLs for all the files we want to open in iPhoto. In our case, we have only one file, but we still need an array. So we start by calling CoreFoundation.CFArrayCreateMutable()
to create a mutable array with room for one item, specifying the address of the standard callback routines exported by Core Foundation using the syntax CoreFoundation.kCFTypeArrayCallBacks.address()
.
If creating the array succeeded, we continue by creating a new CFURL
object from the pathname of the image file returned by the downloadImage()
method. This is done by calling the Core Foundation routine CFURLCreateFromFileSystemRepresentation()
. Conveniently, we can simply pass in the JavaScript string, filePath
, as the string and filePath.length
as its length.
The array is then built by using CFArrayAppendValue()
to add the new CFURL
to the array. Doing this causes the array to retain the URL object, so we can use CFRelease()
to release it now.
Next, we need to build the parameters for the LSOpenURLsWithRole()
function, then call it to start up iPhoto.
The first step here is to create a new FSRef
object to contain the reference to the iPhoto application itself, since LSOpenURLsWithRole()
uses an FSRef
to specify the application to launch.
Then we build the LSApplicationParameters
structure describing the application to launch. Let's take a closer look at this syntax:
var appParams = AppServices.struct_LSApplicationParameters(0, 1, ref.address(), null, null, null, null);
Here you're calling a constructor, created for you by js-ctypes, that creates and fills out the structure, specifying the values of all of the parameters. To specify a pointer to the FSRef
indicating the application to launch, we pass ref.address()
, which obtains the actual memory address of the C data structure.
Note that so far, we haven't actually obtained a value for the FSRef
in question. We do that next by following these steps:
CFString
referring to file:///Applications/iPhoto.app
, which is iPhoto's default path, using CFStringCreateWithCharacters()
.CFURL
object by calling CFURLCreateWithString()
.CFRelease()
, since we're done with it.CFURLGetFSRef()
to fill out the FSRef
structure to reference the same file as the CFURL
.If that fails, we display an error; otherwise, we cast the CFMutableArrayRef
into a CFArrayRef
by calling ctypes.cast()
, then call LSOpenURLsWithRole()
to actually send the image to iPhoto.
After doing that, we clean up after ourselves by releasing the CFURL
object and the array, then shutting down the three libraries we used.
The downloadImage()
method handles actually downloading the image to a temporary file; it then returns the local pathname of the downloaded file to the caller.
downloadImage: function(src) { // Get the file name to download from the URL var fileName = src.slice(src.lastIndexOf("/")+1); // Build the path to download to var dest = Components.classes["@mozilla.org/file/directory_service;1"] .getService(Components.interfaces.nsIProperties) .get("TmpD", Components.interfaces.nsIFile); dest.append(fileName); dest.createUnique(dest.NORMAL_FILE_TYPE, 0600); var wbp = Components.classes['@mozilla.org/embedding/browser/nsWebBrowserPersist;1'] .createInstance(Components.interfaces.nsIWebBrowserPersist); var ios = Components.classes['@mozilla.org/network/io-service;1'] .getService(Components.interfaces.nsIIOService); var uri = ios.newURI(src, document.characterSet, gBrowser.selectedBrowser.contentDocument.documentURIObject); wbp.persistFlags &= ~Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION; // don't save gzipped wbp.saveURI(uri, null, null, null, null, dest); return dest.path; }
This is pretty straightforward, typical Mozilla code. It gets the filename of the file being download by slicing it off the end of the specified image URL, then obtains the path to the temporary items folder and appends the image file's name to that path. Then we call createUnique()
to create a unique file by that name (or a derivative thereof if the name is already in use), and download the contents of the image file to that local file.
This is a fairly simple example of how to use js-ctypes, but it actually does something useful, and should be a helpful demonstration not just for how to use js-ctypes, but also more specifically for developers that want to interface with Mac OS X system frameworks.