Migrating MacRuby scripts to Objective-C

Published
20 December 2013
Tagged

First signs of decay

After upgrading to Mavericks, I found that a few of my MacRuby scripts had stopped working. Apparently it's due to some part of the optparse package, which breaks upon upgrading, and I'm not the only one getting the error. The issue has been open for about a month now, and there doesn't appear to be any response from the devs.

This isn't to say that the project's defunct, but this has happened before to promising ruby-cocoa bridging tools. It's symptomatic (I believe) of a more general trend, in which people build a cool bridge, but Apple has no responsibility to keep it working. When Apple upgrades various portions of the OS, these bridges stop working, which then mean that the devs have to scramble to fix their own products.

Since there's no obvious patch in sight, I decided to try a different tack: resurrecting my old MacRuby "glue" scripts in Objective-C. It turns out it's not too hard.

The overall plan

While I'm not completely incompetent when it comes to Objective-C, ruby is still my mother tongue, so I've adopted the following strategy:

  1. Objective-C program fetches data from Mac apps, and deposits in some sort of file (plaintext, JSON, sqlite).
  2. Ruby program fetches data from the file and does whatever to it.

This means that the Objective-C program can be pretty simple: it just grabs the data, parses it however, and then outputs it. While in the past I've favoured "light" formats like YAML and JSON, these days I'm considering scale: if I'm going to be logging events twice a day for several years, my database file will end up with a considerable number of data points, and gut feeling tells me that sqlite might be the way to go[1].

The setup

There's two ways to make Objective-C binaries: either through Xcode, or with your favourite text editor and clang.

Xcode

I would probably recommend this way. While it means you have to adhere to some of Apple's policies and guidelines regarding build settings and so on, the Xcode environment is really handy when you have to remember whether you want [NSPredicate predicateWithString:] or [NSPredicate predicateWithFormat:].

Upon opening Xcode, select "Create a new Xcode project":

The Xcode landing page.

The Xcode landing page.

Now select "Command Line Tool" under "OS X/Application" to get yourself started.

After naming your baby, you'll be all ready to start coding, with a sample main.m file already populated.

Going it alone

My first experiments with Objective-C command line tools were compiling it myself. I grabbed the basic formula from this post over on Cocoa with Love, and I actually ended up putting the compilation commands in a Rakefile:

desc "Build using clang"
task :build do
  sh "clang -ObjC main.m -o main"
end

task default: :build

I've switched gcc for clang here: it's what Apple uses in Xcode, and I imagine is better supported[2].

I highly recommend using Rakefiles (or language-specific equivalent) if you're going to take this path: since this makes compilation and testing a whole bunch easier. The flags on clang can also be expanded - for example, you might want to run it with -fobjc-arc set to enable ARC in your project.

Hello, world!

If you're building through Xcode, you'll already have a skeleton of a program. If you're doing this on your own, you'll need a framework around which to build your program. My first program looked something like this:

#import <Cocoa/Cocoa.h>

int main() {
  NSLog(@"Hello, world!");
  return 0;
}

You need to import Cocoa here so you get access to NSLog. However, if you try to compile this as-is (using the Rakefile above, for example), you'll get an error:

Undefined symbols for architecture x86_64:
  "_NSLog", referenced from:
      _main in test-O8fjCl.o
  "___CFConstantStringClassReference", referenced from:
      CFString in test-O8fjCl.o
ld: symbol(s) not found for architecture x86_64

Every time you include something system-level (like Cocoa), you need to let clang know you're going to need to add a framework to your build. Thankfully, this is really easy. Just add the flag -framework Cocoa to clang and you're set[3].

Incidentally, I found myself using NSLog all over the place while putting my script together - it's probably the best way to get information about your program if you're not running in Xcode (which at least has a debugger).

Integrating ScriptingBridge

The reason I built MacRuby scripts in the first place was because they could communicate with OS X applications via the incredibly useful ScriptingBridge framework. It turns out you can do this in Objective-C as well - you just need to prepare a couple of files first.

Including the framework

The heart of ScriptingBridge is its framework - this gives you all the wonderful commands that you can use to talk to other programs. To include it, you need to add the ScriptingBridge framework, either by adding the framework in the Rakefile, or by adding it in the appropriate build phase in Xcode:

Adding a framework in Xcode

Adding a framework in Xcode

This gives you access to the SBApplication class, and its most important (for us) method: +[SBApplication applicationWithBundleIdentifier:]. This handy method lets you interact with any application you care to name - assuming the developers played nice and give said application ScriptingBridge support. It turns out the kind folks at OmniGroup are just such people:

