9781449380373
_macruby_in_objective_c_projects.html

Chapter 11. MacRuby in Objective-C Projects

MacRuby is great, but you might already have a Cocoa application written in Objective-C. Rewriting all of it in MacRuby would probably be fun, but maybe not really wise. You might also want to experiment with MacRuby without fully committing to it yet. Another reason to mix the two languages is if you want to better test your Objective-C code, but are not pleased by the existing Objective-C tools. Whatever reason you have, MacRuby is really easy to use within your existing app.

API

MacRuby is a framework, so to use it in your Objective-C project, you just need to add it to your project and import the headers:

#import <MacRuby/MacRuby.h>

Once you have added the framework and imported the headers, you can call out to files or snippets of MacRuby from Objective-C. Let’s take a minute to look at the API provided by the MacRuby framework:

#import <Foundation/Foundation.h>

@interface MacRuby : NSObject

/* Get a singleton reference to the MacRuby runtime, initializing it before if
 * needed. The same instance is re-used after.
 */
+ (MacRuby *)sharedRuntime;

/* Evaluate a Ruby expression in the given file and return a reference to the
 * result.
 */
- (id)evaluateFileAtPath:(NSString *)path;

/* Evaluate a Ruby expression in the given URL and return a reference to the
 * result. Currently only file:// URLs are supported.
 */
- (id)evaluateFileAtURL:(NSURL *)URL;

/* Evaluate a Ruby expression in the given string and return a reference to the
 * result.
 */
- (id)evaluateString:(NSString *)expression;

/* Load the BridgeSupport file (<<bridgesupport-intro>>) at the given path.
 */
- (void)loadBridgeSupportFileAtPath:(NSString *)path;

/* Load the BridgeSupport file at the given URL.
 */
- (void)loadBridgeSupportFileAtURL:(NSURL *)URL;

@end

@interface NSObject (MacRubyAdditions)

/* Perform the given selector and return a reference to the result. */
- (id)performRubySelector:(SEL)sel;

/* Perform the given selector, passing the given arguments and return a
 * reference to the result. The argv argument should be a C array whose size
 * is the value of the argc argument.
 */
- (id)performRubySelector:(SEL)sel withArguments:(id *)argv count:(int)argc;

/* Perform the given selector, passing the given arguments and return a
 * reference to the result. The arguments should be a NULL-terminated list.
 */
- (id)performRubySelector:(SEL)sel withArguments:(id)firstArgument, ...;

@end

Usage

The MacRuby module you imported is the key channel for turning MacRuby code into something the Objective-C code can use. In the following code, the module’s evaluateString method transforms MacRuby code into a generic object that the Objective-C code refers to:

#import <MacRuby/MacRuby.h>

int main(void) {
  NSString *source =  @"module Greetings def self.hello; NSLog('Hello World!');\
   end; end; Greetings";
  id greetings = [[MacRuby sharedRuntime] evaluateString:source];
  [greetings performRubySelector:@selector(hello)];

  return 0;
}

Save the file as hello_from_macruby.m and run the following command:

$ gcc hello_from_macruby.m -o hello -framework Foundation -framework MacRuby -fobjc-gc

Now execute the output file as follows:

$ ./hello
2011-05-23 22:39:19.342 hello[90424:903] Hello World!

The code doesn’t do much. It defines a string called source, which contains some Ruby code. The Ruby code defines a module called Greetings with a hello method, printing the famous “Hello World!” string. The MacRuby string’s final statement returns a reference to the Greetings module.

We then evaluate this string using evaluateString, as mentioned, and assign the value returned (the last value returned by the evaluated MacRuby source) to a variable called greetings of type id. Using the id type in Objective-C is a way of casting variables as a generic object type. In other words, it’s a way to use dynamic typing in Objective-C.

Once we assign our MacRuby module to an Objective-C variable, the Objective-C class has access to the methods within the MacRuby module. So we can dispatch the hello method by calling performRubySelector: and passing the method as a selector.

