2. Running with Ruby
2.1 Import the framework
Now we’re going to use the controller from Ruby. If you haven’t already installed RubyCocoa, you can find instructions for doing so here.
To use the new controller in a Ruby program, you first need to import the framework. We’ll do that with the NSBundle load command. Then we’ll use RubyCocoa’s ns_import command to create a Ruby wrapper for the Objective-C AppleRemote class.
require 'osx/cocoa' OSX::NSBundle.bundleWithPath("/Library/Frameworks/AppleRemote.framework").load OSX.ns_import :AppleRemote
2.2 Write a delegate
Next we need to write a delegate class. Here’s a simple one to get you started.
class AppleRemoteDelegate < OSX::NSObject addRubyMethod_withType('sendRemoteButtonEvent:pressedDown:remoteControl:', 'v@:ii@') def sendRemoteButtonEvent_pressedDown_remoteControl(buttonIdentifier, pressedDown, remoteControl) puts "button #{buttonIdentifier}, pressed #{pressedDown}" end end
Our delegate has one method of interest, sendRemoteButtonEvent_pressedDown_remoteControl, which simply prints the values passed to it by the controller. It’s preceeded by an interesting statement, the addRubyMethod_withType call. The purpose of this statement is to give the Objective-C runtime the correct signature to use for our delegate method. Its two arguments are the selector name (a string), and a string containing the encoded signature. In this case, the signature is six characters long. The first character indicates that the return value of the method is void. The next two are superfluous to us, but important to the Objective-C runtime. The ’@’ means that the message is to be sent to an object of Objective-C type id, and the ’:’ indicates that the message is described by a special Objective-C type called a selector. These two values are the same for all Cocoa messages. The next three characters give the types of the three arguments to the method, in this case they are two integers and an id type.
You can find a full listing of type codes on Apple’s Developer Connection web site. Look here.
But why, you may ask, did we need to go to this trouble? The answer to that is a bit complex. If you’d rather not get into it, just skip to the next section, but remember to look back here if you ever write Ruby code that gets called by Objective-C code that is not part of Cocoa.
Martin’s controller is written in Objective-C and uses the Objective-C runtime environment to send messages to its delegate. When the Objective-C runtime sends a message to an object, it does so with a low-level function that looks for a handler in the method table of the receiving object’s class. When the receiving object is an Objective-C wrapper for a RubyCocoa object, normally there’s no corresponding handler in its method table (with one exception to be explained later). When no handler is present, that low-level message sending function tries to package the message into an NSInvocation object which it resends to the target object with the forwardInvocation method. Then the target can pass the message along to an object capable of responding to it. In the case of RubyCocoa, this NSInvocation gets handled by bridge code that makes a corresponding Ruby method call, converts the Ruby return value into the appropriate Objective-C type, and returns control to the message sender. But for the sender to be able to construct the invocation, a special message must first be sent to the Objective-C wrapper object. This message is called methodSignatureForSelector: and its purpose is to provide the signature for the message to be sent.
In RubyCocoa, there’s a methodSignatureForSelector defined for Objective-C wrapper objects, but it only knows about selectors associated with Cocoa classes—the RubyCocoa build process extracts them from Cocoa headers and hard-codes them into the RubyCocoa source files. (If you’d like to see them for yourself, look at framework/src/objc/DummyProtocolHandler.m in the RubyCocoa source distribution.)
Martin’s controller uses a protocol that is unknown to the RubyCocoa bridge. So when RubyCocoa’s methodSignatureForSelector gets called, it returns a default signature that assumes that all arguments and the return value are of type id (Objective-C objects). One desperate way to fix this problem is to explicitly add the protocol to DummyProtocolHandler.m and rebuild the RubyCocoa sources, but in most cases, the addRubyMethod_withType makes that unnecessary.
The addRubyMethod_withType method doesn’t do anything to change methodSignatureForSelector. Instead, it resolves the problem by adding a message handler with the specified signature directly to the Objective-C wrapper class (there’s a unique Objective-C wrapper class for each wrapped Ruby class). When it is used to set the correct signature, messages are sent and handled correctly. If it is used incorrectly or omitted, all kinds of havoc will follow.
Whew. That’s a lot of writing to explain one line of code. But without that line, the invocation would be sent with the wrong signature and our program would behave mysteriously—and possibly crash.
Earlier in this section I wrote that there’s an exception to the general rule that bridged method calls are handled by invocations. We actually just saw one exception, when we used addRubyMethod_withType to put an entry into the wrapper class’s method table. But the exception I was indicating is ns_overrides, the mysterious function that must be called when any Objective-C method is overridden in a RubyCocoa class. It is necessary because when messages are sent to RubyCocoa’s Objective-C wrapper objects, any message handler defined on a superclass will handle the message and prevent it from being wrapped in an NSInvocation and sent to the RubyCocoa class. ns_overrides prevents this by adding an entry to the wrapper class’s method table.
2.3 Put it to work
Our code-to-talk ratio has gotten dangerously low. Let’s fix that by putting our Apple Remote controller to work. Put the source from the previous two parts of this chapter into a Ruby file called “remote.rb”, start irb from the Terminal, and type the commands in the transcript below:
>> require 'remote' => true >> d = AppleRemoteDelegate.alloc.init => #<AppleRemoteDelegate:0x3691e2 class='AppleRemoteDelegate' id=0x5734a0> >> a = OSX::AppleRemote.alloc.initWithDelegate_(d) => #<OSX::AppleRemote:0x367c52 class='AppleRemote' id=0x5773c0> >> a.startListening 0 => nil >> OSX::NSApplication.sharedApplication.runNext, start pressing buttons on your remote. You should see something like the following listing. You can easily use this to find the codes for each button, or if you prefer, go back to Martin’s RemoteControl.h and look at the _RemoteControlEventIdentifier enum.
button 2, pressed 1 button 2, pressed 0 button 2, pressed 1 button 2, pressed 0 button 4, pressed 1 button 4, pressed 0 button 32, pressed 1 button 32, pressed 0
That’s it! There’s obviously some complicated stuff happening under the covers of the RubyCocoa bridge, and there’s probably also some ways it can be improved. But with the right understanding and incantations, the bridge works right now and can be used to do some dramatic things very quickly. Consider how much more difficult this would have been if Martin’s package was written in C or C++. We would have needed to either write the Ruby wrapper ourselves or use a tool like SWIG to generate wrapper glue code. With Objective-C and RubyCocoa, we were able to use Martin’s package with no glue code and no compilation. Everything we needed to do was done at runtime. Amazing?
Did you find an error? Is something missing? Post your comment or suggestion below!
Comments (4) post
Thanks. I tried it because I try to figure out how RubyCocoa works. I need to bring the Wiiremoteframework and RubyCocoa together to steer a Lego Mindstorms robot with the Wiimote.
hi, i can not compile the framework, because the new version has a other structure than your example. can you help me with this? how can i compile the new version into a framework? greetings, Dennis
Hi Dennis, I’ve updated the previous chapter with details on how to build the latest version.
Thanks for posting the tutorial but I’m getting the following error message:
“Can’t get Objective-C method signature for selector ‘setDelegate:’ of receiver #“
Any suggestions?