4. RubyCocoa Concepts

Ruby is a powerful language and is loaded with features, but for some applications, Ruby programmers face a limitation. It might be a performance problem caused by the slowness of interpreted languages. Or it might be that a critical function or API is only available in a lower-level language like C or C++. To address this, Ruby provides a way for developers to write extensions of Ruby in C.

The extension process is described in the pickaxe book. It is simpler than the processes required by some other scripting languages, but it still requires programmers to write custom “glue code” for every C function or C++ object that we need to make available to Ruby.

There is a popular tool called SWIG that can help generate this code. SWIG reads C and C++ source files and automatically writes the glue code that wraps C functions for Ruby (and lots of other languages). Wrapping C++ objects is also possible, but is more tricky due to the complexity of C++.

At some point, one eventually wonders “why is all this glue code necessary?” The answer to that lies in the C and C++ compilation process, which converts a lot of very high level information about objects and functions into lots more very low level information, mainly the machine instructions that directly implement each function. But all the high level information that Ruby needs about objects, types, methods, and interfaces is discarded in the compilation of C and C++ programs (and sometimes recovered by SWIG).

This is where Ruby programmers begin to love Objective-C. The tables that Objective-C uses for dynamic message handling contain exactly the information that is needed to interface with Ruby. Because they are kept in the compiled object files, they are available at runtime. As a result, to interface Objective-C objects with Ruby, no custom glue code is needed! All the translation that we need is in handlers that are built right into the RubyCocoa framework.

This gives us direct access to Cocoa and a lot more: any Objective-C classes and objects can be accessed through RubyCocoa. What’s more, RubyCocoa also contains a bridge that makes certain kinds of Ruby objects available in Objective-C code. We’ll go into detail about all this in the rest of this chapter.

PyObjC

Ruby isn’t the only popular high-level language that’s been hooked up with Objective-C and Cocoa. The PyObjC project has made Cocoa classes available to Python programmers. Like Ruby, Python is an interpreted language that determines the characteristics of objects at runtime, making it well-suited to a partnership with Objective-C. The PyObjC project is a bit more mature than RubyCocoa; you’ll even find a small tutorial on Apple’s developer site. But don’t let that stop you from creating Cocoa applications with the Programmer’s Best Friend. Much of what you need to know can be found online or in the RubyCocoa source files.

RubyCocoa automatically puts Objective-C objects inside Ruby wrapper objects that receive and translate messages from Ruby callers. Everything is handled automatically, but because of syntax differences between the languages, it is important for a programmer to know the relationship between message names in Objective-C and the corresponding message names in Ruby.

In Objective-C, messages are sent by enclosing the object name and message name in square brackets. Here we are sending a message named play to an object named sound:
[sound play];
When messages include arguments, the arguments are interleaved with tags that describe them:
[sound initWithContentsOfFile:filename
                  byReference:false];
In Ruby, the corresponding messages would be:
sound.play
and
sound.initWithContentsOfFile_byReference_(filename, false)
As a convenience, RubyCocoa allows the trailing underscore to be omitted:
sound.initWithContentsOfFile_byReference(filename, false)
RubyCocoa also supports an alternate syntax in which the argument tags are included as parameters of the message:
sound.initWithContentsOfFile(filename, :byReference, false)
Let’s try it. If you’ve installed RubyCocoa, start irb and type in the commands below:
irb(main):001:0> require 'osx/cocoa'
=> true
irb(main):002:0> sound = OSX::NSSound.soundNamed "Basso" 
=> #<OSX::NSSound:0x3adc34 class='NSSound' id=0x11401e0>
irb(main):003:0> sound.play
=> 1

The first line imports the osx/cocoa module, which makes the Cocoa classes available in the OSX module. The next line creates an NSSound object with one of the system sounds (you’ll notice that we used an easier way to load system sounds; it’s described in the NSSound class documentation). After you enter the third line, you should hear the sound.

When it is imported, the osx/cocoa module imports the classes that are part of Cocoa’s Foundation and AppKit frameworks and wraps them in Ruby objects. Each class is imported using a function called ns_import. For example, the osx/cocoa module imports the NSSound class with a line like the following:
OSX.ns_import :NSSound
ns_import can also be used to import Objective-C classes that are part of an application or another framework. If your application contained a class named Sprite, you could make it visible in Ruby like this:
OSX.ns_import :Sprite

Introspection

You can use the Ruby methods function to get a list of the methods that are implemented for a Ruby object. Because RubyCocoa uses wrapper objects, when you call this function for a RubyCocoa object, you get only the methods defined for the wrapper. To get the messages accepted by the enclosed Objective-C object, use the objc_methods function. Append the sort function to get the list sorted alphabetically.

