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.
Example:
The following Ruby code:
class Foo
def initialize
@bar = 'BAR!'
end
def bar
@bar
end
def baz
'BAZ!'
end
end
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
end
end
Breaking it down we see that:
- it calls
#allocate
in order to setup a raw object (in C it will also allocate the necessary memory) - it forwards all arguments and block to
#initialize
- it returns the object
Here’s how it looks in Opal (the code inside %x{}
is JavaScript):
class Class
def new(*args, &block)
%x{
var obj = self.$allocate();
obj.$initialize.$$p = block;
obj.$initialize.apply(obj, args);
return obj;
}
end
end
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
%x{
var obj = new self.$$alloc;
obj.$$id = Opal.uid();
return obj;
}
end
end
Let’s break it down by line, but this time we’ll go in reverse order:
- The last line is the easiest one in which the code just returns the object:
return obj
. - 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
. - 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.