Advent of YARV: Part 16 - Defining methods

This blog series is about how the CRuby virtual machine works. If you’re new to the series, I recommend starting from the beginning. This post is about defining methods.

In the previous post we looked at the defineclass instruction, which defined a class, singleton class, or module. It then pushed a frame onto the frame stack and executed the instructions within it within the context of the new class, singleton class, or module.

In today’s post we’re going to look at definemethod and definesmethod, both instructions for defining methods on classes. While these instructions look very similar to defineclass on the surface, they function quite differently. Where defineclass immediately executes the instructions in the context of the new class, definemethod and definesmethod associate their given instruction sequence with the method but do not execute them until those methods are called.

Both definemethod and definesmethod have two operands. The first is an ID corresponding to the name of the method being defined. The second is the instruction sequence that corresponds to the body of the method. Neither instruction push anything onto the stack. definemethod pops nothing off of it either. definesmethod pops a single value off of the stack, which is the object on which the method is being defined.

The instruction definitions in insns.def are as follows:

DEFINE_INSN
definemethod
(ID id, ISEQ iseq)
()
()
{
    vm_define_method(ec, Qnil, id, (VALUE)iseq, FALSE);
}

DEFINE_INSN
definesmethod
(ID id, ISEQ iseq)
(VALUE obj)
()
{
    vm_define_method(ec, obj, id, (VALUE)iseq, TRUE);
}

You can see they’re mostly delegating their work to vm_define_method, which is defined in vm_insnhelper.c:

static void
vm_define_method(const rb_execution_context_t *ec, VALUE obj, ID id, VALUE iseqval, int is_singleton)
{
    VALUE klass;
    rb_method_visibility_t visi;
    rb_cref_t *cref = vm_ec_cref(ec);

    if (is_singleton) {
        klass = rb_singleton_class(obj);
        visi = METHOD_VISI_PUBLIC;
    }
    else {
        klass = CREF_CLASS_FOR_DEFINITION(cref);
        visi = vm_scope_visibility_get(ec);
    }

    rb_add_method_iseq(klass, id, (const rb_iseq_t *)iseqval, cref, visi);
}

I’ve simplified this function a bit but the essence is still there. This function basically boils down to gathering up some more information and eventually calling rb_add_method_iseq. You can see it’s searching for the class on which to define the method and defining the visibility of the method (methods defined on a singleton class def self.foo are always public).

rb_add_method_iseq gathers up even more information and then calls rb_add_method which does this again and then finally calls rb_method_entry_make. This function is where the method is actually defined and added into the method table.

That’s really all there is to it. definemethod and definesmethod both associate a given instruction sequence with a method entry in a method table. The instructions contained within those instruction sequences are executed when the method is called. Let’s look at a couple of disassembly examples.

Defining methods

Let’s define a simple method named foo that always returns 1:

def foo
  1
end

In the disassembly we’ll see:

== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,3)> (catch: false)
0000 definemethod                           :foo, foo                 (   1)[Li]
0003 putobject                              :foo
0005 leave

== disasm: #<ISeq:foo@test.rb:1 (1,0)-(3,3)> (catch: false)
0000 putobject_INT2FIX_1_                                             (   2)[LiCa]
0001 leave                                                            (   3)[Re]

You can see definemethod is the first instruction compiled. It accepts the name of the method being defined (:foo in this case) and the instruction sequence for the method body (an instruction sequence named foo in this case). That instruction sequence is then disassembled and we can see it contains the instructions for the method body. After the method is defined, putobject is called with the name of the method (:foo in this case), because using def foo always returns :foo.

Defining singleton methods

Now let’s define a singleton method named foo that also always returns 1:

def Object.foo
  1
end

You can see here we’re defining a singleton method on the value referred to by the Object constant. In the disassembly we’ll see:

== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,3)> (catch: false)
0000 opt_getconstant_path                   <ic:0 Object>             (   1)[Li]
0002 definesmethod                          :foo, foo
0005 putobject                              :foo
0007 leave

== disasm: #<ISeq:foo@test.rb:1 (1,0)-(3,3)> (catch: false)
0000 putobject_INT2FIX_1_                                             (   2)[LiCa]
0001 leave                                                            (   3)[Re]

The opt_getconstant_path instruction will push the value referred to by the Object constant onto the stack. The definesmethod instruction pops that value off of the stack and uses it to define the singleton method. The rest of the disassembly is the same as the previous example.

Wrapping up

In this post we looked at both the definemethod and definesmethod instructions. They are used to define methods on classes and singleton classes. A couple of things to remember from this post:

In this post we looked at defining methods, but purposefully left out the fact that methods can have arguments. In the next post we’ll take a look at all of the different kinds of arguments in Ruby and how YARV implements them.

← Back to home