We can simplify this example by doing the following:

#import <MacRuby/MacRuby.h>

int main(void) {

    NSString *source = [NSString stringWithFormat:@""
    "module Greetings;"
    "  def self.hello;"
    "    NSLog('Hello World!');"
    "  end;"
    "end;"
    "Greetings.hello"];
    [[MacRuby sharedRuntime] evaluateString:source];

    return 0;
}

It’s the same Ruby code, but instead of calling the hello method from Objective-C, we call it directly in our Ruby source code.

However, you probably don’t want to keep Ruby code as strings in your Objective-C code. The easiest way to deal with Ruby source code is to save it in its own file and require it using another API. So let’s rewrite our example to save the Ruby code in its own file called greetings.rb:

module Greetings
  def self.hello
    NSLog('Hello World!')
  end
end
Greetings.hello

Now change the implementation file to just “require” our Ruby file:

#import <MacRuby/MacRuby.h>

int main(void) {
  [[MacRuby sharedRuntime] evaluateFileAtPath:@"./greetings.rb"];
  return 0;
}

Compile the file as shown earlier, and you will notice that the output is the same.

It’s important to understand that the code is evaluated but stays in memory. So the following code will work fine and print two “Hello World!” strings:

#import <MacRuby/MacRuby.h>

int main(void) {
  [[MacRuby sharedRuntime] evaluateFileAtPath:@"./greetings.rb"];
  [[MacRuby sharedRuntime] evaluateString:@"Greetings.hello"];
  return 0;
}

However, each evaluation uses its own local scope, so if you wish to evaluate a Ruby object in two separate calls, you need to make sure the object is available the second time it’s called. Consider the following example:

#import <MacRuby/MacRuby.h>

int main(void) {
  [[MacRuby sharedRuntime] evaluateString:@"@msg = 'Hello!'"];
  [[MacRuby sharedRuntime] evaluateString:@"puts @msg"];
  return 0;
}

In this case, the first evaluation uses a Ruby instance variable (ivars start with @ in Ruby) to store a string. The second evaluation prints this string. The code as shown works fine. But if we were to remove the @ sign in front of the msg and thus convert the instance variable into a local variable, the second evaluation would fail, because the local variable was defined in a different scope.

Earlier, we looked at how to assign a Ruby object to an Objective-C variable. MacRuby can already access all the Objective-C code, but sometimes you want to pass a specific object to your Ruby code. To do that, pass it as a parameter, as follows:

#import <MacRuby/MacRuby.h>

int main(void) {
  NSString *source =  @"module Helper; def self.swapcase(str); str.swapcase; 
end; end";
  [[MacRuby sharedRuntime] evaluateString:source];
  id helper = [[MacRuby sharedRuntime] evaluateString:@"Helper"];
  NSString *result = [helper performRubySelector:@selector(swapcase:)
                                    withArguments:@"MacRuby", nil];
  NSLog(@"%@\n", result);
  return 0;
}

Per the API description, we are passing a NULL-terminated list with our argument being an Objective-C variable.

Here is the compilation and execution:

$ gcc example.m -o example -framework Foundation -framework MacRuby -fobjc-gc
$ ./example
2011-05-23 23:03:12.142 example[92128:903] mACrUBY

Example in an Xcode Project

Until now, we have been using a simple implementation file to explore how to use Ruby code from an Objective-C file. Let’s build a full Cocoa project in Objective-C and use MacRuby within the app.

Open Xcode and create a new project using the Cocoa Application template. Call this project EmbeddedMacRuby and don’t enable any of the options.

Once the project is created, look at the project’s target summary. We need to link the MacRuby framework. Click on the + button, as shown in Figure 11.1, “Linking the MacRuby framework”.

Figure 11.1. Linking the MacRuby framework

Linking the MacRuby framework

Choose the MacRuby framework from the menu and click the Add button. At that point, the framework should appear in the project navigator and you might want to drag it to the framework’s subfolder.