irb(main):001:0> require 'osx/cocoa'
=> true
irb(main):002:0> s = OSX::NSString.stringWithString "hello" 
=> #<OSX::OCObject:0x370976 class='NSCFString' id=0x114dee0>
irb(main):003:0> s.objc_methods.sort
=> ["CI_affineTransform", "CI_copyWithZone:map:", "CI_rect", "UTF8String", ...

As you can see, there’s a lot available in RubyCocoa!

NOTE: if the above example crashes when you try it, make sure that you have the latest RubyCocoa distribution. We discovered a bug in RubyCocoa when I was writing this article, but it was easily resolved and is fixed in the CVS sources and the latest releases.

Other Cocoa data structures

There are also a few important Cocoa data structures that are not Objective-C objects. These include NSPoint, NSSize, NSRect, and NSRange. On the Ruby side, they are defined as pure Ruby classes. Create them using the Ruby new method:
# NSPoint
point = OSX::NSPoint.new(x,y)
# NSSize
size = OSX::NSSize.new(width, height)
# NSRect
rect1 = OSX::NSRect.new(x, y, width, height)
rect2 = OSX::NSRect.new([x,y], [width, height])
# NSRange
range1 = OSX::NSRange.new(range)
range2 = OSX::NSRange.new(location, length)

Use the Ruby methods message to see everything that you can do with each type of object.

It is also possible to call Ruby from Objective-C. In fact, this is what happens nearly every time Ruby code is executed in a RubyCocoa application. The main event loop of a Cocoa application is written in Objective-C: with RubyCocoa, the Objective-C framework code calls Ruby implementations of class functions. But the power of this goes further. Since Ruby code can call Objective-C code and Objective-C code can call Ruby code, we can freely mix the two languages in our applications. We get to use Ruby’s high-level data structures and programming constructs and when needed, we can write fast C code using Objective-C.

Here’s an example that illustrates calls back and forth between Objective-C and Ruby using RubyCocoa.

First, the Objective-C class interface description (we’ll put it in a file called Client.h):

Client.h [Objective-C]
#import <Cocoa/Cocoa.h>

@protocol Helper
- (NSNumber *) helpfullyAddX:(NSNumber *)x toY:(NSNumber *)y;
@end

@interface Client : NSObject {
   id helper;
}
- (id) helper;
- (void) setHelper:(id)helper;
- (NSString *) name;
- (NSNumber *) addX:(NSNumber *)x toY:(NSNumber *)y;
@end

Here is the Objective-C class implementation (Client.m):

Client.m [Objective-C]
#import "Client.h"

@implementation Client

- (NSString *) name {
    return helper ? [helper name] : @"Client";
}

- (id) helper {
    return helper;
}

- (void) setHelper: (id) h {
    [h retain];
    [helper release];
    helper = h;
}

- (void) createHelper {
    Class helperClass = NSClassFromString(@"Helper");
    id helperInstance = [[helperClass alloc] init];
    [self setHelper:helperInstance];
}

- (NSNumber *) addX:(NSNumber *)x toY:(NSNumber *)y {
    return helper ? [helper helpfullyAddX:x toY:y] : [NSNumber numberWithInt:0];
}
@end

Finally, here’s the Ruby code:

helper.rb [ruby]
require "osx/cocoa"
require "client"
OSX.ns_import :Client

class Helper < OSX::NSObject
  def name
    "helper"
  end
  def helpfullyAddX_toY(i,j)
    puts "helper arguments are of type #{i.class} and #{j.class}"
    i.to_i + j.to_i
  end
end

client = OSX::Client.alloc.init

puts ">>> testing client without helper"
puts "name: #{client.name}"
puts "sum:  #{client.addX_toY(5,2)}"

puts ">>> testing helper"
helper = Helper.alloc.init
puts "name: #{helper.name}"
puts "sum:  #{helper.helpfullyAddX_toY(5,2)}"

puts ">>> testing client use of helper"
client.setHelper(helper)
puts "name: #{client.name}"
puts "sum:  #{client.addX_toY(5,2)}"

puts ">>> repeating; now the client looks up the helper class by name and makes its own helper"
client.createHelper
puts "name: #{client.name}"
puts "sum:  #{client.addX_toY(5,2)}"

The example shows two ways of referring to Ruby objects from Objective-C. In the first case, we have our Ruby code tell our Objective-C code about itself—Ruby code sets the helper object of our Objective-C object. In the second case, the Objective-C code looks up our Ruby helper class by name using NSClassFromString. It is important to remember that to be visible from Objective-C, any Ruby class (1) must be derived from OSX::NSObject, and (2) its definition must have already been evaluated by the Ruby interpreter.

You may have noticed that we call to_i on each of the arguments to helperAddX_toY. When we call Ruby from Objective-C, each numeric argument is passed as an NSNumber. The RubyCocoa bridge currently requires that every argument in calls from Objective-C to Ruby be an NSObject. Exceptions are made for many Cocoa (Framework and AppKit) messages, but they are hard-coded into the RubyCocoa bridge. The numeric value returned by helperAddX_toY is automatically converted back to an NSDecimalNumber by RubyCocoa.

Confused? Try it for yourself! Append the following Ruby module initialization function to Client.m:
void Init_client(){}
Then use gcc to compile the above Objective-C code into a bundle that can be loaded by Ruby:
gcc -o client.bundle -bundle -framework Foundation Client.m
After you’ve build client.bundle, you can import it into Ruby with the command:
require 'client.bundle'
or simply
require 'client'

When you run the example, you should see the following results:

Test results [output]
>>> testing client without helper
name: Client
sum:  0
>>> testing helper
name: helper
helper arguments are of type Fixnum and Fixnum
sum:  7
>>> testing client use of helper
name: helper
helper arguments are of type OSX::NSDecimalNumber and OSX::NSDecimalNumber
sum:  7
>>> repeating; now the client looks up the helper class by name and makes its own helper
name: helper
helper arguments are of type OSX::NSDecimalNumber and OSX::NSDecimalNumber
sum:  7

If you have any problems, download this tar file that contains everything. If you still have problems, email me, there’s probably something online here that I need to fix!

Be sure to use irb to explore further. This is just the beginning.

The previous section showed an example where a Ruby class is derived from NSObject. You can subclass other Cocoa classes and your own Objective-C classes as well.

First you need to make your Objective-C class visible in the Ruby environment. You do this with OSX.ns_import:
OSX.ns_import :Sprite

You can then use your class inside Ruby. You’ll need to refer to it with the OSX:: prefix unless you’ve used include OSX somewhere earlier in your session or program to get inside the OSX module namespace.

You define subclasses of the Objective-C class in the same way that you would do so for any Ruby class.
class RubySprite < OSX::Sprite
  def hello
     "hello" 
  end
end
To create instances of the class, be sure to use the Objective-C creation sequence, allocating space for the object first and then initializing it:
s = RubySprite.alloc.init

You also need to know a few other important things:

Overriding methods

When a RubyCocoa class overrides a method of an Objective-C class, a special declaration must be included to alert the RubyCocoa runtime. The keyword to use is ns_overrides, and it needs to be placed in the Ruby class declaration. It requires the full name of the method being overridden; if there’s a trailing underscore (to replace a ’:’), you’ll need to include it. For example, to override the drawRect: method of an NSView, a Ruby view subclass would contain:
   ns_overrides :drawRect_
   def drawRect(rect)
       super_drawRect(rect)
       ... custom code...
   end

In the example above, the drawRect: method of the parent class is being called by the name super_drawRect.

Initializing objects

As noted previously, when Objective-C-based objects are created, the Ruby calling code must use the Objective-C allocation and initialization methods. So instead of calling mySprite.new, Ruby code must call mySprite.alloc.init.

A Ruby class will often require additional initialization. When overriding initializers, be sure to call the superclass’s initialization function. If you have overridden the superclass initializer (let’s assume it’s init), then call it using super_init. If you have written a new initializer and have not overridden the superclass initializer, then you can simply call it by name.

Finally, don’t forget to return self! It’s a Cocoa requirement that all initializers return the object that was initialized. It’s easy to forget this, and serious problems will result when you do.

I decided to include this section for completeness, but currently I don’t know of any way to write Objective-C subclasses of Ruby classes. I also can’t think of a good reason to do so. If you can help me with either, please email me!

Objective-C and Ruby are both children of Smalltalk, but they have grown up separately. There are a few ways you can trip over the gap between them.

Although method calls are bridged automatically, Objective-C and Ruby use different classes to represent similar kinds of things. When a Cocoa function returns a string, it returns an OSX::NSString and not a Ruby String. irb is a great way to explore and debug problems. When you can’t use irb, use puts or OSX::NSLog to write the class of an object (which you can get in Ruby by calling object.class).

Also, watch out for name conflicts between Objective-C and Ruby. If you try to call an Objective-C method when there’s a Ruby version with the same name, the Ruby one will get called instead. And since the RubyCocoa bridge code won’t get called, you won’t get any warning about this! I’ve run into this problem a few times when I’ve tried to call functions named load and display. To make sure that you get the Objective-C versions, add the oc_ prefix (that’s why you’ll sometimes see oc_load and oc_display in my RubyCocoa examples).

Name conflicts can also occur among source files. Watch out for file names that may be the same as files in Ruby libraries. I recently put some code in a file called “delegate.rb”, but on my local system, this file was masked by /usr/local/lib/ruby/1.8/delegate.rb.

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (3) post
  1. jeanpierre Thu Oct 26 23:37:08 +0000 2006

    i’m not quite sure what the ruby garbage collection + objc reference counting interplay is, but from an objC standpoint Client.m has two memory leaks.

    Client#setHelper retains the object passed in so Client#createHelper should release the instance it allocated and initialized.
    
    - (void) createHelper {
        Class helperClass = NSClassFromString(@"Helper");
        id helperInstance = [[helperClass alloc] init];
        [self setHelper:helperInstance];
        [helperInstance release];
    }
    
    also, since the client is retaining the helper object, it needs to release it when the client itself is deallocated:
    
    - (void)dealloc {
        [helper release];
        [super dealloc];
    }
    
  2. Diego Kuperman Wed Dec 19 02:22:12 +0000 2007

    There are also bindings for the dynamic-mother language (perl): camelbones are called.

  3. Sandro Paganotti Wed Feb 06 13:46:34 +0000 2008

    I’ve created a small utility for measure the strength of a wireless signal usign the tips of this page so thank you very much !