Rubinius for the Layman, Part 3 - Try Rubinius in 20 minutes

I guess we’ve all heard last week’s sad news about Engine Yard diminishing the awesome support they’ve given the Rubinius project. That’s personally how I see it: they’ve put the project on steroids for roughly a year, rather than “they’re now cutting back” x people.

As Brian Ford pointed out, Rubinius is a community project. And Rubinius is not going away. A lot of people can’t wait to have a Ruby written more in Ruby than in C or C++. Koichi Sasada (lead developer on Ruby 1.9) even recently projected that Rubinius would eventually be the Ruby implementation of choice.

Rubinius’ future is promising. I’d like to go from that positive note, and have YOU try Rubinius now. I promise that in 20 minutes, you’ll be running Rubinius and enjoying it. The longest parts of the process will be waiting (cloning, compiling), so it won’t even be difficult. Note: the article may look long, but it’s not. Half of it is code samples and the results returned.

So it’s been aeons since the last article of the RFTL series. But yes, this article is part of a series. You can look at the first 2 parts if you want. Some details1 won’t be up to date anymore, but it’ll help you get the gist of Rubinius if you’re not familiar with the project yet.

The 20 minutes timer starts now, for those who went ahead and read the past articles.

Install Rubinius

Prerequisites

Prerequisites are probably already taken care of on most Ruby developers’ machines:

  • ruby 1.8, rubygems, rake and ParseTree (sudo gem install rake ParseTree)
  • git (no need to understand it, really, just having it installed)
  • general C++ building tools

Dependencies for Mac users

Leopard users should have all they need when Xcode is installed. Insert your Leopard upgrade DVD and from your terminal:

open /Volumes/Mac\ OS\ X\ Upgrade\ DVD/Optional\ Installs/Xcode\ Tools/XcodeTools.mpkg 

Dependencies for Linux users

Linux users have to make sure the following packages are installed. Use your the equivalent command for your distro:

sudo apt-get install gcc bison make pkg-config libtool git

(where make == GNU make)

Get the Rubinius code

Right now the GitHub feature to download a tarball is disabled for big projects like Rubinius. They say the feature on the radar as one of the things to fix in the next few weeks. Which will be awesome.

For now we have have to clone the repo, which in this case takes a few minutes. Find yourself a comfortable directory and:

git clone git://github.com/evanphx/rubinius.git
# go make coffee
cd rubinius

Build Rubinius

Assuming you’ve installed all the dependencies, the following should work right off the bat. If it’s not the case, check out the notes on the subject.

rake build
# go get a http://www.brawndo.com/

Later, when you want to recompile from a clean slate, just run rake distclean.

If you have not gotten a BRAWNDO, please skip over the next paragraph.

RUBINIUS, like BRAWNDO, is one of the CRAZIEST ideas of the LAST DECADE! Can you realize you’re ABOUT to try the Ruby IMPLEMENTATION that’s got the BEST Ruby code ratio AMONG THEM ALL! Isn’t that FREAKING AWESOME? You can also do some of the CRAZIEST INTROSPECTION with MethodContext, StaticScope and other classes like THAT!

Run Rubinius

The Rubinius executable is rbx in the ‘bin’ subdirectory. It’s a bit different from MRI in that rbx starts an irb session if you don’t specify a file to run. So let’s do that:

bin/rbx
# in irb
puts "Don't you spring a hello world on me"
#=> Don't you spring a hello world on me

Ok then. Well, technically you’ve now tried Rubinius in less than 20 minutes. Now if you keep reading, you’ll really taste some true Rubinius awesomeness.

Kick-ass introspection

Let’s start slow by patching Object to help us quickly grok the new kinds of objects we may encounter:

class Object
  # Return only the methods not present on basic objects
  def interesting_methods
    (self.methods - Object.new.methods).sort
  end
end
#=> #<CompiledMethod interesting_methods file=(irb)>

And now let’s create a basic little class that will help us start our exploration of a MethodContext instance.

class C
  def initialize
    @inst = 42
  end
  def get_mc
    local_var = 'value'
    MethodContext.current
  end
end
#=> #<CompiledMethod get_mc file=(irb)>

So with the help of an instance of the class C, let’s start poking gently at a MethodContext.

c   = C.new
ctx = c.get_mc
#=> #<MethodContext:0xcc #<C:0xca>#get_mc (irb):7>

ctx.interesting_methods
#=> ["__add_method__", "_get_field", "_set_field", "activate", 
 "active_path", "alias_method", "back_ref", "block", "class_variable_defined?",
 "const_defined?", "const_path_defined?", "context_from_proc", "context_stack", 
 "copy", "current_scope", "describe", "disable_long_return!", "dynamic_locals", 
 "file", "fp", "from_eval?", "get_eval_local", "ip", "ip=", "last_match", 
 "last_match=", "line", "lines", "locals", "locals=", "location", 
 "make_independent", "method=", "method_module", "method_scope", 
 "method_scope=", "name", "normalized_name", "nth_ref", "position_info", 
 "receiver", "receiver=", "reload_method", "script_object", "send_private?", 
 "sender", "set_eval_local", "set_iseq", "sp", "stack_trace_starting_at"]

