Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Open Source

Examining RubyCocoa


May02: Programmer's Toolchest

Chris is a system software engineer who has contributed to Apple's Carbon framework for Mac OS X. He can be reached at [email protected].


Apple's Mac OS X melds the best features from several disparate operating systems and environments, including BSD UNIX, the Mach microkernel, and classic Mac OS. However, one of the more interesting technologies in the Mac OS X melting pot is Cocoa, an application framework derived from what was originally the NeXT Application Kit — a powerful object-oriented, user-interface framework (http://developer.apple.com/techpubs/macosx/Cocoa/CocoaTopics.html). You can use Cocoa to create applications that are at once useful, beautiful, unique, and familiar. Cocoa is solidly grounded in the model-view-controller design pattern, which lets you not only create sophisticated applications, but successfully maintain them over time.

Every copy of Mac OS X ships with developer tools such as the Project Builder IDE and Interface Builder. Together with Cocoa, these tools give Mac OS X users a powerful platform for rapid application development.

The Cocoa framework is composed of two subframeworks:

  • Foundation is a useful set of building-block objects for interacting with the underlying operating system, such as string, file, and thread classes.
  • AppKit is a collection of UI objects that applications use to implement the Mac OS X GUI.

Normally, both Cocoa subframeworks are programmed using Objective-C, a Smalltalk influenced, object-oriented variant of C (http://developer.apple.com/techpubs/macosx/Cocoa/ObjectiveC/index.html). The Objective-C run-time support library lets classes and methods be added at run time, and provides facilities for introspection through a programming interface that provides access to the internal data structures of Objective-C class definitions. These Objective-C run-time APIs can be used to admit other OO languages with dynamic method dispatch capabilities into the Cocoa programming environment. Apple provides bridge layers for programming Cocoa in Java and in AppleScript.

Enter RubyCocoa. Ruby, created by Yukihiro Matsumoto, is an interpreted, strongly typed, fully object-oriented language (see "Programming in Ruby," by Dave Thomas and Andy Hunt, DDJ, January 2001 and http://www.ruby-lang.org/). Ruby makes OO programming easy and enjoyable without placing arbitrary constraints on you. RubyCocoa, developed by Hisakuni Fujimoto, is a combination Mac OS X framework and Ruby extension that provides a bridge between the Objective-C implementation of Cocoa and the Ruby language (http://www.imasy.or.jp/~hisa/mac/rubycocoa/INDEX.en.html).

Ruby is a powerful productivity aid and can speed up development manyfold. But since the Ruby interpreter is currently not very fast, performance-critical code should still be written in C. RubyCocoa is well suited to prototypes and simple custom applications. There currently isn't a packaging mechanism that lets you create a standalone RubyCocoa application you can ship to end users; RubyCocoa requires each user to have both Ruby and RubyCocoa installed.

So What Can You Do with RubyCocoa?

With RubyCocoa, you can do just about anything in Ruby that you can do in Objective-C. Additionally, Ruby provides many benefits to Cocoa programmers, aside from Ruby's many inherently useful features (such as Perl-like regular expressions, infinitely large integer data types, and iterators).

From a language perspective, Ruby's primary strength in combination with Cocoa is that it removes the need to perform the most tedious tasks of Objective-C programming. For example, Objective-C objects allocated from Ruby are garbage collected. Garbage collection eliminates the need to track down memory leaks and crashes related to dangling object pointers, tasks that are frequent debugging headaches even for experienced Cocoa programmers.

Ruby also removes the need to write most accessor functions, another tedious, time-consuming task in Cocoa. In Ruby, the attr_writer, attr_reader, and attr_accessor functions do all of the work of creating the accessors, while still letting you change the underlying implementation without impacting clients of your class.

From a run-time perspective, Ruby lets you write applications quickly. Because there is no need to compile and link the program, you can skip the most time-consuming part of the application build process. The downside is that errors are discovered at run time.

Hello, Ruby Baby

Listing One, which illustrates use of Cocoa from Ruby, uses RubyCocoa to create a window with a transparent background displaying a text message. This is a simple Cocoa program that demonstrates some basic features of RubyCocoa. It uses a single custom view class that overrides a single method, with a small amount of code to create the window and start the application's event loop.

Due to constraints imposed by the Mac OS X UI frameworks, a small amount of bootstrap C code (Listing Two) is required to start up the RubyCocoa framework run-time and start the Ruby interpreter running. Although the actual code is in C, this file has the ".m" Objective-C file extension because it might reference headers containing Objective-C class declarations.

This example is straightforward: Method calls basically map directly between Ruby and Objective-C, one-to-one.

Using RubyCocoa

All Cocoa objects are reference counted. When first allocated, objects have a reference count of 1. Calling an object's retain method increments the reference count by 1; calling its release method decrements the reference count by 1. When the reference count reaches zero, release calls the dealloc method to deallocate the object.

Reference counting lets multiple objects safely reference the same object. Without reference counting, one object might deallocate the referenced object without notifying other objects, leaving the other objects with an invalid reference. However, with reference counting, it is easy to get into situations where two objects both reference each other and can thus never be deallocated — a circular reference. This is one reason why garbage collection is useful — objects are kept allocated only as long as they are actually referenced.

In Objective-C, you instantiate an object by sending the alloc message to the object that represents the class, then by sending an init message to the resulting object; see Example 1(a). When using Objective-C objects in RubyCocoa, you do the same thing; see Example 1(b). When you're done with the object in Objective-C, you release it, as in Example 1(c). In Ruby, all Objective-C objects that are referenced by Ruby code are associated with Ruby wrapper objects, and when the associated Ruby object is released by the Ruby garbage collector, the Objective-C object is released as well. There is no need to release or retain objects in RubyCocoa.

Method Names

Calling a method in Objective-C requires you to perform some simple translation from Objective-C syntax into Ruby. In Objective-C, each argument in a method call is separated by colons and segments of the name of the method; see Example 2(a).

Ruby methods have no colons and use a C-like comma-separated form for arguments, so RubyCocoa translates Objective-C method names into Ruby using a different form, replacing the colons with underscores, as in Example 2(b). For convenience, you can leave off trailing underscores; see Example 2(c).

Finally, RubyCocoa provides a form that lets you preserve some semblance of the Objective-C flavor and break up extremely long method names. You can pass each portion of the Objective-C selector following the initial portion to the function as a Ruby symbol; see Example 2(d).

Subclassing

RubyCocoa exports all of Cocoa through a Ruby module called OSX. You can directly subclass Cocoa classes, override methods, and call superclass method implementations.



class RubyDocument < OSX::NSDocument</p>
end

To override an Objective-C method in Ruby, use the ns_overrides declaration to tell RubyCocoa to register the method with the Objective-C run time:



class RubyDocument < OSX::NSDocument</p>
   ns_overrides :documentNibName</p>


def documentNibName</p>


"MyDocument.nib"</p>


end</p>
end

To call an Objective-C superclass's version of a method, prefix the method name with super_:



def windowControllerDidLoadNib(sender)</p>
super_windowControllerDidLoadNib(sender)</p>


end</p>

Parameter Conversion

When you pass a Ruby string to an Objective-C method, RubyCocoa automatically converts the string into an NSString. RubyCocoa uses the type information of the Objective-C method to perform automatic data conversion on all other basic data types.

RubyCocoa cannot currently handle arbitrary unions or data structures, so certain commonly used data structures (such as NSRect, NSPoint, and NSRange) are handled as special cases. RubyCocoa defines these classes in Ruby, and you allocate them using the standard Ruby new method; see Example 3(a).

RubyCocoa can automatically convert arrays to NSRects and NSPoints, which simplifies the task of defining these structures inline; see Example 3(b). Also, Ruby ranges are converted automatically to NSRange data structures; see Example 3(c).

Functions and Data Types

Cocoa defines a small number of C functions and data types (in addition to those just mentioned), and RubyCocoa provides these within the OSX module as well. Global variables such as NSFontAttributeName are implemented as functions within the OSX module, so you access them like functions (OSX.NSFontAttributeName). Cocoa C functions can be accessed just like normal module functions (OSX.NSBeep). RubyCocoa implements enum values as Ruby constants (OSX::NSNotFound).

Exceptions

RubyCocoa translates exceptions raised by Objective-C code into Ruby exceptions. The class OSX::OCException contains information about the original Objective-C exception. It is a simple wrapper for the Cocoa NSException class.

Ruby and Interface Builder

When you load an Interface Builder file ("nib," a historical abbreviation for "NeXT Interface Builder"), the Cocoa nib loading code asks the Objective-C run time for pointers to the instance variables corresponding to the outlets connected to the views in the nib. It sets the value of each outlet instance variable to the corresponding object instantiated from the nib.

To support nib files, RubyCocoa provides the ib_outlets declaration, which adds specified instance variables to the Objective-C run time so that the nib loading code can find them.



class RubyDocument < OSX::NSDocument</p>
   ns_overrides :documentNibName</p>


ib_outlets   :textView</p>


def documentNibName</p>


"RubyDocument.nib"</p>


end</p>


end</p>

A Simple Test Editor in Ruby

MyDocument.rb (available electronically; see "Resource Center," page 5) is a minimal complete text editor written entirely in Ruby (see Figure 1). This example illustrates some of the features of the Cocoa framework, the use of RubyCocoa in a real application, and some capabilities of pure Ruby that can be used to enhance Cocoa. In addition to basic styled text editing, this application contains a basic incremental search feature, similar to that seen in Emacs and other text editors. When users type into a small text field in one corner of the text document window, the corresponding text is instantly selected (if it is found) in the main text view. Because this editor is implemented in Ruby, you can support text containing regular expressions in this field with little additional effort.

So where is all of the application code that handles opening files, instantiating the document class, loading the interface, managing the window, and so on? The answer is that, by default, Cocoa provides all of this basic document-handling functionality. All you have to do is specify the name of a subclass of NSDocument in the application's settings, and Cocoa will do the rest — the reading of the file data from disk, creating the document window given an Interface Builder nib file, maintaining the menu items, and other behind-the-scenes chores. You implement only the code specific to your application — Cocoa handles the rest.

For more complex applications, you can change more of the underlying behaviors and achieve better model-view-controller layering by creating subclasses of NSWindowController in your NSDocument subclass. For this example text editor, a simple NSDocument subclass suffices.

This example shows a few things that RubyCocoa's automatic data conversion does for you, and also shows a few things that you must do yourself. When you pass a Ruby string to an Objective-C method, RubyCocoa automatically converts the string to an NSString. However, when comparing an NSString object to a Ruby string, you must explicitly convert the NSString to a Ruby string using the to_s method. The same logic applies to NSRange and the Ruby range class.

To build this application, you'll need to use the document-based template provided with RubyCocoa. You'll also need to perform a small amount of work in Interface Builder and Project Builder to set up the basic window interface and application metadata. First, in Project Builder, create a new project using the RubyCocoaDocApp template provided in the RubyCocoa distribution. In the project, replace the source with the code in MyDocument.rb. Next, open the project file MyDocument.nib in Interface Builder. Add a text view and a text field for incremental search to the window, and use the inspector window to add outlets named text_view and incremental_search_field to the MyDocument class. Connect the outlets to the objects, save, and you're ready to build and run.

Conclusion

RubyCocoa is still in early development, and will become even more useful and flexible as the inevitable bugs are worked out and more features are added. Some form of compatibility with RIGS, the Objective-C bridge for the GNUstep project (an open-source implementation of the Cocoa libraries for Linux and BSD systems) is a possible project. Another possible project is an application packaging mechanism to let RubyCocoa applications be used on systems that don't have Ruby or RubyCocoa installed. Ruby itself will gain additional valuable features and the speed of the Ruby interpreter will likely improve as performance optimizations are added and bytecode compilation and loading are introduced.

Apple will continue to enhance Cocoa, adding missing features and fixing inevitable bugs. With a small number of notable exceptions (all for historical reasons), nearly all of the applications that ship with Mac OS X were created with Cocoa, including the development tools. Additionally, Apple may eventually publish specifications allowing third parties to develop plug-in modules for the Project Builder development environment. When and if this happens, we will be able to develop Project Builder plug-ins for easier navigation and interactive debugging of Ruby code.

Acknowledgment

Many thanks to Hisakuni Fujimoto for both inspiring and reviewing this article.

DDJ

Listing One

require 'osx/cocoa'
class HelloView < OSX::NSView
    # Tell RubyCocoa to setup Objective-C overrides
    # for the NSView method drawRect:.
    ns_overrides    :drawRect_
        # When the Cocoa view system wants to draw a view, it calls the 
        # method -(void)drawRect:(NSRect)rect. The rectangle argument is 
        # relative to the origin of the view's frame, and it may only be 
        # a small portion of the view. For this reason, simple views with
        # only one or two graphical elements tend to ignore this parameter.
    def drawRect(rect)
        # Set the window background to transparent
        OSX::NSColor.clearColor.set
        OSX::NSRectFill(bounds)
        # Draw the text in a shade of red and in a large system font
        attributes = OSX::NSMutableDictionary.alloc.init
        attributes.setObject_forKey(  OSX::NSColor.redColor, 
                                      OSX.NSForegroundColorAttributeName )
        attributes.setObject_forKey(    
                                      OSX::NSFont.boldSystemFontOfSize(48.0),
                                      OSX.NSFontAttributeName )
        string = OSX::NSString.alloc.initWithString( 
                                      "Hello, Ruby Baby" )
        string.drawAtPoint_withAttributes([0,0], attributes)
        # Turn window's shadow off/on. This is a kludge to get the shadow
        # to recalculate for the new shape of the opaque window content.
        viewWindow = window
        window.setHasShadow(0)
        window.setHasShadow(1)
    end
end
# If this file is the main file, then perform the following commands. (This
# construct is often useful for adding simple unit tests to library code.)
if __FILE__ == $0
    # First, to establish a connection to the window server,
    # we must initialize the application
    application = OSX::NSApplication.sharedApplication

    # Create the window
    window = OSX::NSWindow.alloc.initWithContentRect([0, 0, 450, 200],
                :styleMask, OSX::NSBorderlessWindowMask,
                :backing,   OSX::NSBackingStoreBuffered,
                :defer,     0)
    # Allow the window to be partially transparent
    window.setOpaque(0)

    # Setup the window's root view
    view = HelloView.alloc.initWithFrame([0, 0, 450, 200])
    window.setContentView(view)

    # Place the window near the top of the screen.
    # (Screen coordinates in Cocoa are always PostScript coordinates, which 
    # start from the bottom of the screen and increase as they go up, so we 
    # have to do some math to place the window at 100 pixels from the top 
    # of the screen.
    screenFrame = OSX::NSScreen.mainScreen.frame
    windowOriginPoint = 
               [40, screenFrame.origin.y + screenFrame.size.height - 100]
               window.setFrameOrigin( windowOriginPoint )
    # Show the window
    window.makeKeyAndOrderFront(nil)

    # And start the application event loop
    application.run
end

Back to Article

Listing Two

#import <RubyCocoa/RBRuntime.h>
int main( int argc, char* argv[] )
{
  return RBApplicationMain("helloruby.rb", argc, argv);
}




Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.