Scripting Elysium using MacRuby

For a couple of months now my hobby has been working on a Cocoa application, code name "Elysium" for generating music. If you want to read a little of the background I posted about it earlier.

Something I've had in mind, almost from the beginning, was that Elysium should be scriptable. That, through the ability to script parts of the application, you can have more control over the musical strategy. For example a script might regulate how often new playheads are generated in order to control the "energy" of a piece.

But how to script a Cocoa application?

My original plan had been to use Nu however, although I still find Nu an interesting idea, I've not spent much time with it recently and it's continued lack of support for Objective-C garbage collection put me off.

With Nu out of the picture I switched my attention to Javascript. It's not perfect but it's pretty good and the JavascriptCore framework comes with Leopard (which is fine since using GC makes my app Leopard only anyway). With all the news of JSCore performance enhancements via SquirrelFish this seemed a solid choice.

Embedding JSCore is simple, just copy the framework into your project. That's when the fun begins though. As soon as I started trying to use it I realized that JSCore is unbridged. This means that, in order to expose Elysium's Objective-C based objects to Javascript I would have had to implement a set of "facades" that would emulate JS classes.

There is an example, JSPong, and while you know it's not impossible it's pretty tedious (and not helped by JSCore very thinly documented, and I'm being generous there) and results in a heavy stink of pollution of your model classes. In short, right now, using JSCore as a Cocoa scripting language is a mess. Worse still I knew, from the Nu experience, that it was totally unnecessary mess.

While casting around for how to bridge Javascript I came across F-Script. F-Script is a Smalltalk like language that is beautifully bridged with Cocoa. In less than an hour I was scripting Elysium. For example here is a callback that tells a note not to play on beats divisible by 7.

[:noteTool :playhead | noteTool hex layer beatCount rem: 7 == 0 ifTrue:[ noteTool setSkip:YES ]]

This is an F-Script block (which will be familiar to anyone who has used Smalltalk or Ruby) where the noteTool and playhead arguments are Cocoa objects being passed, transparently, from Objective-C. Equivalently the F-Script code can call the setSkip: method in the other direction.

The F-Script Block class even used a category on NSString so you could write:

[[@"some f-script code" asBlock] value]

to evaluate arbitrary F-Script code from Objective-C. Very nice.

However as nice as this was F-Script felt very alien and I could see it being a stumbling block to other people playing with Elysium (due out RSN). All the time I was using F-Script I was thinking "If only I could get Javascript bridged like this."

In the end I hunted down the author of the Leopard BridgeSupport tool, Laurent Sansonetti to ask him. His reaction was "Why not use Ruby?"

Why not indeed?

Well because I've done it before and it doesn't work very well.

"But what about MacRuby?"

It turns out that MacRuby is getting pretty real, is bridged just like F-Script, uses Objective-C collections and even shares the Objective-C garbage collector (which JSCore would not have done). Given that I love Ruby and would prefer to write it than F-Script or Javascript I was sold.

So, you've read the boring pre-amble. How is it done?

First you have a choice, use MacRuby 0.3 (the last release) or Trunk. Laurent is still working on the GC code which can make trunk unreliable but, for various reasons, that's where I ended up. While working on this Laurent cooked up a MacRuby API for Objective-C which is a pretty good reason to pick trunk (or wait for 0.4).

So checkout MacRuby trunk:

svn co http://svn.macosforge.org/repository/ruby/MacRuby/trunk MacRuby

By default MacRuby is setup to be installed in /Library/Frameworks which we don't want. We want it ready to embed in an app, so from the command line:

rake BUILD_AS_EMBEDDABLE=true
DESTDIR=/tmp/build rake install

this will "install" MacRuby out of the way so you can pick out the framework and delete the rest. From your Cocoa project, copy the framework in.

cp -R /tmp/build/Library/Frameworks/MacRuby.framework .

So, now you have the framework. In your Xcode project (or wherever) add MacRuby.framework as a linked framework (don't forget to also add it to your copy frameworks build phase).

From here it depends on what you want to do but I will describe what I did.

I wanted the user to be able to edit small snippets of code that would be attached as callbacks in various places (e.g. when a layer runs or has run, or when a tool is about to run). My experience with F-Script told me I wanted some kind of RubyBlock class and a similar category on NSString for making them.

In my code I have an NSWindowController subclass to handle inspectors that allow callback management for example:

#import "RubyBlock.h" // in turn #import <MacRuby/MacRuby.h>

- (void)editWillRunScript:(ELTool *)_tool_ {
  RubyBlock *block;
  if( !( block = [[_tool_ scripts] objectForKey:@"willRun"] ) ) {
    block = [[NSString stringWithFormat:@"do |%@Tool,playhead|\n# write your callback code here\nend\n", [_tool_ toolType]] asRubyBlock];
    [[_tool_ scripts] setObject:block forKey:@"willRun"];
  }
  [block inspect];
}

Here I steal another trick from F-Script in providing inspect on the RubyBlock class and an associated ScriptInspectorController (and Xib) that pops up an editing window. Right now it's no more than an NSTextView with an 'ok' and 'cancel' buttons but it's still a nice to have.

This block encapsulates a Ruby proc and can be called via one of:

[block eval]
[block evalWithArg:x]
[block evalWithArg:x arg:y];

and of course we could add more or a generic that takes an array or something.

Under the hood the RubyBlock class uses one method from Laurents shiny new MacRuby class to generate the proc:

- (void)setSource:(NSString *)_source_ {
  source = _source_;
  NSString *procSource = [NSString stringWithFormat:@"proc %@", source];
  proc = [[MacRuby sharedRuntime] evaluateString:procSource];
}

Pretty simple. We make a proc out of anything we're passed. Now I also added (with a lot of help from Laurent) some methods to MacRuby via category for eval'ing procs. I won't go into the implementation here but it means, for example, that RubyBlock can evaluate as simply as:

- (id)evalWithArg:(id)_arg1_ arg:(id)_arg2_ {
  return [[MacRuby sharedRuntime] evalProc:proc arg:_arg1_ arg:_arg2_];
}

I'll be making the source to RubyBlock and the categories NSString+AsRubyBlock and MacRuby+EvalProcs available either separately or as part of MacRuby itself. If you're in a hurry send me an email and I can share them with you now.

The upshot is that by embedding MacRuby I can now very simply create Ruby procs that can be called from Objective-C using a convenient syntax and that can interact with the Objective-C quite naturally. This is a big usability win, not least because it means I can write Ruby for my scripts.

I'm very grateful to Laurent Sansonetti both for his herculean efforts with MacRuby and for his patient efforts with me.

For those of you who are interested in generative music a public beta release of Elysium is imminent.

09/10/2008 22:14 by Matt Mower | Permalink | comments: