Calling AppleScript Handlers from Cocoa
If you write software for Mac OS X, and you’ve ever needed to do interapplication communication, chances are you’ve waded at least a little bit into the murky waters of AppleEvents. If your task involved getting another application to do anything remotely complicated, however, you probably decided fairly quickly that constructing whole hierarchies of raw AppleEvent descriptors is for the birds, and that there is a much nicer, high level interface to the AppleEvents infrastructure: AppleScript.
Cocoa has had simple support for programmatically executing AppleScripts since Jaguar, in the form of NSAppleScript. Unfortunately, though, NSAppleScript’s usefulness in the scenario I’ve described is somewhat limited, since it only provides support for executing an entire AppleScript from start to finish, without any way of passing arguments.
A preferable solution would allow the Cocoa programmer to call a specific handler within the AppleScript, almost as if it was a normal function call. This would allow essentially all of the interapplication communication code to be implemented in AppleScript and encapsulated within the handler, and the data being sent to the other application provided through the handler’s parameters.
Fortunately, while Cocoa may not provide this facility directly, it does at least provide the raw tools to make it happen. After a lot of trial and error, I managed to distill the necessary magic into a relatively straightforward NSAppleScript category with a single method:
- (NSAppleEventDescriptor *) callHandler: (NSString *) handler withArguments: (NSAppleEventDescriptor *) arguments errorInfo: (NSDictionary **) errorInfo;
The first parameter is simply the name of the handler to be called. The second is an AppleEvent descriptor containing the arguments. The third is an NSDictionary reference that will point to error information in the event that something goes wrong. The method returns an AppleEvent descriptor containing the handler’s return value.
The high level outline for how to use this category in your own code is as follows:
- Open Script Editor and create a script containing the handler to be called. Make sure to save the script as a .scpt file containing a compiled data fork (as opposed to a simple text file containing the script source)—otherwise you will likely run into -1708 (errAEEventNotHandled) errors on Jaguar.
- Add the script to the appropriate target in your Xcode project and make sure it gets copied into the Resources folder of your app bundle when you build.
- In your Cocoa code, load the script file out of the bundle and into an NSAppleScript instance.
- Construct an NSAppleEventDescriptor containing the arguments to be sent to the handler.
- Send your NSAppleScript instance the “callHandler:withArguments:errorInfo” message using the argument vector you constructed the previous step (make sure you are importing the NSAppleScript+HandlerCalls category).
All of this is illustrated and explained in more detail in an Xcode project I have posted (disclaimer: the code is provided under a Creative Commons License, and represents my own late night hacking—not some sort of officially endorsed code sample from my employer or anything like that :-)):
- ASHandlerTest.dmg (27k)
Enjoy, and let me know if anyone has any interesting feedback (it’s amazing how much I end up learning from other people when I post code on this site!).
(Update: The ever vigilant Michael Tsai pointed out that I had a misplaced “release” in the earlier version of this code (I copied the category code from a larger, non-category method I originally wrote and forgot to get rid of the “release” of an object that is now passed in as an argument). The code is fixed now.
April 28th, 2005 at 9:36 am
Why not use symbolic names from instead of using hardcoded four character codes? Like kASAppleScriptSuite, kASSubroutineEvent, and keyASSubroutineName.
I don’t think it’s required to set the targetDescriptor: parameter when making the Apple Event to send to a script. I just use +[NSAppleEventDescriptor nullDescriptor] and it works fine.
September 14th, 2005 at 4:40 pm
The fatal flaw is this line:
subroutineDescriptor = [NSAppleEventDescriptor descriptorWithString: handler];
It should be:
subroutineDescriptor = [NSAppleEventDescriptor descriptorWithString: [handler lowercaseString]];
The example stacks the deck with a handler whose real name *is* lowercase, but of course in real life that’s unlikely, and the example code will break. For more info, see:
http://developer.apple.com/qa/qa2001/qa1111.html
And for a working formulation:
http://www.cocoadev.com/index.pl?CallAppleScriptFunction
Hope this helps - m.
November 20th, 2005 at 10:21 pm
Whew! I had a need today to call an AppleScript from a cocoa app - and be able to target a specific handler inside that script. I read through all the NSAppleScript and NSappleEventDescriptor docs, read a few things on the web and was figuring I’d have to write something like this! Now I don’t, and you have no idea how happy that makes me
Thanks, Buzz.