Skip to content
jballanc edited this page Dec 16, 2012 · 13 revisions

Method call syntax

When copying Objective-C method signatures over from source or documentation in order to modify in a MacRuby program, note the first argument of the method should not be separated from the method name with ':'.

E.g. you'd call -[NSString initWithData:encoding:] in MacRuby as:

NSString.alloc.initWithData(data, encoding:encoding)

or:

NSString.alloc.initWithData data, encoding:encoding

but not:

NSString.alloc.initWithData:data, encoding:encoding

This can lead to confusing errors depending on whether the Cocoa method functions with a Ruby symbol.

Here is a small ruby script to translate the method calls from the apple docs into MacRuby syntax.

Lowered-case constants

Sometimes certain Objective-C APIs declare constants or enumerations whose names start with a lower-case letter. For instance:

extern int kFoo;

These constants are still accessible from MacRuby, however in Ruby constant names must be capitalized. The constant above can be used from MacRuby like this:

puts KFoo

The concept of nil

The notion of nil, the null object, has manifested differently between Ruby and Objective-C. Both Objective-C and Ruby have a nil object. The details of the Objective-C nil class are private and only vaguely defined in the Objective-C Runtime Reference, but the Ruby NilClass is well documented (and public).

Messaging the nil object/class

As mentioned above, the Ruby nil object has a defined API and will respond to only the methods it has defined. If you call a method on nil and it doesn't have a definition for the method then you will get an exception.

nil.class  # => NilClass
nil.to_s   # => ""
nil & true # => false
nil.join   # => NoMethodError: undefined method `join' for nil:NilClass

nil has no defined interface in Objective-C. What happens when you message nil? You get nil back. Always. In Objective-C, nil will always return nil for any method you call.

[nil to_s]; // nil
[nil join]; // nil
[[nil size] multiplyBy:2]; // nil

You will notice that we chained two method calls together in that last example. Since nil returns nil we just end up messaging nil again! Objective-C developers may not check for nil values in their APIs because of this behaviour and they may not care if they return nil for the same reason.

Variable arguments (Objective-C APIs that expect nil as a list terminating token)

Some Cocoa APIs, specifically those that accept a variable number of arguments, were designed to work with nil differently than you would expect in Ruby. Objective-C level APIs will often require that you pass nil as the last argument in a list to signal the end of a variable length list.

framework 'Foundation'
NSArray.arrayWithObjects(1, 2, 3, nil)

In Ruby it is not unnatural to have an array that includes nil objects, so you should be careful when going back and forth between APIs. This problem may actually be solvable by MacRuby in the future, but for now it is a "gotcha".

Cocoa has an NSNull class, which is meant to act as a bandage for these edge cases, but you will have to apply it manually.

Additionally, sometimes you may get crashes when trying to call a method that accepts a variable number of arguments. For instance, the following snippet alone will crash.

NSArray.arrayWithObjects(1, 2, 3, nil)    # crashes

The solution is to require the Foundation framework before calling the method (as in the previous snippet that doesn't crash). It is necessary since MacRuby does not load certain bridging features by default, and defers loading them until a framework has been loaded. There may also be additional metadata information for those special APIs that must be loaded by bridge support at runtime.

TL;DR

If you want to use Cocoa APIs, make sure to load the appropriate frameworks using the framework method.

MacRuby seems slow

There are a few things that can cause MacRuby to be slow. At boot time there are a couple of factors, but at runtime there is really only one culprit.

Allocations

When using certain gems, or when you have many gems installed, you will notice that the load time for your scripts is very long---possibly more than 10 seconds. There are a few contributing factors to why this happens.

One problem is huge literal collections. Some gems contain source code with unbelievably large literal collections, such as the addressable gem.

This is a problem for MacRuby for two reasons. First, it requires several thousand allocations at once. Most of MacRuby's performance issues come from code that allocates too much, and large collections can allocate several thousands of objects all at once. The fix to this problem will have to come from upstream gem developers and MacRuby itself. In the mean time, AOT compilation of gems can usually make them load significantly faster, but has some other trade offs.

A good sign of too many allocations is if your code is running abnormally slow after boot. An easy way to check is by running the same code with CRuby (if possible) and compare performance. Remember that things like literal strings have to be #duped every time the line of code they are on is executed, whereas immutable things like symbols, numbers, and regular expressions do not have to be copied. At the same time, if you pass a symbol to a method that will coerce the symbol to a string then you haven't saved an allocation.

The allocation issue in your program may be the way you've architected your code, or perhaps you have many intermediate allocations that are not needed and could be replaced with methods that perform in-place mutation. A contrived example of lots of intermediate allocations might look like this:

  def sanitize string
    string.gsub(/pie/, 'cake').gsub(/lie/, 'delicious').upcase
  end

While the example is relatively compact code, and Ruby has been created for programmer happiness, it makes the computer a bit sad; MacRuby will have to allocate a new string for each #gsub call, the #upcase call, and then also has to #dup the "cake" and "delicious" strings.

JIT

As mentioned above, there is another problem with large literal objects. As it turns out, JIT isn't that great for short lived processes that need to start up over and over again. The overhead of JIT'ing can also be very expensive for very large chunks of code.

MacRuby will normally try to JIT the code (unless you have used AOT compilation), and the LLVM chokes on things literal hashes with thousands of entries. Some work has been done to break JIT'ing into smaller pieces (it used to take over 2 minutes to load the addressable gem), but it can still take a while for MacRuby and the LLVM to work through the code.

If you need to start a program frequently and it is taking a long time, try disabling the JIT compiler as described in the Troubleshooting wiki page.