The Shape of Everything
A website mostly about Mac stuff, written by August "Gus" Mueller
» Acorn
» Retrobatch
» Mastodon
» Micro.blog
» Instagram
» Github
» Maybe Pizza?
» Archives
» Feed
» Micro feed
January 9, 2009
(This post is from my old, old, super old site. My views have changed over the years, hopefully my writing has improved, and there is now more than a handful of folks reading my site. Enjoy.)

I gave a presentation last night at Xcoders about JSCocoa, which is a bridge between JavaScript and Cocoa (using Apple's JavaScriptCore, which is the same JavaScript engine in WebKit).

ThatsALotOfInterCaps.

Anyway, I thought I would briefly summarize the talk and share some code. The short version is "I really like JSCocoa". Here's the long version:

The Bad: JSCocoa has got some amazing KVC hacks in it which I think would drive me insane if I ever actually saw them used. Here's how you would get the current application name in Cocoa:

NSString* appName = [[[NSWorkspace sharedWorkspace] activeApplication] objectForKey:@"NSApplicationName"];

And this will work in JSCocoa:

var appName = NSWorkspace.sharedWorkspace.activeApplication.NSApplicationName

No strings, no obvious method calls, just ... well, what exactly is going on here? If you hadn't seen the Cocoa code above you'd probably be thinking... wtf?

Here's what I'd call the sane way to do it in JSCocoa:

myDictionary = NSWorkspace.sharedWorkspace().activeApplication() var appName = myDictionary.objectForKey_("NSApplicationName")
It is a bit more obvious what's going on in this code. We're actually naming a variable that tells us what type it is, and we're using a string to get a value out of the dictionary. While KVC hacks can be awesomely cool and amazing and generally useful... it is possible to go a bit too far. So don't use that sample code, or I'll come looking for you and slam your toe in a door or something equally painful for you and very satisfying for me.

The Meh:

Holy crap you can write whole apps in almost 100% JavaScript! This is amazingly useful, and I bet the Konfabulator folks wish they had this years ago. I say almost 100% because you do need a tiny bit of objc code to start off your JS app: %-

 int main(int argc, char *argv[]) {
  [[NSAutoreleasePool alloc] init];
  id c = [JSCocoaController sharedController];
  id mainJSFile = [NSString stringWithFormat:@"%@/Contents/Resources/main.js",
    [[NSBundle mainBundle] bundlePath]];
  [c evalJSFile:mainJSFile];
  return NSApplicationMain(argc,  (const char **) argv);
}

-%

That's not too bad. You can even hook up your JS objects (and objc subclasses!) in Interface Builder. Neato.

So why is this "Meh"? I think I'd shoot myself if I had to code in JavaScript all day long. I like my brackets and my debugger. However, if someone paired up JSCocoa with Cappuccino's preprocessor and we had "ObjCScript", I'd probably wet myself.

The Awesome:

Holy crap my customers can writing their plugins in JavaScript (If I hadn't already told them to use Lua and/or Python already)!!!11

Running a JavaScript file from your app is ridiculously easy:

[[JSCocoaController sharedController] evalJSFile:pathToSomeFile];

As an example I decided I wanted the ability to run JavaScript from within Acorn (I did the same thing for Coda a little while back). Here's some quick requirements for an Acorn JavaScript plugin:

a) The plugin has to live in a plain text file ending with .js or .jscocoa in your ~/Library/Application Support/Acorn/Plug-Ins/ folder. b) The plugin has to have at least a main() method that takes a single parameter, which is a CIImage. c) The main() method has to return a CIImage, or nil if it decided to do something else than alter the current image.

I turns out this is pretty easy to do, and I've got the source up on a project in my google code svn repository. Or you can just download JSEnabler.acplugin, and put those files in your Acorn Plug-Ins folder if you don't want to play with a compiler.

