Opal under a microscope: Object creation in Ruby and JavaScript

In this post we’ll learn how Ruby objects are mapped in JavaScript-land by the Opal compiler, how to call methods on them and how object instantiation works for both Ruby and JavaScript.

The basics: constants, methods and instance variables

The rule by which Ruby objects are compiled into JavaScript by Opal is quite simple. Constants are registered with their regular name under the window.Opal (i.e. the Opal property on the JavaScript window object). Methods are mapped to properties prefixed by a $ (dollar sign). Instance variables are just regular properties.


The following Ruby code:

class Foo
  def initialize
    @bar = 'BAR!'

  def bar

  def baz

can be used from JavaScript in this way:

var Foo = Opal.Foo; // Constant lookup
obj = Foo.$new();   // calling method on class Foo

obj.bar             // accessing @bar      => 'BAR!
obj.$bar();         // another method call => 'BAR!'

obj.baz;            // a js property => undefined
obj.$baz();         // another method call => 'BAZ!'

As a Ruby developer you may be surprised that obj.baz returns undefined and not nil (actually window.Opal.nil) as that’s the value that you would expect to find while reading an instance variable for the first time.

What happens is that the Opal compiler will do just that, as long as you access those instance variables from Ruby code. In fact it statically analyzes the class’ code and pre-initializes to nil any instance variable for which it can find a reference.

Now that we have the basics let’s go further and put Foo.new under the microscope for both Opal and CRuby.

The birth of an object in 3 steps

By looking in detail how object instantiation is done in JavaScript by the Opal compiled code we’ll have a chance to learn how it actually works in CRuby.

The implementation of .new

At the cost of oversimplifying here’s the rough implementation of the #new method available to all classes in Ruby:

class Class
  def new(*args, &block)
    obj = allocate # allocates the memory in CRuby
    obj.send(:initialize, *args, &block) # forwards all arguments to #initialize
    return obj

Breaking it down we see that:

  1. it calls #allocate in order to setup a raw object (in C it will also allocate the necessary memory)
  2. it forwards all arguments and block to #initialize
  3. it returns the object

Here’s how it looks in Opal (the code inside %x{} is JavaScript):

class Class
  def new(*args, &block)
      var obj = self.$allocate();
      obj.$initialize.$$p = block;
      obj.$initialize.apply(obj, args);
      return obj;

(Class#new source)

As you can see the the only notable difference is in how the block is passed. Opal will store any block call on the method itself under the $$p property. This way blocks can’t be confused with regular arguments. Apart from that it’s clear that it’s fundamentally the same code.

Uncovering Class#allocate

The curious reader at this point is wondering what the allocate method does in JavaScript because surely it can’t manage memory. Let’s see the implementation:

class Class
  def allocate
      var obj = new self.$$alloc;
      obj.$$id = Opal.uid();
      return obj;

(Class#allocate source)

Let’s break it down by line, but this time we’ll go in reverse order:

  1. The last line is the easiest one in which the code just returns the object: return obj.
  2. The middle one is still quite clear and seems to just assign a unique identifier to the object. That value will be the one returned by #object_id.
  3. The first and most important one is where stuff actually happens. The JavaScript new keyword is used to create an object whose constructor seem to be stored in $$alloc.

For those not very familiar with JavaScript I’ll show how objects are usually created:

// this function acts as the MyClass constructor
function MyClass() { this.foo = 'bar'; }

var obj = new MyClass;
obj.foo         // => 'bar'
obj.constructor // => MyClass

The awesome thing to me is that Opal manages to have an implementation that is really idiomatic in both Ruby and JavaScript.