This page shows some tips and pitfalls from my experience in using RubyCocoa. You should also take a look at the FAQ page at the RubyCocoa's official site.
- Pitfalls
- Ruby thread or NSThread may cause weird behaviors
- Invalidating NSTimer in its timer handler will crash your application
- cloned/dup'd NSBoxed object (like NSPoint, NSSize, NSRect) causes an exception (on RubyCocoa-0.11.1)
- Passing -d (DEBUG) option may crash your application (occurs on RubyCocoa-0.4.3d2 or earlier)
- Got an error "uninitialized constant" when using a class in a framework
- 'include OSX' at the top-level Object will not allow a module to implicitly use OSX name scope
- Exception 'null class#<method> invalid object/message' raises but nothing is wrong with the class.
- Tips
Pitfalls
Ruby thread or NSThread may cause weird behaviors
According to rubycocoa-dev ML, using threads on your RubyCocoa application is not an easy thing. As long as I've experienced, creating OSX::NSThread on your RubyCocoa application will never run on a new thread but runs on the main thread.
Using ruby thread instead of NSThread may also cause weird behaviors or crash your application. So you better not use threads as long as you can.
One thing you MUST NOT do is create an NSThread in a ruby thread (like the code below). It will crash your application.
Thread.new {
OSX::NSThread.detachNewThreadSelector(
selector, :toTarget, self, :withObject, nil)
}
By the way, using only one thread really matters to you? Oh yes. When you process some long process in response to a user action on a window, the beach-ball will come up. This means you cannot do anything during the process.
Workaround
When you want to do some process that takes more than a few seconds, you can make the process timer-activated one. this means that you split entire routines in the process into small pieces so that each piece can run on every NSTimer event. This is not an elegant way but it will get the job done.
Invalidating NSTimer in its timer handler will crash your application
Though NSTimer is one of the inevitable things on your application, it has a problem that you cannot invalidate an NSTimer object within its timer handler. If you do this, NSTimer will punish you by launching GDB, showing tons of method call stuck info. For example, the code below will crash your application.
def keyDown(event=nil)
@timer = OSX::NSTimer.scheduledTimerWithTimeInterval(1.0, :target, self,
:selector, :tick, :userInfo, nil, :repeats, true)
end
def tick(object=nil)
# do something here and
# invalidate the timer
@timer.invalidate if (someCondition) # <- crashes your application
end
The same thing happens when you invoke [timer invalidate] from its timer callback method in Objective-C code. This is not a RubyCocoa specific thing but one thing you should know.
Workaround
Sometimes you may want to invalidate a timer on its callback routine (assume it is not a one-shot timer). In this situation, you can delay the invalidation by using the code below.
class TimerEvent
def initialize(listener, interval)
@listener = listener
@timer = OSX::NSTimer.scheduledTimerWithTimeInterval(interval,
:target, self, :selector, :deliver, :userInfo, nil, :repeats, true)
end
def deliver(object=nil)
TimerInvalidater.instance.invalidateOldTimers()
@timerInvoked = true
@listener.tick(object) (if (@listener && defined?(listener.tick))
@timerInvoked = false
end
def invalidate()
if (@timerInvoked)
TimerInvalidater.instance.reserveInvalidation(timer)
else
@timer.invalidate()
end
end
class TimerInvalidator
include Singleton
# reserve a timer for invalidating later time.
def reserveInvalidation(timer)
@timers = [] if (!defined?(@reserved))
# Set fire date to 200 hours later,
# reserving the timer to be invalidated when another timer event occurs
timer.setFireDate(OSX::NSDate.dateWithTimeIntervalSinceNow(720000))
@timers << timer
end
def invalidateOldTimers()
return if (!defined?(@reserved))
@timers.each { |timer| timer.invalidate() }
end
end
# this class receives timer event
class TimerEventListener
def initialize()
@timer = TimerEvent.new(self, 1.0)
end
def tick(object)
#do something
@timer.invalidate() if (someCondition)
end
end
TimerEvent in this code encapsulates an NSTimer event. It generates an NSTimer object, specifying TimerEvent.deliver as a timer callback method. When a timer event occurs, NSTimer invokes TimerEvent.deliver. TimerEvent.deliver, then, invokes @listener.tick. To invalidate an NSTimer at listener.tick, call TimerEvent.invalidate instead of calling NSTimer.invalidate.
TimerEvent.invalidate delays the invalidation of a timer at the next time when TimerEvent.deliver is called for another timer. You should call TimerInvalidator.invalidateOldTimers at the time you quit your application.
cloned/dup'd NSBoxed object (like NSPoint, NSSize, NSRect) causes an exception (on RubyCocoa-0.11.1)
On RubyCocoa-0.11.1, assigning value to dup'd/cloned NSBoxed object causes an exception. Here is the simple code that causes "Given structure 0x4b83fc has null data." on 0.11.1.
point1 = NSPoint.new(10, 20); point2 = point1.clone if (point2.y < 100) # This doesn't raise exception point2.y += 10 # Exception occurs here, in this case point2 is 0x4b83fc end
Workaround
Easiest way to avoid this issue is to clone/dup an object manually. The code can be like this:
point2 = NSPoint.new(point1.x, point2.y)
It's very simple and not that smart but it works. Better way to do the same thing is to define dup/clone method in NSBoxed class as shown below (excempt from rubycocoa-devel list):
class OSX::Boxed
def dup
values = self.class.fields.map {|sym| self.send(sym)}
self.class.new(*values)
end
end
You can also alias this dup to clone. One of the best ways is to do this in C level, which is already done on rubycocoa r1817 in the svn repository. So the next release of RubyCocoa can fix this problem. I hope they add the alias for clone.
Passing -d (DEBUG) option may crash your application (occurs on RubyCocoa-0.4.3d2 or earlier)
RubyCocoa allows us to see a lot of verbose information by specifying -d option when debugging a RubyCocoa application. However, passing -d option to a RubyCocoa application may crash it. As long as I know, it causes BUS ERROR or an uncaught exception depending on your code, none of which occurs without designating -d option. This problem occurs in 0.4.2, 0.4.3d2, and cvs-head (as of 09/08/06)
As long as I've experienced, XXXX.alloc.init causes this problem. A single line of code like this:
NSString.alloc.init
will crash your application, showing the messages:
2006-09-13 04:39:59.772 ruby[1004] Did you forget to nest alloc and init? 2006-09-13 04:39:59.772 ruby[1004] *** Uncaught exception: <NSInvalidArgumentException> *** -length only defined for abstract class. Define -[NSPlaceholderString length]! Trace/BPT trap
Workaround
It's very simple. just don't use -d option. you may use -W2 option to see some warnings. You may also use ruby debugger by specifying -r debug.rb option for a RubyCocoa application. Though an application runs very slow in debug mode, ruby debugger tells you a lot of useful information.
Note
This issue is solved at RubyCocoa-0.5.0. Now you can use it.
Got an error "uninitialized constant" when using a class in a framework
When you try to use a class in a framework that you added to an Xcode project., this error sometimes happens. When you want to use QTMovie, you will add QTKit.framework to Xcode project. You compile it and run it, only seeing the error "undefined method `__ocid__' for nil:NilClass (NoMethodError)." even you import the class by using the code like this:
module OSX ns_import :QTMovie end
This error means that RubyCocoa cannot import the class because it doesn't know the existence of the class.
Workaround
This error occurs when you put such framework into the "Other Frameworks" group at the Xcode project window. Moving QTKit.framework to the "Linked Frameworks" group will solve this problem. You can also load the framework at the top of a script like this:
NSBundle.bundleWithPath('/System/Library/Frameworks/QTKit.framework').load
Loading a framework by using NSBundle is dynamic and thus very useful when you distribute an add-on game for MiniKidsGames since you don't have to recompile MiniKidsGames.
'include OSX' at the top-level Object will not allow a module to implicitly use OSX name scope
On RubyCocoa-0.5.0, the following code causes 'Uninitialized constant TextEntity::NSDictionary.'
require 'osx/cocoa'
include OSX # <- declared at the top-level Object
module TextEntity
def drawText(string, fontSize, color, point)
attrib = NSDictionary.dictionaryWithObjects( # <--- ERROR!
[NSFont.fontWithName(font, :size, fontSize), color],
:forKeys, [OSX.NSFontAttributeName,
OSX.NSForegroundColorAttributeName])
text=NSAttributedString.alloc.initWithString(string, :attributes, attrib)
text.drawAtPoint(point)
end
end
class MyView < NSObject
include TextEntity
def drawRect()
# <- MyView is a subclass of Object so it can implicitly use OSX name scope
NSColor.whiteColor.set
drawText("Hello", 24, NSColor.blackColor, NSPoint.new(100, 100))
end
end
In pure Ruby world, this should work since TextEntity is included in MyView so it must be able to use the OSX name scope. As a matter of fact, the code below works with no problem. In RubyCocoa world, unfortunately, the modules inside a class can't implicitly use OSX name scope. RubyCocoa bridges Cocoa classes by using OSX.const_missing, which is invoked when no constant is found in Ruby world, but it is not applied to a module that is included in a class when you include OSX at the top of a ruby script. The same thing happens even if you declare such modules inside the OSX module.
# In pure Ruby world, MyModule can use FakeOSX name scope with no problem
# This outputs "Hello" with no errors.
module FakeOSX
class FakeNSObject
def self.hello() puts "Hello" end
end
end
include FakeOSX
module MyModule
def sayHello() # calls FakeOSX::FakeNSObject.hello
FakeNSObject.hello()
end
end
class MyClass
include MyModule
def initialize()
sayHello()
end
end
MyClass.new
Note
A module can implicitly use OSX name scope on RubyCocoa-0.4.3d2 when you include OSX at the top-level.
Workaround
I've tried to find a good way to solve this problem but haven't found one. There are some workarounds to avoid this issue like:
- Inlcude OSX in each module
- Clearly add OSX:: to each Cocoa class inside a module
Exception 'null class#<method> invalid object/message' raises but nothing is wrong with the class.
You may encounter a error message like this during posting/delivering a notification. Most of the time this error means the method is missing. The following code causes this error.
def windowDidBecomeKey(notification=nil) view = @window.contentView view.addSubView(@mySubview) end # Exeception raised during posting of notification. Ignored. # null class#addSubView: - invalid object/message.
Solution
This error, in this case, shows the message 'addSubView' sent to view was wrong as there is no such selector in NSView. Changing the selector name to 'addSubview' will resolve the problem. Though I didn'g know what this error actually means, it took about half an hour to find the typo. How hard to find a typo!
Tips
Try 'ConstantName' if both OSX::Constants and OSX.Constants don't work
You might encounter such a situation that you cannot find a constant name. Bad thing is that you can't stop Ruby keep complaining "uninitialized constant <ConstantName> (NameError)" for either OSX::ConstantName or OSX.ConstantName.
For example, when you try to call QTMovie.setAttribute to set true to QTMovieLoopsAttribute, you may would the following code.
movie = OSX::QTMovie.alloc.initWithFile(filename) movie.setAttribute(true, :forKey, OSX::QTMovieLoopsAttribute)
It causes a name error. You would probabry change the code like this:
movie.setAttribute(true, :forKey, OSX.QTMovieLoopsAttribute)
But it doesn't work either. Next thing you would do is to look at QTMovie.h in QTKit.framework, only to see the line:
QTKIT_EXTERN NSString *QTMovieLoopsAttribute AVAILABLE_MAC_OS_X_VERSION_10_4_AND_LATER; // NSNumber (BOOL)
There are some ways to avoid this name error. One is to find the definition of a constant in Cocoa framework headers. Another one is to try using 'ConstantName.' For the example above, quoting the constant name QTMovieLoopsAtribute as shown below will get the job done.
movie.setAttribute(true, :forKey, 'QTMovieLoopsAttribute')
Though it may not solve all such problems, sometimes it works.
Note
OSX.ConstantName is obsolate on RubyCocoa-0.11.1 (I'm not sure when it became obsolate)