ctx.name
#=> :get_mc

ctx.describe
#=> "C\#get_mc"

ctx.method_module
#=> C

Interesting, that reminds me of the monkey-patching discussion that often comes up in the Ruby community. Let’s try something else:

module ModuleMC
  def module_mc
    MethodContext.current
  end
end
#=> #<CompiledMethod module_mc file=(irb)>

C.include ModuleMC
#=> [ModuleMC]

c.module_mc.method_module
#=> #<IncludedModule:0xd6>

c.module_mc.method_module.name
#=> "ModuleMC"

Wouldn’t it be nice if we had that kind of introspection, when comes time to debug some mixin magic?

Now let’s go back our MethodContext object. Or rather, its method accessor, which gives us a CompiledMethod instance:

m = ctx.method
#=> #<CompiledMethod get_mc file=(irb)>

m.interesting_methods
#=> ["__ivars__", "__ivars__=", "activate", "activate_as_script", 
"as_script", "child_methods", "compile", "decode", "describe", 
"exceptions", "exceptions=", "file", "file=", "first_ip_on_line", 
"first_line", "from_string", "hints", "hints=", "inherit_scope", 
"is_block?", "iseq", "iseq=", "line_from_ip", "lines", "lines=", 
"literals", "literals=", "local_count", "local_count=", "local_names", 
"local_names=", "locate_line", "min_stack_size", "name", "name=", 
"primitive", "primitive=", "private?", "protected?", "public?", 
"required_args", "required_args=", "scope", "scope=", "send_sites", 
"serial", "serial=", "splat", "splat=", "stack_size", "stack_size=", 
"total_args", "total_args="]

m.describe
#=> "method get_mc: 0 arg(s), 0 required"

m.local_names
#=> #<Tuple: :local_var>

m.literals
#=> #<Tuple: "value", :MethodContext, #<SendSite:0xda 
#     name=current hits=0 misses=0>>

m.file
#=> :"(irb)"

The local_names method sounds extremely promising, but unfortunately for now, there’s no primitive for actually getting the local variable’s value, but it’s perfectly possible3. It’s just not been done yet.

By the way, what is that? #describe summarizes the arguments? Let’s try something more interesting with it:

def method_with_args(arg1, arg2='default', *args)
  MethodContext.current
end
#=> #<CompiledMethod method_with_args file=(irb)>

ctx2 = method_with_args(42, 'towel', "don't panic")
#=> #<MethodContext:0x16e main#method_with_args (irb):4>

m2 = ctx2.method
#=> #<CompiledMethod method_with_args file=(irb)>

m2.describe
#=> "method method_with_args: 2 arg(s), 1 required, splatted."

m2.local_names
#=> #<Tuple: :arg1, :arg2, :args>

m2.literals
#=> #<Tuple: "default", :MethodContext, #<SendSite:0x16c 
#     name=current hits=1 misses=0>>

Nice! Ok, now let’s come back to our CompiledMethod instance and check out it’s scope accessor.

ss = m.scope
#=> #<StaticScope:0xea parent=#<StaticScope:0xe8 parent=nil 
#     module=Object> module=C>

ss.interesting_methods
#=> ["initialize", "module", "parent", "script", "script="]

ss.parent
#=> #<StaticScope:0xe8 parent=nil module=Object>

So now we’ve essentially poked 2 levels deep: ctx.method.scope. Let’s rewind again and look at a our context’s receiver and sender accessors. To better understand both, let’s come back to our OO roots of 30 years back and start calling ‘method calls’ ‘messages’ instead.

sender (sends message ‘get_mc’) => receiver

sender is then the caller of the method, and receiver is, the object receiving the message. Also known as self, during the execution of the method.

Let’s see that in action:

r  = ctx.receiver
#=> #<C:0xca @inst=42>

r == c
#=> true

So ctx.receiver is a reference to the instance we’d put in the variable c. From there of course we can do Ruby’s regular meta-poking around:

r.instance_variable_get '@inst'
#=> 42

Now let’s look at the sender:

s = ctx.sender
#=> #<BlockContext:0xf0 main#irb_binding (irb):1>

s.class.ancestors
#=> [BlockContext, MethodContext, Object, PP::ObjectMixin, Kernel]

s.interesting_methods - ctx.interesting_methods
#=> ["env", "home"]

s.home
#=> #<MethodContext:0xf6 main#irb_binding
#     /Users/mat/dev/_rubies/rubinius/rubinius/lib/irb/workspace.rb:1>

s.env
#=> #<BlockEnvironment:0xf8 @initial_ip=0 @last_ip=268435456 
#     @post_send=0 @bonus=#<Tuple: true>>

When calling a method from IRB, we’re in a BlockContext instead of a MethodContext, but it’s still in the family.

s = ctx.sender
#=> #<BlockContext:0xfe main#irb_binding (irb):1>

s.class.ancestors
#=> [BlockContext, MethodContext, Object, Kernel]

Anything new we need to know about?

