6. RubyCocoa Tips
6.1 Build RubyCocoa Projects with Rake
Rake is a Ruby-based build system that is similar to make. But it differs in being 100% Ruby, meaning that Ruby code can be freely mixed with build tasks and dependencies. Rake was written by Jim Weirich, and you can learn more about it by reading its excellent online documentation.
To use Rake to build RubyCocoa projects, we just need to define a few tasks to create the directory and file structure of an OS/X application.
Here’s an example Rake input file, commonly called a Rakefile:
require 'rake' require 'rake/clean' APPLICATION = 'MyCoolApp' OBJCSRCS = FileList['objc/*.m'] OBJCOBJS = OBJCSRCS.sub(/\.m$/, '.o') RUBYSRCS = FileList['ruby/*.rb'] NIBDIRS = FileList['*.lproj'] FRAMEWORKS = FileList['/Library/Frameworks/RubyCocoa.framework'] CC = "gcc" CFLAGS = "-g -Wall" LDFLAGS = "-lobjc -framework RubyCocoa" CLEAN.include("**/*.o") CLOBBER.include("#{APPLICATION}.app") rule ".o" => [".m"] do |t| sh "#{CC} #{CFLAGS} -c -o #{t.name} #{t.source}" end file "#{APPLICATION}.app/Contents/MacOS/#{APPLICATION}" => OBJCOBJS do |t| mkdir_p File.dirname(t.name) sh "#{CC} #{OBJCOBJS} #{LDFLAGS} -o #{t.name}" end file "#{APPLICATION}.app/Contents/Resources" do |t| mkdir_p t.name RUBYSRCS.each {|f| cp f, t.name} NIBDIRS.each {|d| cp_r d, t.name} end file "#{APPLICATION}.app/Contents/Frameworks" do |t| mkdir_p t.name FRAMEWORKS.each {|d| cp_r d, t.name} end file "#{APPLICATION}.app/Contents/Info.plist" => ["Info.plist"] do |t| mkdir_p File.dirname(t.name) cp t.prerequisites[0], t.name end file "#{APPLICATION}.app/Contents/PkgInfo" do |t| mkdir_p File.dirname(t.name) sh "echo -n 'APPL????' > #{t.name}" end desc "build the application" task :default => [ "#{APPLICATION}.app/Contents/MacOS/#{APPLICATION}", "#{APPLICATION}.app/Contents/Resources", "#{APPLICATION}.app/Contents/Frameworks", "#{APPLICATION}.app/Contents/Info.plist", "#{APPLICATION}.app/Contents/PkgInfo" ] desc "run the application" task :run do sh "open #{APPLICATION}.app" end
To see the tasks available, run “rake -T” in the Terminal.
To build your application, simply run “rake”.
The Rakefile defines a task named run that runs your application. To use it, type “rake run”.
Since the Rakefile requires “rake/clean”, it supports two more tasks: clean, which deletes all the build products except the final result, and clobber, which deletes everything including the compiled application. You might notice that a few dependencies are missing in the above Rakefile. If you use it as-is, when you add or modify .nib files, you will need to run rake clobber before rebuilding to be sure that you get all the .nib files copied to your application (watch this chapter for an updated Rakefile!).
6.2 Debug RubyCocoa problems with gdb
Sometimes errors in memory management, threading, or calling conventions can cause an application to crash in the RubyCocoa bridge code. To help diagnose problems like these, you might want to work with debuggable versions of Ruby and RubyCocoa. Here’s how to get them.
First, build a debuggable version of Ruby. In your ruby source directory, run the following commands from the bash shell:$ CFLAGS='-g -ggdb -fno-common' ./configure $ make $ sudo make install
Next, in your RubyCocoa distribution directory, open the xcode project in framework/RubyCocoa.xcodeproj (or .pbproj). Then build the default product, which is a development version of RubyCocoa.framework. You’ll find the result in framework/build/Development/RubyCocoa.framework. To use this in your RubyCocoa application, copy it to the Frameworks directory under your application’s Contents directory. If your application doesn’t have a Frameworks directory, create it first.
Now when you debug your RubyCocoa application inside Xcode, you should be able to see the Ruby and RubyCocoa code. You can step through it, set breakpoints, examine variables, and (most importantly) analyze stack traces to see why a crash is occurring.
If you want to use your debuggable framework all the time, copy it to /Library/Frameworks (replacing the one placed there by the RubyCocoa installer).
#import <RubyCocoa/RBRuntime.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char *argv[])
{
#ifdef DEBUG_MY_RUBYCOCOA_APP
int i;
char **new_argv = (char **) malloc ((argc + 1) * sizeof (char *));
for (i = 0; i < argc; i++)
new_argv[i] = (char *) argv[i];
new_argv[argc++] = strdup("-d");
argv = (const char **) new_argv;
#endif
return RBApplicationMain("rb_main.rb", argc, argv);
}
That’s messy. Recently, I found there is a better way to do this if you are running your application within Xcode. Here it is:
In the Groups & Files pane, find your application’s executable (it’s under Executables). Double-click on it to open it’s info window. Then add “-d” as an “argument to be passed on launch” as shown here. You can use the checkbox to easily disable and reenable the option.6.3 Convert a pure Objective-C Cocoa application to a RubyCocoa app
Do you have an existing Cocoa application that you would like to extend with RubyCocoa? Recently I successfully converted one of my pure Objective-C Cocoa projects to RubyCocoa. Here’s the story:
First I added the RubyCocoa framework to the project in Xcode. In my case, I right-clicked on the project name in the Groups & Files panel in Xcode, then selected Add->Existing Frameworks… and navigated to /Library/Frameworks/RubyCocoa.framework.
Next I replaced the contents of main.m with the following, taken from the RubyCocoa Xcode templates:
#import <RubyCocoa/RBRuntime.h>
int main(int argc, const char *argv[])
{
return RBApplicationMain("rb_main.rb", argc, argv);
}This is necessary to make sure that Ruby is properly initialized and it also gives you a way to run some Ruby code when your program starts. As far as I know, you must replace your main() to use RubyCocoa, but you probably didn’t have anything special there anyway —just a call to NSApplicationMain, which your converted app will make with Ruby.
Then I added rb_main.rb to my project using Add->Existing Files… and then finding and selecting one from a template-based RubyCocoa application. For reference, this short file contains the following Ruby code:
require 'osx/cocoa' def rb_main_init path = OSX::NSBundle.mainBundle.resourcePath.to_s rbfiles = Dir.entries(path).select {|x| /\.rb\z/ =~ x} rbfiles -= [ File.basename(__FILE__) ] rbfiles.each do |path| require( File.basename(path) ) OSX::NSLog "require #{File.basename(path)}" end end if $0 == __FILE__ then rb_main_init OSX.NSApplicationMain(0, nil) end
It loads any Ruby files in your application’s resource directory and then calls NSApplicationMain.
Then I built and tested my application. Everything seemed to work just as before, but now my formerly mild-mannered Objective-C program can speak Ruby.
From here forward, you can add Ruby files to your application and know that all inline code in them will get executed when rb_main_init loads the files during startup.
In my case, I added a console object that I’ve been writing and debugging. Accessing my Objective-C objects was a puzzle at first, but I managed to get to my top-level document object (a game) from the NSDocumentController:> game_controller = OSX::NSDocumentController.sharedDocumentController > game = game_controller.documents.objectAtIndex(0)From there, I could directly call the methods of my object from Ruby:
> game.start > game.sendEvent_forPlayer(10, 0) > game.stop
That’s all there was to it!
6.4 Creating special Cocoa data types from Ruby
There are a few Cocoa data types that seem difficult to pass to RubyCocoa, but that can actually be handled quite easily:
NSPoint, NSSize, NSRect
There are equivalent Ruby classes for each of these C structures. You can also pass Ruby arrays in their place which will be automatically converted.
p = OSX::NSPoint.new(x,y) # or simply [x, y] s = OSX::NSSize.new(w,h) # or simply [w,h] r = OSX::NSRect.new(x,y,w,h) # or simply [x, y, w, h]
C arrays
Sometimes Cocoa messages call for C arrays of data. You can create them using Array.pack(). The arguments to pack() specify the type of data in the C array or structure being created.
dasharray = [a,b].pack('f2') dashcount = 2 path = OSX::NSBezierPath.bezierPath path.setLineDash_count_phase(dasharray, dashcount, 0)
Selectors
Pass a string or symbol corresponding to the selector that you want. If you’re passing a string, just enclose the selector name in quotes. If it’s a symbol, replace colons (’:’) with underscores(’_’) and add a colon at the beginning. It also seems to be sometimes safe to delete the trailing colon or underscore (but not for ns_overrides).
# these two lines both specify selectors for Ruby methods that override ones # in Objective-C parent classes (use either, but only one) ns_overrides "drawRect:" ns_overrides :drawRect_ # these two lines pass the tick: selector to a new timer t = OSX::NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats( 1.0/60.0, self, :tick, nil, true) t = OSX::NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats( 1.0/60.0, self, "tick", nil, true) # here's a selector for an NSSortDescriptor # (again, use only one of these but either will work) descriptor = NSSortDescriptor.alloc.initWithKey_ascending_selector( "foo", true, :localizedCaseInsensitiveCompare_) descriptor = NSSortDescriptor.alloc.initWithKey_ascending_selector( "foo", true, "localizedCaseInsensitiveCompare:")
Did you find an error? Is something missing? Post your comment or suggestion below!
Comments (1) post
Minor typo above. In this line: s = OSX::NSSize.new(w,h) # or simply [w,y]
the line should read: s = OSX::NSSize.new(w,h) # or simply [w,h]