While you are editing the settings, you also need to make sure your project uses the Objective-C garbage collector (GC), because MacRuby requires it. Click the Build Settings tab and scroll down until you find the Objective-C Garbage Collection field. Toggle the value and select Required [-fobjc-gc-only] as shown in Figure 11.2, “Making sure the GC support is marked as required”.

Figure 11.2. Making sure the GC support is marked as required

Making sure the GC support is marked as required

Now that you have added the framework and set the GC settings, you can import the headers, and your project will build properly.

Edit EmbeddedMacRubyAppDelegate.m and add the following import statement:

#import <MacRuby/MacRuby.h>

To make sure everything is set correctly, run your new project. Everything should build and an empty window should display.

Now that you know that MacRuby is available in our Objective-C project and you know that this project builds correctly, let’s build a Ruby interpreter inside your UI. The idea is to write and execute/evaluate Ruby code from within the app as it is running. While this is a demo exercise, I’m sure you can imagine how useful it might be to debug an existing Objective-C app.

User Interface

As usual, we will set the UI first. Start by editing MainMenu.xib and expand the Window and View objects. Drag and drop a Horizontal Split View from the Object Library (Utilities Panel), as shown in Figure 11.3, “Setting up a horizontal split view”.

Figure 11.3. Setting up a horizontal split view

Setting up a horizontal split view

We are going to use the upper part of the split view to type in Ruby code and the lower part to show the result. Go back to the Object Library and drag a Text View in the top Custom View into the Split View. Also drag in a label, change the text to read “Ruby Code,” and organize the UI to look like Figure 11.4, “Text field to input some Ruby code”.

Do the same for the lower part, and add a button to trigger evaluation of the code. The end result should look like Figure 11.5, “End result”.

You will also want to tweak the Split View and the Text views to autoresize (Figure 11.6, “Setting the views to autoresize”).

Using the MacRuby Method

Now that the UI looks good, let’s write some code. Edit EmbeddedMacRubyAppDelegate.h and add two outlets, one for each text view. Also add an action for our button. Your header file should look like this:

#import <Cocoa/Cocoa.h>

@interface EmbeddedMacRubyAppDelegate : NSObject <NSApplicationDelegate> {
  IBOutlet NSTextView *rubySourceTextView;
  IBOutlet NSTextView *resultTextView;

@private
  NSWindow *window;
}

- (IBAction)evaluate:(id)sender;

@property (assign) IBOutlet NSWindow *window;

@end

Figure 11.4. Text field to input some Ruby code

Text field to input some Ruby code

Go back to the Interface Builder and right-click Embedded Mac Ruby App Delegate. Drag the rubySourceTextView outlet to the upper scroll view, the resultTextView to the lower scroll view, and the evaluate: action to the push button. Your UI is now wired to your delegate class.

Figure 11.5. End result

End result

Figure 11.6. Setting the views to autoresize

Setting the views to autoresize

Edit the EmbeddedMacRubyAppDelegate.m implementation file and add an awakeFromNib method:

- (void)awakeFromNib
{
  NSFont *niceFont;

  niceFont = [NSFont fontWithName:@"Monaco" size:12.0];
  [rubySourceTextView setFont:niceFont];
  [resultTextView setFont:niceFont];

  NSString *demoSource = [NSString stringWithFormat: @""
       "@window = NSWindow.alloc.initWithContentRect(\n"
       "          [200,100,200,200],\n"
       "          styleMask:NSTitledWindowMask,\n"
       "          backing:NSBackingStoreBuffered,\n"
       "          defer:false)\n"
       "@window.BackgroundColor = NSColor.blueColor\n"
       "@window.orderFront(NSApp)"];
  [rubySourceTextView setString:demoSource];
}

Even if you are not familiar with Objective-C, this code should be understandable. We start by setting the same font for the views of our Ruby source and our result text. Then we insert some demo text in the source view.

This method is called when the UI finishes loading. At that point, everything is set for the user and he needs only to press the button to create a new window.