s.interesting_methods - ctx.interesting_methods
#=> ["env", "home"]

s.home
#=> #<MethodContext:0x102 main#irb_binding
#     /Users/mat/dev/_rubies/rubinius/rubinius/lib/irb/workspace.rb:1>

s.env
#=> #<BlockEnvironment:0xf8 @initial_ip=0 @last_ip=268435456
#     @post_send=0 @bonus=#<Tuple: true>>

All of this is strangely reminiscent of a stack trace. Before you go collecting all senders to explore the execution stack, let me point you to the convenient context_stack:

ctx.context_stack.length
#=> 27

puts *ctx.context_stack
# Too noisy to output here

puts *ctx.context_stack.map{ |s| s.describe }

#=>
# C#get_mc
# Object#irb_binding {}
# Kernel(IRB::WorkSpace)#eval
# IRB::WorkSpace#evaluate
# IRB::Context#evaluate
# IRB::IrbRubinius#process_statements {}
# IRB::Irb(IRB::IrbRubinius)#signal_status
# IRB::IrbRubinius#process_statements {}
# RubyLex#each_top_level_statement {}
# Kernel(RubyLex)#catch {}
# ThrownValue.register
# Kernel(RubyLex)#catch
# RubyLex#each_top_level_statement
# IRB::IrbRubinius#process_statements
# IRB::Irb(IRB::IrbRubinius)#eval_input
# IRB.start {}
# Kernel(Module)#catch {}
# ThrownValue.register
# Kernel(Module)#catch
# IRB.start
# main.__script__
# CompiledMethod#activate_as_script
# CompiledMethod#as_script
# Compile.single_load
# Compile.unified_load
# Kernel(Object)#require
# Object#__script__
# #=> nil

I’m pretty sure there’s other areas specific to Rubinius that can be explored like that. Please share any insight in the comments.

S-Expressions

Rubinius groks s-expressions out of the box (similar to standard Ruby with ParseTree or ruby_parser. An example).

require 'pp'
pp sx = "
  class C
    def meth(arg)
      arg * 2
    end
  end".to_sexp

#=>
# s(:class,
#  :C,
#  nil,
#  s(:scope,
#   s(:defn,
#    :meth,
#    s(:args, :arg),
#    s(:scope,
#     s(:block, s(:call, s(:lvar, :arg), :*, s(:arglist, s(:fixnum, 2))))))))

sx[0]
#=> :class

sx[3][1][1]
#=> :meth

With something that reminiscent to Lisp, it’s probably better to explore recursively, though.

S-expressions are used by a lot of the Ruby code inspection tools to understand your ugly Ruby code.

Gems

I won’t touch trying out gems for today. There seems to be little issues as the moment. They do install, but I’ve been having problems running them. Please leave a comment if you’ve had success with specific gems.

If you’re curious and want to try playing with gems, Rubygems is already installed.

rbx gem install rails --no-rdoc --no-ri

Pro tip: always skip the documentation when playing with gems Rubinius. The doc takes unusually long to compile.

Run the famous test suite

The spec suite that’s been keeping all Ruby implementations honest was born from the Rubinius project. It’s been split into a separate project a while ago, since it’s now such an important and central piece of the Ruby ecosystem.

Update the specs

Since they are now in a different project, we first have to get the most recent version. Easy stuff:

rake rubyspec:update

rbx in your PATH

Before you run the specs, you need to do one little thing. One of the specs expects a shell call to rbx to start Rubinius (as in, the executable must be in your path).

The simplest way to do that for now is just to temporarily add the directory to your path (right in your console, not in your .bash_profile).

pwd
#=> /path/to/project/rubinius
export PATH=$PATH:/path/to/project/rubinius/bin
rbx -v

Run the specs

rake spec
# Time for another BRAWNDO!

Or rather, time to actually look at some of the specs you’re currently running. If you look in the spec directory, you’ll see that it’s pretty extensive, to say the least. To start with something familiar, navigate to spec/ruby/1.8, in subdirectories core or library. Open up a few of the specs in there and stare at them for a few minutes. Or better, improve a few of them and try them out on MRI, JRuby and of course, Rubinius.

Conclusion

Well, now I’ve tricked you into putting the Rubinius project on your hard drive. And you’re a Ruby developer. What are you waiting for?

I think Rubinius will be an awesome runtime for our Ruby programs. It probably won’t be only Ruby in the close future, but the kernel of Ruby (base classes & stuff) is mostly implemented in Ruby, and the compiler is also implemented in Ruby. This is awesome to help understand the workings of the language and to lower the barrier to contribution. Which is already pretty low.

Rubinius is here to stay and it’s gonna keep rocking.

Footnotes

  1. Examples of details not up to date in the old articles are: the LOC numbers and the base language of the VM (used to be C, now C++).
  2. If the build craps out with a message you understand, cool. Try to install the dependency through apt-get or macports/fink, or search your hard drive to see if the tool’s just not in your PATH. Otherwise, here are a few related pointers.
  3. For a quick discussion about getting local variable’s values out of a CompiledMethod, check the [IRC logs around 19:20][9].

Comments