(Just a quick note- I've moved these files to http://code.google.com/p/flycode/source/browse/trunk/jstalk/extras/acornplugin.)

And finally, here's the bit of ObjC code that you use to call the main() method, with a single argument, in a particular JS file. Ideally, JSCocoaController would have a [c callMethod:@"main" withArguments:arrayOfArguments] to make all this moot.. but it doesn't (yet).

First, the JavaScript file, named "Grayscale.py": %-

function main(image) {
    color = CIColor.colorWithRed_green_blue_(0.5, 0.5, 0.5)
    filter = CIFilter.filterWithName_('CIColorMonochrome')
    filter.setDefaults()
    filter.setValue_forKey_(image, 'inputImage')
    filter.setValue_forKey_(color, 'inputColor')
    filter.setValue_forKey_(1, 'inputIntensity')
    return filter.valueForKey_('outputImage')
}

-% Just some simple Core Image filter stuff, but in JavaScript.

And the method in the Acorn plugin that does the real work, which will hopefully make sense to the ObjC folks out there:

%-

- (CIImage*) executeScriptForImage:(CIImage*)image scriptPath:(NSString*)scriptPath {

    NSError *err            = 0x00;
    NSString *theJavaScript = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:&err];

    // JSCocoaController is a singleton object which holds a single JavaScript context which gets used over and over.
    JSCocoaController *jsController     = [JSCocoaController sharedController];
    JSGlobalContextRef ctx              = [jsController ctx];

    // evaluate our script, which has "function main(image) { ... }" in it somewhere.
    // or at least we hope it does.
    [jsController evalJSString:theJavaScript];

    // now we're going to get a reference to our main(image) method, by asking the JS context for it, and stuff it
    // in a var named "jsFunctionObject"
    JSValueRef exception            = 0x00;   
    JSStringRef functionName        = JSStringCreateWithUTF8CString("main");
    JSValueRef functionValue        = JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), functionName, &exception);

    JSStringRelease(functionName);  

    // Check for errors and bail if there are any.  formatJSException: is a handy way of way of printing out line
    // numbers and such.
    if (exception) { 
        NSLog(@"%@", [jsController formatJSException:exception]);
        return nil;
    }

    JSObjectRef jsFunctionObject = JSValueToObject(ctx, functionValue, &exception);

    if (exception) { // Once again, check for errors and bail if there are any
        NSLog(@"%@", [jsController formatJSException:exception]);
        return nil;
    }


    // Now that we've got a handle to the function we want to call, we need to push our cocoa object into a 
    // javascript object, which we'll pass to the main() method.

    JSValueRef imageArgRef;
    [JSCocoaFFIArgument boxObject:image toJSValueRef:&imageArgRef inContext:ctx];

    // This is the array that holds the args we pass to our main() method.  We've just got one argument, which
    // is our ciiimage
    JSValueRef mainFunctionArgs[1] = { imageArgRef };

    // finally, call the function with our arguments.
    JSValueRef returnValue = JSObjectCallAsFunction(ctx, jsFunctionObject, nil, 1, mainFunctionArgs, &exception);

    if (exception) { // Bad things?  If yes, bail.
        NSLog(@"%@", [jsController formatJSException:exception]);
        return nil;
    }

    // Hurray?
    // The main() method should be returning a value at this point, so we're going to 
    // put it back into a cocoa object.  If it's not there, then it'll be nil and that's 
    // ok for our purposes.
    CIImage *acornReturnValue = 0x00;

    if (![JSCocoaFFIArgument unboxJSValueRef:returnValue toObject:&acornReturnValue inContext:ctx]) {
        return nil;
    }

    // fin.
    return acornReturnValue;
}

-%

Update:

Patrick Geiller (the author of JSCocoa) wrote me and pointed some things out to me, which should be noted.

There are functions for calling javascript methods. doh! %-

- (JSValueRef)callJSFunctionNamed:(NSString*)name withArguments:(id)firstArg, ...

// By function reference :

  • (JSValueRef)callJSFunction:(JSValueRef)function withArguments:(NSArray*)arguments
-%

JSCocoa also has a Google Group http://groups.google.com/group/, and is hosted on Github now: http://github.com/parmanoir/jscocoa/tree/master

And Patrick also adds this:

The a.b.c dot syntax is by default the only way to call objects, therefore myDictionary = NSWorkspace.sharedWorkspace().activeApplication() will fail unless you first call [[JSCocoaController sharedController] setUseAutoCall:NO];

This changed because if (a.b.c) needed to be written with extra parentheses : if (a.b.c()) Setting autocall makes things consistent as a.b.c and a().b().c() can no longer be mixed in the same file.

Thanks Patrick!