dominic dagradi

code, art and everything in between

MacRuby Ups and Downs

We’ve been building Briquette at Bearded1 on and off since December 2010. When beginning the project, we chose to use MacRuby as a way to make the project more accessible to our developers that weren’t familiar with Objective-C, and to explore a possible future of native Mac OS X (and iOS) development.

For building new Mac apps, I would recommend MacRuby without hesitation. It delivers on the promise of bringing the best aspects of Ruby to native, desktop app development. Abstracting away from Objective-C’s sometimes-too-verbose syntax is often a joy, and it’s useful to have access to the complete library of both Cocoa frameworks and Ruby gems2. It’s a young framework that’s getting better by the day, and the good has consistently outweighed the bad.

But there are drawbacks, especially when using Xcode heavily. I want to touch on some areas that I feel have often been ignored, or covered inadequately, and pour a little cold water on the heads of anyone starting a MacRuby project. But just a little.

Objective-C: It’s Not Optional

One of our goals in choosing MacRuby was to simplify the learning process for developers with minimal experience with Objective-C and Apple’s frameworks. In practice, though, one can’t get by with zero knowledge of Cocoa APIs before diving into the Ruby wrapper.

Cocoa is a verbose framework. It’s actually one of the things I find most attractive about it. Xcode does everything it can to enhance the experience of coding with excellent method suggestions and code completion for Objective-C, but is unable to help in Ruby files. This lack of help, coupled with the need to have a grasp of delegates, actions, outlets, and XIBs/Interface Builder, makes MacRuby more difficult for someone coming from a purely Ruby background.

Deployment in the Walled Garden

Submitting to the Mac App Store can be a complicated process, even when using only vanilla frameworks. MacRuby (and Apple) have made this a moving target, whether it’s Xcode’s failure to sign binaries, MacRuby embedding itself incorrectly, or other build inconsistencies.

With Xcode 4.1, our current process is this:

  • Archive project with Xcode
  • Export archive from Xcode’s organizer
  • Embed MacRuby’s framework and compile the Ruby code from the command line
  • Manually code-sign app
  • Manually build installer package and submit with Application Loader

This process has actually gotten more complex over time, and it’s unclear where the fault lies in that. Initially, all building, embedding and compiling took place via Xcode actions, but over time, each step has broken down.

If you’re having any issues deploying, feel free to ping me on Twitter. It might be the subject of an upcoming blog post.

Blindly Debugging

Let’s look at an issue I just had to track down in the production build of Briquette.

1
10/3/11 1:03:54.260 PM Briquette: /Applications/Briquette.app/Contents/Resources/rb_main.rb:25:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)

That’s not a great error message: rb_main.rb is the root Ruby file of a MacRuby application, and the interpreter (in)correctly catches errors there. Maybe the stack trace will help more…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
10/3/11 1:03:54.277 PM Briquette: (
	0   CoreFoundation                      0x00007fff8f78f986 __exceptionPreprocess + 198
	1   libobjc.A.dylib                     0x00007fff920c3d5e objc_exception_throw + 43
	2   libmacruby.dylib                    0x00000001001ec2ee rb_vm_raise + 430
	3   libmacruby.dylib                    0x00000001000cc6b9 rb_exc_raise + 9
	4   libmacruby.dylib                    0x00000001001f0c82 rb_vm_method_missing + 562
	5   libmacruby.dylib                    0x00000001001dc3d4 rb_vm_dispatch + 7828
	6   libmacruby.dylib                    0x00000001001df640 rb_vm_respond_to2 + 736
	7   libmacruby.dylib                    0x00000001001dab48 rb_vm_dispatch + 1544
	8   Room.rbo                            0x000000010898c8d4 Room.rbo + 6356
	9   Room.rbo                            0x000000010898cc65 Room.rbo + 7269
	10  Room.rbo                            0x0000000108995de6 MREP_81CDDE71AED14C6D8C5A0685717E5E9E + 35574
	11  libmacruby.dylib                    0x00000001001dc919 rb_vm_dispatch + 9177
	12  libmacruby.dylib                    0x0000000100181d02 rb_objc_isEqual + 1842
	13  Briquette                           0x0000000100019d6e Briquette + 105838
	14  CoreFoundation                      0x00007fff8f77f11d -[NSObject performSelector:withObject:] + 61
	15  Foundation                          0x00007fff88ed9830 __NSThreadPerformPerform + 214
	16  CoreFoundation                      0x00007fff8f6fe241 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
	17  CoreFoundation                      0x00007fff8f6fdaad __CFRunLoopDoSources0 + 253
	18  CoreFoundation                      0x00007fff8f7248d9 __CFRunLoopRun + 905
	19  CoreFoundation                      0x00007fff8f724216 CFRunLoopRunSpecific + 230
	20  HIToolbox                           0x00007fff8e9a84ff RunCurrentEventLoopInMode + 277
	21  HIToolbox                           0x00007fff8e9afc21 ReceiveNextEventCommon + 355
	22  HIToolbox                           0x00007fff8e9afaae BlockUntilNextEventMatchingListInMode + 62
	23  AppKit                              0x00007fff8b4abfc5 _DPSNextEvent + 659
	24  AppKit                              0x00007fff8b4ab8c9 -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] + 135
	25  AppKit                              0x00007fff8b4a820a -[NSApplication run] + 463
	26  AppKit                              0x00007fff8b72635e NSApplicationMain + 867
	27  ???                                 0x000000010462a5ad 0x0 + 4368541101
	28  libmacruby.dylib                    0x00000001001da979 rb_vm_dispatch + 1081
	29  ???                                 0x0000000104600c34 0x0 + 4368370740
	30  ???                                 0x000000010460059d 0x0 + 4368369053
	31  libmacruby.dylib                    0x00000001001f55f6 rb_vm_run + 470
	32  libmacruby.dylib                    0x00000001000ccd00 ruby_run_node + 80
	33  libmacruby.dylib                    0x00000001001ec021 macruby_main + 385
	34  Briquette                           0x000000010000180c Briquette + 6156
)

