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:
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:
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!