You can now run the app. It should build fine and the UI will display properly with the sample text in the source text view. But a warning should let you know that the evaluate action implementation is missing. Let’s add it:

- (IBAction)evaluate:(id)sender
{
  @try {
    id object;

    object = [[MacRuby sharedRuntime] evaluateString:[rubySourceTextView string]];
    [resultTextView setString:[object description]];
  }
  @catch (NSException *exception) {
    NSString *string = [NSString stringWithFormat:@"%@: %@\n%@",
                           [exception name], [exception reason],
                      [[[exception userInfo] objectForKey:@"backtrace"] description]];
    [resultTextView setString:string];
  }
}

The evaluate action isn’t complicated, now that we know the MacRuby framework API. We start by declaring the variable that will hold the returned object from the Ruby code evaluation. This object is cast as id, since we know only that it will be an Objective-C object, not its exact type. We then evaluate the content of the Ruby source text view and store the result into the object variable. The description of the object is then set as the text displayed in the result text view. In case something goes bad with our Ruby code, the method is wrapped so it can catch any exception thrown and display it in the result text view.

Fire the app and click the Evaluate button. You should see something similar to Figure 11.7, “Ruby interpreter in action”.

Figure 11.7. Ruby interpreter in action

Ruby interpreter in action

Creating new objects and modifying them on the fly is really nice, but what about modifying the UI we already created? Well, we are missing a reference to our main window, which we can’t access from our evaluated script. To fix that, change the awakeFromNib method as shown here:

- (void)awakeFromNib
{
  NSFont *niceFont;

  niceFont = [NSFont fontWithName:@"Monaco" size:12.0];
  [rubySourceTextView setFont:niceFont];
  [resultTextView setFont:niceFont];

  id appHelper = [[MacRuby sharedRuntime] evaluateString:@"module App;\
                    def self.setWindow(val); @window=val; end;\
                    def self.window; @window; end; end; App"];
  [appHelper performRubySelector:@selector(setWindow:)
                                withArguments:window, nil];
  [rubySourceTextView setString:@"App.window.title = 'W00t!'"];
}

This time, when the app loads, we evaluate an App module helper that we assign to an Objective-C reference. Then we pass the Objective-C window reference to our module so we can access it from our Ruby code. To prove that it works, we add a demo statement that changes the title of the window.

Have fun inspecting your live views and subviews, as well as tweaking them in real time from within your Objective-C app. If you want to find the available methods on the class, evaluate App.window.methods(true, true).sort or read the NSWindow Class Reference.

You might have noticed that the Ruby setter in our App module is called setWindow, not a very Ruby-like setter name. Usually, you will want to use window=. However, if we had used that method name, we wouldn’t be able to perform our selector. This is because the Objective-C compiler won’t accept a selector name containing an equals sign. As a matter of fact, the compiler will reject all selector names containing nonstandard characters: =, <, >, ?, [], |, +, -, *, /, `, and more. However, these characters are common in Ruby methods. Thankfully, there is a workaround. You can build the selectors yourself using the Objective-C runtime API, which does not suffer from this limitation.

Here is the awakeFromNib method, reimplemented using the sel_registerName method:

- (void)awakeFromNib
{
  NSFont *niceFont;

  niceFont = [NSFont fontWithName:@"Monaco" size:12.0];
  [rubySourceTextView setFont:niceFont];
  [resultTextView setFont:niceFont];

  id appHelper = [[MacRuby sharedRuntime] evaluateString:@"module App;\
                    def self.window=(val); @window=val; end;\
                    def self.window; @window; end; end; App"];
  [appHelper performRubySelector:sel_registerName("window=:")
                                withArguments:window, nil];
  [rubySourceTextView setString:@"App.window.title = 'W00t'"];
}

We can now perform a selector containing the equals sign.

Site last updated on: November 9, 2011 at 10:00:57 AM PST
Cover for MacRuby: The Definitive Guide