• 在进入正题之前,先声明一下,这里讨论的是方法调用的结构,而非方法执行的过程。

    我一直觉得了解Ruby内部结构的一个很好的入口点是Ruby的API,讨论方法定义的时候,我们便是从Ruby API起步。同样,这次先来看看方法调用的Ruby API。在方法调用这个问题上,Ruby显得特别的大方,一口气提供了四个API:
    VALUE rb_funcall( VALUE recv, ID id, int argc, ... );
    VALUE rb_funcall2( VALUE recv, ID id, int argc, VALUE *args );
    VALUE rb_funcall3( VALUE recv, ID id, int argc, VALUE *args );
    VALUE rb_apply( VALUE recv, ID name, int argc, VALUE args );

    其中,recv相当于this指针。id指定了调用的方法,因为在Ruby中,字符串和id的一一对应关系,这就相当于用一个字符串指定了一个方法名。之后就是参数了,argc表示参数的个数,args表示实际的参数,这种手法在C编程中太常用了。这四个API的差别也就在参数上。

    第一个和第二个API实际上是一样的,差别是在于第一个API运用了可变长度参数,简化了代码编写。第三个API不能调用private方法。第四个API允许以Ruby的数组作为参数。

    Ruby的方法调用分了两个层次,这里的列出的API是最上面的一个层次,作为接口提供出来,而真正的调用过程是由rb_call完成,下面是rb_funcall2的代码:
    VALUE
    rb_funcall2(VALUE recv, ID mid, int argc, const VALUE *argv)
    {
        return rb_call(CLASS_OF(recv), recv, mid, argc, argv, NOEX_NOSUPER);
    }

    有了这样的区分就可以背着广大的API使用者,偷偷的在内部提供更细的控制,事实上,也是这样,rb_call在Ruby解释器的内部还是得到了广泛的采用。下面是rb_call的代码:

    static VALUE
    rb_call(klass, recv, mid, argc, argv, scope)
        VALUE klass, recv;
        ID    mid;
        int argc;   /* OK */
        const VALUE *argv;  /* OK */
        int scope;
    {
        NODE  *body;  /* OK */
        int    noex;
        ID     id = mid;
        struct cache_entry *ent;

        if (!klass) {
            rb_raise(rb_eNotImpError, "method `%s' called on terminated object (0x%lx)",
                rb_id2name(mid), recv);
        }
        /* is it in the method cache? */
        ent = cache + EXPR1(klass, mid);
        if (ent->mid == mid && ent->klass == klass) {
            if (!ent->method)
                 return method_missing(recv, mid, argc, argv, scope==2?CSTAT_VCALL:0);
            klass = ent->origin;
            id    = ent->mid0;
            noex  = ent->noex;
            body  = ent->method;
        }
        else if ((body = rb_get_method_body(&klass, &id, &noex)) == 0) {
            if (scope == 3) {
                return method_missing(recv, mid, argc, argv, CSTAT_SUPER);
            }
            return method_missing(recv, mid, argc, argv, scope==2?CSTAT_VCALL:0);
        }

        if (mid != missing && scope == 0) {
            /* receiver specified form for private method */
            if (noex & NOEX_PRIVATE)
                return method_missing(recv, mid, argc, argv, CSTAT_PRIV);

            /* self must be kind of a specified form for protected method */
            if (noex & NOEX_PROTECTED) {
                VALUE defined_class = klass;

                if (TYPE(defined_class) == T_ICLASS) {
                    defined_class = RBASIC(defined_class)->klass;
                }
                if (!rb_obj_is_kind_of(ruby_frame->self, rb_class_real(defined_class)))
                    return method_missing(recv, mid, argc, argv, CSTAT_PROT);
                }
        }

        return rb_call0(klass, recv, mid, id, argc, argv, body, noex);
    }
    (eval.c)

    抛开基本的检查代码,这段代码做了几件事:
    1 查找方法
    2 校验方法
    3 执行方法

    先来看看查找方法,谈到类的方法时,我们知道,方法在类中是以hash表的方式存放,所以,一般会有一个查表的过程,查找的代码在那篇blog中已经展示过了。

    在这里,查找代码首先做的并不是在hash表中查找,而是在一个方法缓存中查找:
    ent = cache + EXPR1(klass, mid);

    通常讨论hash表的时候,我们会把它看作是一个O(1)的操作,相比于很多高复杂度的算法来说,它是可以接受的。但是,对于方法调用这个在Ruby中很常见的操作来说,需要在O(1)的系数一级进行优化以提高整体的性能。如果一个方法如果定义在超类中,那么每次调用都执行循环来找,最差情况就是定义在类层次的根部,除此之外,hash本身还要有不少的运算负担。这里采用了方法缓存,只进行简单的运算,就可以把已经调用过的方法找到,这使得再次调用方法的负担大幅度减小。缓存技术是一项伟大的技术,在计算机的各个领域得到了广泛的应用,比如CPU的多级缓存,比如OS中虚拟内存。

    一个朋友实现一个类似的结构,不过,没有实现缓存,而且搜索方法没有采用循环,而是采用了递归。另外一个朋友做了一个测试,代码是Fibonacci数列,而且采用的是递归的方式,也就是可以导致复杂度非常高的那种:
        F(n) = F(n-1) + F(n-2)
    结果稍微大一点的数就要求用上很长一段时间,这是一个不错的例子,各个方面不是最优的选择。

    如果找到了这个方法,就会进行一些校验,看看调用端是否具备合法调用方法的资格。对比一下前面几个API的实现不难发现,这里提供了更细的设置,这些参数会在Ruby求值的过程中用到,所以,前面说前面说它们背着广大的API使用者。

    至于方法的执行过程,开篇就说过了,这里不讨论了。

  • 在C/C++中,函数虽然不像functional语言中一样,拥有first class的地位,但函数指针的存在,还是让它在一些表达上显得很简洁。在C层次上,定义一个Ruby方法的API是这样的:
    void rb_define_method( VALUE classmod, char *name, VALUE(*func)(), int argc )

    它为类或模块(classmod)定义了一个实例方法,在Ruby层次上的参数名为name,参数个数为argc。通过这个函数,一个Ruby层次上的函数就和C层次上的函数连接到了一起,如同我在《管窥Ruby——Allocator》中谈到两个层次对象之间的关系一样。

    下面是一个例子,它定义了Array#length方法:
    rb_define_method(rb_cArray, "length", rb_ary_length, 0);
    (array.c)

    如果把同样的问题映射到Java之中,又会是怎样的情形呢?

    我们知道,在Java中没有指针的概念,所以,我们不可能像C语言这样,直接用函数(实际上是函数指针)作为参数。通常的替代方案是使用接口(或是基类)作为参数。类似于上面的功能,我们可能会这样定义接口:
    void defineMethod(RubyClass klassmod, String name, RubyMethod method, int argc);

    当我们只有一个方法要实现的时候,或许这种方式与C的方式看不出来什么差别。当我们有铺天盖地的方法要实现,这种定义的差异便显现出来。

    通常,我们会把相关的内容实现在一个类里:
    class K {
    void methodA() {}
    void methodB() {}
    void methodC() {}
    }

    但这样的实现并不能让它和前面的定义友好协作,因为前面那个定义调用一次,只能加入一个方法。
    defineMethod(RubyKClass, "methodA", ???, 0);

    若要把所有的方法都加入其中,仅仅让K继承自RubyMethod是不够的,因为它只有一个接口,只能让一个方法起作用,当然,如果打算在接口内部来做dispatch,那又是另外的故事了。

    一种方法是加入中间层,让中间层负责完成这个任务。
    class MethodAWrapper implements RubyMethod {
    ... run(...) {
    K k = ...
    k.methodA();
    }
    }

    这样定义就变成了这样
    defineMethod(RubyKClass, "methodA", MethodAWrapper, 0);
    defineMethod(RubyKClass, "methodB", MethodBWrapper, 0);
    defineMethod(RubyKClass, "methodC", MethodCWrapper, 0);

    这样的解决方案也带来了问题:方法的Wrapper如何来写。因为Wrapper是真正注入进去的内容,所以,有些实现即便K中没有对应实现,只要其接口可以完成,我们一样可以通过Wrapper实现,换句话说,它起到了一个辅助实现的作用,也就是Java编程中常用的Util。所以,有时候,我们会疑问这个方法放在哪实现更好,是方法中还是Wrapper中?再者,虽然Wrapper通常是一个很薄的层次,但毕竟要写,所以,也是一道繁琐的工序。

    事实上,JRuby在实现的时候,也有这样的问题。下面是一个定义方法底层的接口:
    public void addMethod(String name, ICallable method) ;
    (RubyModule.java)

    这里的ICallable起到的作用与我们前面例子中的RubyMethod是一样的。这个函数是这样调用的:
    public void defineMethod(String name, Arity arity, String java_name) {
    ...
    addMethod(name, new ReflectedMethod(this, builtinClass, java_name,
    arity, visibility));
    }
    (AbstractMetaClass.java)

    ReflectedMethod,看名字就知道,它利用Java中的Reflection。实际上,它正是利用了Reflection对Java的方法进行封装,将Wrapper和方法合二为一,解决我们前面提到的两个问题。

    或许,从开发的角度来说,这种解决方案有一定的优越性,因为它大大的减少了编码量,但客观的事实是,Java的Reflection无论如何在性能上也无法达到普通函数调用的水平,而且,在JRuby中,这种函数会大量的调用,所以,这也是JRuby性能为人诟病的一个根本原因之一。

    出现这种左右两难的原因是Java语言本身的限制,因为在Java中如果不用Reflection的话,我们无法单独操作方法。显然后来者在这个问题上做得比较好,如果使用C#,delegate会让这个问题的解决方案优雅许多。显然,在继续讨论下去的话,就成了《程序设计语言的表达》的延续。

  • Allocator,熟悉编程的人一看到这个名字,自然就会把它与内存分配联系起来,再近一步,它通常是与对象初始化联系到一起。在《对象的生命》中,我讨论过对象初始化的过程,将它分为“分配内存”和“初始化对象”两个动作,Allocator主要就是承担分配内存的工作。

    如果以Java之类可以合分配与初始化于一体的语言来实现Ruby,那是不是意味着可以将Allocator并入到初始化过程中去,从而将它从Ruby中消灭呢?我一开始确实这样想,但无情的现实总会恰如其分的站出来教育人。

    与Ruby源码打交道,有一点需要谨记,我们面对的是两个层次的东西,一个是Ruby层次,一个是C层次。比如,在之前探讨过的关于Class的内容,属于定义在C层次上,而在Ruby代码中写的类都是定义在Ruby层次上。初读代码时,因为把二者混淆,所以,很长一段时间内,我都被它们之间恼人的关系说困扰。实际上,它们之间有如角色和演员,我们在“台上”看到的对象——Ruby对象,后面有一个真正的“表演者”——C对象。

    如何将C层次的对象和Ruby层次的对象结合起来呢?

    我们来看一下如何创建一个对象。我们知道,Ruby中也有“一切皆对象”的口号,类也不例外,所以在Ruby代码中创建对象,不像很多语言中那样有一个专门的new操作符,而是调用了类对象的一个new方法:
        MyClass.new

    这个new方法在源码中是这样定义的:
    rb_define_method(rb_cClass, "new", -1);
    (object.c)

    主要的内容实现在rb_class_new_instance:
    VALUE
    rb_class_new_instance(argc, argv, klass)
        int argc;
        VALUE *argv;
        VALUE klass;
    {
        VALUE obj;

        obj = rb_obj_alloc(klass);
        rb_obj_call_init(obj, argc, argv);

        return obj;
    }
    (object.c)

    在这个对象的方法中,我们可以清楚看见,“分配内存”(rb_obj_alloc)和“初始化对象”(rb_obj_call_init)两个过程。可想而知,rb_obj_call_init完成的是调用Ruby中的对象初始化方法——“initialize”,这里不再赘述。我们关注一下rb_obj_alloc。

    这个方法实际上就是调用前面所提到的Allocator,由它完成对象的内存分配。如果不定义自己的Allocator,那么按照对象继承的关系,我们用到就是超类的方法,在Ruby中,最底层的Allocator定义在Object类中:
    rb_define_alloc_func(rb_cObject, rb_class_allocate_instance);
    (object.c)

    其中rb_class_allocate_instance的实现如下:
    static VALUE
    rb_class_allocate_instance(klass)
        VALUE klass;
    {
        NEWOBJ(obj, struct RObject);
        OBJSETUP(obj, klass, T_OBJECT);
        return (VALUE)obj;
    }
    (object.c)

    NEWOBJ是一个内存分配的过程,OBJSETUP设置了这个对象的几个字段。

    一般在Ruby层次上定义的类都不会有自己的Allocator,这段代码告诉我们,对于这样类的对象,在底层对应的就是一个RObject的结构,不同类对象之间的差异仅仅是klass字段。这也是我在《管窥Ruby——类的变量》中解释rb_ivar_set中真正的主干是T_OBJECT的原因。

    其实,至此所有的故事都可以结束。但是,Ruby中还有许多定义在C层次上的类,找个堂皇点的理由,使用C层次上的类可以比这种用法效率上稍微有些优势,因为RObject的所有实例变量都放到st_table中,显然这种方式没有直接访问来得快:
    struct RObject {
        struct RBasic basic;
        struct st_table *iv_tbl;
    };
    (ruby.h)

    既然有定义C层次类的需求,那怎么才能把它加入到Ruby的体系之中呢?回头看看rb_class_new_instance,实际上,rb_obj_alloc和rb_obj_call_init这两个函数除了承担前面所说的工作之外,还有其它的角色,按照前面的讨论,要说的已经很清楚了,rb_obj_alloc用来初始化C层次的类,rb_obj_call_init用来初始化Ruby层次的类。

    如果需要加入C层次的类,我们要做的就是加入一个Allocator,Ruby提供了这样一个函数:
    void rb_define_alloc_func( VALUE classmod, VALUE(*func)() );

    这里classmod表示一个Ruby层次上的类,而Allocator中定义一个C层次上的类,通过这段函数,我们就将它们关联了起来:

    下面是一个例子,来自Array的实现:
    rb_define_alloc_func(rb_cArray, ary_alloc);
    (array.c)

    ary_alloc的实现如下:
    static VALUE
    ary_alloc(klass)
        VALUE klass;
    {
        NEWOBJ(ary, struct RArray);
        OBJSETUP(ary, klass, T_ARRAY);

        ary->len = 0;
        ary->ptr = 0;
        ary->aux.capa = 0;

        return (VALUE)ary;
    }
    (array.c)

    回到最初的问题上,想让Allocator退出历史舞台还真不是一件容易的事情,因为它在分配内存之外,还扮演了初始化底层类的角色。如果采用相同的结构,即便用Java这样的语言可以避开内存分配,但底层类初始化的过程是无法避开的。当然,在这种情况下,还叫Allocator,名称上会有些歧义。

  • 关于一个RClass结构,我已经写了几篇blog来讨论其中的内容,没办法,谁让Ruby是一种面向对象的程序设计语言呢!类是核心的概念之一,而且很多看似魔术的东西,就是靠这些基础的东西支撑起来的。前面的讨论,我们已经见识过方法变量了,这里就来看一下RClass中的另外一个字段:super。

    回忆一下面向对象的特点:封装、继承、多态。super的出现为继承打下了基础,而多态是要靠继承来发挥作用的,所以,super这样一个字段将原本孤立的类联系到了一起,一个庞大的体系得以运转起来。

    从这里的定义,我们便不难发现,Ruby是一个单一继承的体系,因为它只能有一个super。

    在Ruby中,super并不像看起来那么简单,因为在实际的处理中,有些功能就是通过这个字段做一些手脚来支持的。下面就来看看以包含模块是如何实现的。

    前面的讨论已经提到了,Ruby是一个单一继承的体系,通过mixin,它可以达到类似于多重继承的功能,而mixin实际上就是包含了一个模块。下面这段代码便展示了如何实现一个类包含一个模块的方法:

    void
    rb_include_module(klass, module)
        VALUE klass, module;
    {
        VALUE p, c;
        int changed = 0;

        rb_frozen_class_p(klass);
        if (!OBJ_TAINTED(klass)) {
            rb_secure(4);
        }
       
        if (NIL_P(module)) return;
        if (klass == module) return;

        if (TYPE(module) != T_MODULE) {
            Check_Type(module, T_MODULE);
        }

        OBJ_INFECT(klass, module);
        c = klass;
        while (module) {
            int superclass_seen = Qfalse;

            if (RCLASS(klass)->m_tbl == RCLASS(module)->m_tbl)
                rb_raise(rb_eArgError, "cyclic include detected");
            /* ignore if the module included already in superclasses */
            for (p = RCLASS(klass)->super; p; p = RCLASS(p)->super) {
                switch (BUILTIN_TYPE(p)) {
                    case T_ICLASS:
                        if (RCLASS(p)->m_tbl == RCLASS(module)->m_tbl) {
                            if (!superclass_seen) {
                                c = p; /* move insertion point */
                            }
                           goto skip;
                        }
                        break;
                    case T_CLASS:
                        superclass_seen = Qtrue;
                        break;
                }
            }
            c = RCLASS(c)->super = include_class_new(module, RCLASS(c)->super);
            changed = 1;
        skip:
            module = RCLASS(module)->super;
        }
        if (changed) rb_clear_cache();
    }
    (class.c)

    前面部分的代码大多用于检查,比如参数是否合法,防止重复包含等等,简化一下,这段代码主要就是一句话:
    c = RCLASS(c)->super = include_class_new(module, RCLASS(c)->super);

    创建一个类,把用来包含模块类的super指向它,我们也看到了在创建这个类的函数使用了原有的super作为参数,不难猜测,通过这样一个语句,我们便把一个创建出来的新类插入到现有的类层次结构之中。下面便是这个函数的实现:
    static VALUE
    include_class_new(module, super)
        VALUE module, super;
    {
        NEWOBJ(klass, struct RClass);
        OBJSETUP(klass, rb_cClass, T_ICLASS);

        if (BUILTIN_TYPE(module) == T_ICLASS) {
            module = RBASIC(module)->klass;
        }
        if (!RCLASS(module)->iv_tbl) {
            RCLASS(module)->iv_tbl = st_init_numtable();
        }
        klass->iv_tbl = RCLASS(module)->iv_tbl;
        klass->m_tbl = RCLASS(module)->m_tbl;
        klass->super = super;
        if (TYPE(module) == T_ICLASS) {
            RBASIC(klass)->klass = RBASIC(module)->klass;
        }
        else {
            RBASIC(klass)->klass = module;
        }
        OBJ_INFECT(klass, module);
        OBJ_INFECT(klass, super);

        return (VALUE)klass;
    }
    (class.c)

    这段代码主要完成了一些赋值,包括我们前面所说的,将类连接到类层次结构中。稍微需要提一下的是,这里对iv_tbl和m_tbl采用了直接赋值,而这两个字段都是指针,这样的赋值方式让两个不同的对象指向了同一个表,也就是说,如果module新增了方法,class同样可以拥有。

    或许,我们会有疑问,这样的实现修改了类的继承关系,岂不会有很大的影响。这完全不必担心。我们看到,在这里给这个新类附的类型是T_ICLASS,而不是通常类说具有的T_CLASS。在Ruby的类层次结构中,存在一些“实际中不存在”的类,这里的包含类就是其中一种,这些类在Ruby的层次上根本看不见。当我们需要找一个类的超类时,这些类会被跳过。把它们放到类的层次结构中,查找方法时,我们不必做特殊的处理。

  • UPDATE
    这篇blog的叙述存在一些偏差,所以,又重新写过:《管窥Ruby——类的变量(更新版)

    变量和方法是面向对象难以割舍的两个重要组成部分。在《管窥Ruby——类的方法》中,我们谈到方法,沿着这条路继续,我们再来看看类中的变量。

    开始之前,我们还是要再次回顾RClass的定义:
    struct RClass {
      struct RBasic basic;
      struct st_table *iv_tbl;
      struct st_table *m_tbl;
      VALUE super;
    };
    (ruby.h)

    如果你看过《管窥Ruby——类的方法》,了解了方法存储方式,变量的存储方式便也一目了然了,同样的st_table,意味着同样的处理方式。

    不得不承认的一点是,在讨论类的方法时,我故意忽略了一个事实:方法分为类的方法和实例的方法两种。如果对其它语言实现有些许了解,我们知道,这两种方法差别仅仅是this(C++或Java的说法),到了底层时,这个差别可以视为无物,可以统一存放。关于这点,有兴趣可以参考一下《深入Java虚拟机》,它的第五章讲解了虚拟机内方法的表现形式。

    但是,当我们遇到变量时,同样的问题,我们便不能忽略了:类的变量和实例变量是无法统一管理的,因为类的变量只有唯一的一份,而实例变量则是每个实例都有一份。代码是说明问题的最好方式,下面这段代码说明了如何设置一个实例变量:

    VALUE
    rb_ivar_set(obj, id, val)
    VALUE obj;
    ID id;
    VALUE val;
    {
      if (!OBJ_TAINTED(obj) && rb_safe_level() >= 4)
        rb_raise(rb_eSecurityError, "Insecure: can't modify instance variable");
      if (OBJ_FROZEN(obj)) rb_error_frozen("object");
      switch (TYPE(obj)) {
        case T_OBJECT:
        case T_CLASS:
        case T_MODULE:
          if (!ROBJECT(obj)->iv_tbl) ROBJECT(obj)->iv_tbl = st_init_numtable();
          st_insert(ROBJECT(obj)->iv_tbl, id, val);
          break;
        default:
          generic_ivar_set(obj, id, val);
          break;
      }
      return val;
    }
    (variable.c)

    抛开前面那些复杂的东西,直接来看switch语句后面的内容。在这里,我们看到只有在特定的情况下,才会向iv_tbl中写入内容,其它情况则交给了generic_ivar_set处理。观察一下这些"特定"情况,我们不难发现,实际上,iv_tbl中存放的是类的变量,而非实例的变量。所以,这个名为“实例变量表(instance value table)”的变量,多少有些名不符实。

    类的变量找到了落脚点,那实例的变量放在了哪里。别忘了,后面还有一个generic_ivar_set。下面是generic_ivar_set的实现:

    static void
    generic_ivar_set(obj, id, val)
    VALUE obj;
    ID id;
    VALUE val;
    {
      st_table *tbl;

      if (rb_special_const_p(obj)) {
        special_generic_ivar = 1;
      }
      if (!generic_iv_tbl) {
        generic_iv_tbl = st_init_numtable();
      }

      if (!st_lookup(generic_iv_tbl, obj, (st_data_t *)&tbl)) {
        FL_SET(obj, FL_EXIVAR);
        tbl = st_init_numtable();
        st_add_direct(generic_iv_tbl, obj, (st_data_t)tbl);
        st_add_direct(tbl, id, val);
        return;
      }
      st_insert(tbl, id, val);
    }
    (variable.c)

    同样忽略一些非主干的部分,我们看到,这段代码先在一个generic_iv_tbl中进行查找,用作查找键值的是对象实例(obj),而目标同样是一个st_table。得到这个表之后,利用变量名做键值将值插入到表中。

    由此,我们便不难分析出实例变量的存储方式。存在一个全局的实例变量表,所有实例都在其中拥有一席之地,而实例变量存储在实例对应的表中。Ruby就是用了这样一个二级结构,解决了实例变量存储的问题。

    UPDATE
    事实上,上面的说法并不完全正确,rb_ivar_set中真正的主干是T_OBJECT,因为在Ruby层次上定义的类,走的都是这条路,这一点可以从初始化的代码中看到。

    另外,上面没有提到类变量如何处理,实际上,在Ruby中,类也是对象,类的变量实际上就是类对象的实例变量。