#import <ScriptingBridge/ScriptingBridge.h>

SBApplication *omniFocus = [SBApplication applicationWithBundleIdentifier:@"com.omnigroup.omnifocus"];

Fetching scripting definitions

However, in order to actually do things with the application object that ScriptingBridge, we need to fetch a list of methods and properties for a given application and its objects. These methods and properties are encoded in a .h file, and there's a couple of tools to create such a file. I found the relevant code via CocoaBuilder, and I actually ended up slotting it into my Rakefile. It looks like this:

task :headers do
  sh "sdef /Applications/Productive/OmniFocus.app | sdp -fh --basename OmniFocus"
end

The application sdef collects the ScriptingBridge definitions from an applications and outputs it as XML. It gives you useful descriptions of properties and methods, and it's human-readable, but it's nothing that Objective-C can do anything with. By running it through sdp (a sdef processor) we can turn it into a .h file, which we then include in our application bundle[4].

Your new .h file is a good reference for your application's properties, classes and methods: for example, OmniFocus.h gives me classes like OmniFocusApplication, OmniFocusTask, and OmniFocusProject, which are handy as all get-out for manipulating and recording tasks.

Now you should have the ScriptingBridge framework included in your binary, and the definitions for your favourite application locked and loaded. Now you get to have fun fetching, filtering and formatting your data.

Spanning across files with clang

As my project got bigger, I started naturally pushing methods into classes, DRYing up bits of code, and so on. Some of these went into separate .m files with their associated headers, but I ended up stuck on how to add these in to my application when compiling with clang. Memories of my old C days told me I had to do something with...linkers? And makefiles? And I was pretty sure there were some .o files involved somewhere down the line.

After a bunch of messing around, I found out it was easier than I thought. If you have methods and objects in the files foo.m, bar.m and main.m, you can compile and link them all with clang incredibly easily:

clang -ObjC -framework Cocoa main.m foo.m bar.m -o appname

As far as I'm aware you don't even have to put main.m first: clang will work all that out for you. This is another example where the Rakefile comes in handy, as the command to compile your app gets longer and longer.

Outputting files

By now you'll have fetched your data, filtered it as you feel necessary, and used NSLog to make sure you're fetching what you want. The next question is: how should you output it?

I started off with a simple plaintext output - for logging or basic info, this might be all you need. NSString has a pretty low-level method for output, actually:

NSString *outputData = @"foo";
[outputData writeToFile:@"output.txt" atomically:YES encoding:NSUTF8StringEncoding error:nil];

Now your data's ready to be processed by your favourite language. If, however, you've got a bit more data, you might want to look at some of the many libraries out there for parsing data. I've had a brief look at JSONKit, which is nice library for encoding basic objects as JSON, but watch out: it doesn't play nicely with ARC, so if you want to use it for serialisation, you might have to brush up on your retain/release.

Since long-term recording of OmniFocus tasks may well figure in my near future, I'm currently looking at FMDB for writing data to sqlite. So far the syntax seems pretty simple, although I'm wrapping everything in a JRDatabaseManager class to keep raw SQL from littering all my classes. Setting up an sqlite database is a bit more investment than simply outputting to JSON, and I'm currently setting up the required methods before giving the whole thing a test run.

TL;DR

That brings us up to date with my Objective-C experiments. Work is currently on hold due to Ph.D.-related deadlines, but I expect to have my first sqlite database transactions happening within the month (when pressure should let up on other areas of my life). The plan after that is to continue using Objective-C scripts to store data in some sort of "quantified self" database on my drive. By picking Apple's favourite language, I'm hoping that these scripts will outlast at least a few more operating system upgrades, freeing me up to pursue other useless tasks in my life.


  1. Apple already seems to be pursuing this trend. ↩︎

  2. I actually found that my copy of /usr/bin/gcc is symlinked to /usr/bin/clang: I have no idea if this is standard, or if I did this at some point to fix a bug or some-such. The lack of an equivalent /usr/bin/_gcc suggests that Apple does this as standard in OS X, though. ↩︎

  3. We're going to be including another framework soon - this is why Rakefiles are a nice way of simplifying the build process. ↩︎

  4. Incidentally, I had a couple of compile warnings with this header - a property or two were defined twice in the file. I was able to comment out one of the duplicate definitions without problems, and this made the problem go away. Since you only have to generate these files once (barring application updates that significantly alter the scripting interface of the app), this sort of thing isn't too tedious. ↩︎