RubyMotion Block Scope Bug
As a Rubyist, there is not much that is more exciting to me than the idea programming iOS in Ruby. For that reason, I was excited to try RubyMotion (link). Unfortunately, in our experiments, we pretty quickly ran into a bug that seemed to be a deal-breaker until we were able to experiment our way to a workaround.
In short, we found that block scope in RubyMotion does not behave as you would expect it to in either Ruby or Objective-C. Local variables defined outside the block will not be available inside the scope. Here are some details on what works, what fails, and how to work around the issues.
Ordinarily in both Objective-C and Ruby locally defined variables are bound to a block when it is declared.
1 2 3 4 5 |
x = 'hello failure'
Dispatch::Queue.concurrent.async do
puts x
end |
In the above code, the expected behavior is that x is available in the block passed to the concurrent asynchronous queue. However, what actually happens in this is case is that RubyMotion will crash without an error notification.
Mason, one of the Objective-C heroes at Blazing Cloud, was the first to see this problem and suggest the underlying issue, variable scope. He suggested that we try retaining the local variables, a concept that is foreign to ordinary Ruby development.
1 2 3 4 5 6 |
x = 'hello work around'
x.retain
Dispatch::Queue.concurrent.async do
puts x
end |
Calling retain on the variable ensure that the local variable remains around after the block is defined. It’s unclear in the universe of RubyMotion what the memory implications might be for this workaround. Since we don’t have a clear sense of how retained variables are released in this hybrid Ruby/Objective-C universe, does this retained variable get cleaned up?
Another interesting note, is that ‘x.copy’ which would also work in Objective-C, does not work in RubyMotion. It is not clear whether the ‘copy’ method maps to the Objective-C memory reference function, or something else.
A more Ruby-like solution is to declare the variable as an instance variable.
1 2 3 4 5 6 |
@x = 'hello another work around'
Dispatch::Queue.concurrent.async do
puts @x
end
</code> |
While this provides a clearer sense of the memory implications, and it makes more sense in Ruby-land, it demands that we structure our classes to hold on any variable required by a block. Holding local method variables as state on the instance muddies the waters of our object and is not ideal for design.
I would like to say I have another solution up my sleeve, but I don't yet. Maybe they will come out with a bug fix. Maybe with more work in RubyMotion, a cleaner workaround will emerge.
3 Comments
Just a follow up, the bug has been fixed in RubyMotion 1.20 (which was released a couple months ago).
in 1.29 even global variables aren’t working. When declared outside a block, they don’t change state when the block ends.
Nevermind. PEBCAK.