That narrows it down a little more. I can at least identify that it’s coming from my Room.rb model (lines 8-10). Better, but not great. The .rbo file extension is a compiled Ruby file, and as expected, being compiled wasn’t its life goal. It’s currently difficult to get good debugging symbols out of compiled Ruby, and the alternative — shipping uncompiled code — is a scary prospect3.

Even while developing, your tools will be limited. You can’t pause your application and step through Ruby code, since the built-in debugger isn’t designed to work with an interpreted language. The working solution is to use the tried-and-true print statement to query application state: effective, but far less valuable than Xcode’s advanced debugging (and profiling) tools.

At RubyConf this past weekend, @brixen’s talk about Nikita and Ruby tools included a great quip:

Ruby has the best tools

No OneEver

Debugging Ruby has never been a pleasurable experience, and it’s no different here. Hopefully future updates to MacRuby can address this with better Xcode integration.

Memory, or a Lack Thereof

@briquetteapp I like you but are you sure you need this much memory? d.pr/tggu

@briquetteapp Any upcoming changes around the memory consumption issue? It’s bad here: http://t.co/ay015aI

Tell me about it, guys. No, we certainly don’t need all that memory. Honestly, we’re still trying to track this one down, though we’ve squashed quite a few bugs recently. The crux of the issue is that when profiling, as with debugging, we can’t use standard tools to examine what objects our application is leaking. Instruments, Apple’s profiling tool, isn’t aware of Ruby objects and considers them all leaks, making it difficult to extract real data.

The other potential snag is that using MacRuby forces you to require garbage collection in your application. Apple’s garbage collector is lacking, and not all Cocoa frameworks are garbage collection safe. Xcode 4.1, with the new LLVM compiler, does an excellent job of tracking down the latter issue in Cocoa libraries, but determining the source of leaks otherwise is nearly impossible.

Our memory issues could be due to our use of Webkit4, bugs in Apple’s garbage collector, leaks in MacRuby itself, or bad design in our Ruby code. There’s just no easy way to find out. I can say with certainty that MacRuby claims ~70-100MB of RAM per process. Where does the rest of it go? We’ll let you know.

Well That Sounds Awful

It’s not.

No really, it isn’t.

This isn’t a condemnation of MacRuby or its effectiveness as a framework. As a developer mapping my Ruby knowledge onto my Objective-C and Cocoa experience, I found some surprises along the way. I do want new MacRuby developers to be informed about potential pitfalls, and be ready for whatever challenges come their way. Despite the setbacks, MacRuby enabled us to rapidly prototype and iterate on an idea, and easily build and submit it to the App Store.

  1. Bearded is where I work. It’ll be on the “About” page once that’s live, but if you’re impatient, check out the great work of all the Beardeds at http://bearded.com.

  2. Some gems won’t work quite right. As MacRuby is an implementation distinct from the standard MRI Ruby, compatability issues can arise.

  3. You can certainly ship a MacRuby app with uncompiled Ruby files, but it exposes any Ruby source code used by your application. See? Petrifying!

  4. Seriously, Webkit is a bad citizen. Have you used Safari lately?

Comments