• 在Ruby On Rails中文社区的的Ruby版上,karl问了一个有趣的问题,如何根据类名生成一个类的实例。
    链接在这里:
    http://www.railscn.com/about2289.html

    下面是一个Java语言的实现:
    public static Object createObject(String className)
        throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Class clazz = Class.forName(className);
        return clazz.newInstance();
    }

    在Ruby中怎么做呢?karl自己给出了第一种做法:
    str = 'String'
    eval "obj=#{str}.new"

    这里利用了Ruby的动态语言的特性,在代码中执行代码。不过,照着尝试了一下,我有了一个疑问,如何使用生成的对象,也就是,我们如何利用这里的obj呢?

    按照karl提供的方式,编写出这样的代码:
    str = 'String'
    eval "obj = #{str}.new"
    p obj

    这里故意使用了p方法,因为对于一个空串,它可以打出"",而不是无声无息,便于查看。

    这样的代码是无法运行的,异常如下:
    undefined local variable or method `obj' for main:Object (NameError)

    然而,这段代码在irb中执行没有问题,这是irb和Ruby不兼容的一个地方。在《Programming Ruby》第二版中的第15章,谈及irb限制的时候,提到了这一点。

    eval执行会有一个值,也就是它最后执行的语句的值,对应到这里就是obj的值。根据这个特性,改写得到了第二种做法,代码如下:
    str = 'Array'
    obj = eval "obj = #{str}.new"
    p obj

    显然eval里的obj没有发挥什么作用,再修改一下:
    str = 'Array'
    obj = eval "#{str}.new"
    p obj

    这样,我们就根据字符串创建出一个可用的对象。事实上,第一种方式也是可用的,不过,要obj在前面已经存在,只不过要稍做修改:
    obj = nil
    eval "obj = String.new"
    p obj

    qiezi的回帖给出了一个我认为最漂亮的一种做法:
    obj = eval(str).new

    同样是利用eval,不同的是,这里是直接执行类名,结果是得到一个类对象。调用类的new方法就创建实例。这段代码看上去类似于一个普通的函数调用,很简洁。

    对应到前面的Java函数,Ruby的实现就可以这样写:
    def create_obj(class_name)
        eval(class_name).new
    end

    UPDATE
    故事有了新的进展,njmzhang提供了另外的方法,按照他的说法,这几乎是一种标准用法:
    c = Object.const_get("Array")
    s = c.new

    这里利用到了一个事实,所有的类对象在Ruby中都是一个常量,所以,可以根据它的名字把这个常量提取出来。

    除此之外,njmzhang还给出了另一种利用扩展实现的方法:
    c = Class.by_name("Array")

    这里用到的扩展在
    http://extensions.rubyforge.org/

    从方法的命名上来说,它比上一种方法更恰当一些。

    算上这两种实现,我最喜欢的仍然是qiezi给出的那种方法。

  • 2006-09-24

    扩展Ruby

    这个例子来自《Programming Ruby》(第二版)的第21章《Extending Ruby》,实现的功能类似于下面这段Ruby代码:

    class MyTest
      def initialize
        @arr = Array.new
      end
      def add(obj)
        @arr.push(obj)
      end
    end

    C实现如下:
    #include "ruby.h"

    static int id_push;

    static VALUE t_init(VALUE self)
    {
      VALUE arr;
      arr = rb_ary_new();
      rb_iv_set(self, "@arr", arr);
      return self;
    }

    static VALUE t_add(VALUE self, VALUE obj)
    {
      VALUE arr;
      arr = rb_iv_get(self, "@arr");
      rb_funcall(arr, id_push, 1, obj);
      return arr;
    }

    VALUE cTest;

    void Init_my_test() {
      cTest = rb_define_class("MyTest", rb_cObject);
      rb_define_method(cTest, "initialize", t_init, 0);
      rb_define_method(cTest, "add", t_add, 1);
      id_push = rb_intern("push");
    }

    扩展Ruby时,所有的表演都是从最后一个函数Init_my_test开始的,它是Ruby核心部分提供给C的接口。在很多设计中,我们会通过接口进行回调,这样,我们就可以代码中调用“未来”的模块。这里呈现的就是这样一个接口,通过它,我们就可以对Ruby进行扩展。它的命名规则是Init_name。注意,这里的名字并非是类的名字,而是要生成的文件的名字,也是我们在编写程序中require的那个名字,下面还会提到。

    通过前面的讲解,我们便不难理解加载这个扩展时的动作,当require出现的时候,调用Init_name这个方法,这样,这个文件中定义的内容便注册到内核中,程序的其它部分便可以使用这些内容了。多次require的时候,多次调用Init_name方法是一种奢侈,这样,会想到cache,不想了,有些远。

    我常常把与程序的交互分为两种:一种是程序库,也就是我们编写的程序对其进行调用,完成特定的功能;另一种是框架,也就是我们编写的程序由它调用。通常所说的框架一般包括这两个部分。对比Ruby中的实现,这里的Init_my_test就是属于框架的部分,也就是我们编写出来由Ruby的核心部分进行调用,让这段代码有了运行的机会。再来看这个函数中的实现,从函数名我们就可以看出,这里定义了一个名为“MyTest”的类,之后又为这个类定义两个方法,它们分别是“initialize”方法,其实现为t_init,和“add”方法,其实现为t_add。通过框架和程序库的组合,我们就可以与Ruby进行交互,既让自己的代码有了参与的机会,也可以让自己完成想做的事情。至于最后的id_push,我们可以认为它就是“push”字符串,因为在Ruby的实现中,字符串总是和一个ID一一对应。

    t_init和t_add两个函数的实现很简单,但却为我们演示了很多的东西。在Ruby层次上未曾示人的this指针露出了真容(self),我们有了操作实例变量的机会(rb_iv_set/rb_iv_get),也知道了在这个层次上调用函数的方法(rb_funcall)。

    在这里看到的内容,通常会分C和Ruby两个层次上的内容,比如,在Ruby层次上,我们定义给程序定义了名为“initialize”的方法,而它在C层次上的对应是t_init。如果有机会看Ruby的实现,这样的代码会有很多,基本上,引号里面的名字都属于Ruby,比如代码中出现的"@arr",具有明显的Ruby特征。

    有了代码,接下来就是构建的工作。编写Makefile是一件繁琐的工作,所以,Ruby为我们提供了一个更简洁的方式。创建一个Ruby文件,注意,create_makefile的参数就是要生成文件的名字,也就是前面Init_name中的name。

    require 'mkmf'
    create_makefile("my_test")

    执行这个Ruby程序,便会创建出一个Makefile,剩下的工作就很简单了:make。

    顺便说一下,小规模的Makefile编写起来还比较容易,但规模大了,着实让人难以忍受,幸好有许多自动化的工具,比如在*nix下常用的autoconf,这里的也算是一种。通常这种自动化工作生成的Makefile具有相当完整的功能,比如清理、安装之类的功能等。自动化虽好,用起来才好,一些在*nix下工作朋友还停留在手工编写Makefile的阶段,有耐性。

    做完事,该检查一下,下面是一个测试程序,这里用到了Ruby的单元测试框架。
    require 'my_test'
    require 'test/unit'

    class TestTest < Test::Unit::TestCase
        def test_test
            t = MyTest.new
            assert_equal(Object, MyTest.superclass)
            assert_equal(MyTest, t.class)
            t.add(1)
            t.add(2)
            assert_equal([1,2], t.instance_eval("@arr"))
        end
    end

    前面提到过这个例子的出处,当然,那里也是获得更多信息的更好去处。

  • 在大学里最初接触C++的时候,一个比我高一年级的师兄告诉我,C++就是结构里加入了函数,这便是我对面向对象的最初的认识。那时的我,没有太多的经验,也不知道什么原则。这样一个最简单的印象一直持续到今天,当然,今天谈起面向对象,我可以说很多,但由繁化简,最基础的东西是数据和方法。

    在《管窥Ruby——对象基础》中,我们看到了类的结构:RClass。为了加深印象,先来回顾一下RClass的定义:
    struct RClass {
        struct RBasic basic;
        struct st_table *iv_tbl;
        struct st_table *m_tbl;
        VALUE super;
    };
    (ruby.h)

    先来看看,iv_table和m_tbl的类型:st_table,其实它就是一个hash表,当然,名字让人有些混淆。它声明在st.h中,实现在st.c中。这个hash表是学习数据结构的一个好例子,它的实现很清楚。不过,这段代码并非Ruby的产品,而是从他处借鉴而来,文件开头的声明,说明了这一点:
    /* This is a public domain general purpose hash table package written by Peter Moore @ UCB. */

    这次管窥的目标是类的方法。

    知道了方法存储的结构,它的使用方法便也不难估计。要执行一个方法,大约就是一个查表的过程,下面的代码证实了这一点:
    static NODE*
    search_method(klass, id, origin)
        VALUE klass, *origin;
        ID id;
    {
        NODE *body;

        if (!klass) return 0;
        while (!st_lookup(RCLASS(klass)->m_tbl, id, (st_data_t *)&body)) {
            klass = RCLASS(klass)->super;
            if (!klass) return 0;
        }

        if (origin) *origin = klass;
        return body;
    }
    (eval.c)

    这段代码相当于在klass中查找名为id的方法。VALUE的用法在《管窥Ruby——对象基础》中已经提到过。需要解释一下的是id,它的类型是ID,ID定义如下:
    typedef unsigned long ID;
    (ruby.h)

    按照我们通常的理解,函数应该以其名字进行表示,这并不矛盾,因为在Ruby实现中,字符串和ID之间有个一一对应的关系,用ID而非原始的字符串,在处理的性能上要得到一些提升,ID和原始字符串之间的转换实际上也是有st_table来完成的。

    在这段代码上,我们可以看到了查找方法的方式,如果在该类的方法中找不到,就会沿着超类向上找。

    熟悉Java或C++的人可能看出了一些差异,这里没有传说中的虚拟函数表。回忆一下,使用虚拟函数表的调用方式,其查找过程就是以在虚拟函数表中的偏移(相当于数组索引)找到这个函数,通常,这种方法要比这里查表的方法快一些。

    牺牲性能,换来的是灵活。使用查表法,我们用的是名字与方法体对应,如果需要,我们在运行时可以在这个表中添加新的项,这也就实现了所谓的动态,也成为许多人吹捧动态语言的一个重要基础。有了这样的基础,我们不难理解那些看上去似乎神奇的做法,比如动态修改代码,比如为已有的类添加新的方法,要做的只是修改对应函数名的实现或是添加一个新的表项而已。当然,要做到动态,不只这一点这么简单,还要有其它的部分与之配合。

    使用虚拟函数表的方法,表项在编译时便已固定,把函数映射为在虚拟函数表中的偏移,到了运行时,只知道“偏移、执行”,至于究竟是哪个函数,无从知晓。类似于查表的过程,在编译的时候一定是存在的,但不存在于运行时。对程序而言,从源码到运行是一个完整的过程,一些功能被某些语言放到了编译时,而在另一些语言中被放到了运行时,折衷的原则取决于语言设计。

    两种方法还有一个差别。我们已经看到了查表的实现,每个类只要保持它自己的方法就可以,但依然可以使用超类的方法,去它的超类中找就是了。虚拟函数表的实现中,每个类的表中,不仅仅要保持自己的定义的方法,还要保持自己超类的方法,我们知道,在面向对象的语言中,子类对象常常要当作超类对象使用,而在运行时,要找某个方法,只知“偏移”,所以,子类的虚拟函数表必须完全兼容超类的虚拟函数表,才能保证整个系统的正常运行,而保证的方法就是保存超类的所有表项。

    这样带来的问题是,当子类增多,虚拟函数表就无可避免的会增多,即便子类只有一个属于自己的方法,但它仍要带有超类所有的方法,这是一个巨大的负担。所以,那些建议“不要定义太庞杂的继承系统”的说法,是有一定物理基础的。

    当然,我们可以想,是否虚拟表可以像动态语言这样,每个类只保持自己的方法,找不到的话,逆流向上。不要忘了,虚拟表本身是为了让运行更快,这样的实现会让性能受到损失,那样做便背离了语言设计的初衷。而且前面已经说过了,方法的信息在编译时已经消除了,若要判断找的方法是不是自己要找的方法,岂不是要增加额外的信息。

    查表不是一个快速的选择,在具体的实现中,Ruby中有一个缓存的实现,将一些调用过的方法缓存起来,调用方法的时候会先看方法是否已存在于缓存之中,这样的话,可以提升一些函数调用的性能。

  • 从C/C++或Java这种静态类型语言起步的人,初涉Ruby之类动态类型语言的时候,感觉最明显的差异莫过于无处着力的类型了,每每编写程序,总会习惯性的加上类型的声明,这虽不是什么大不了的事情,但总要有个适应的过程。

    形,需要有神对应,代码中没有体现类型,那么在实现中,就要把所有的对象纳入同一个体系之中。Ruby的基本实现以C完成,在C中不可能再去除类型,因此总要有个类型作为基础,就像Java中所有类的基类都是Object一样。在Ruby的实现中,随处可见的是VALUE类型,它就是我们要找的根。熟悉C的人都知道,要想实现这种可以代表一切的东西,最常用的类型就是void*。稍微有些出乎意料,下面就是VALUE的定义:

    typedef unsigned long VALUE;
    (ruby.h)

    对于很多熟悉C编程的人而言,指针就是一个整数,一个内存地址,所以,unsigned long与void*之间没有本质的差别,事实上,在Ruby的代码中,VALUE确实经常被当作普通的指针使用。当然,这里存在一个问题,只有unsigned long和void*大小相同的时候,我们才能安心使用。严格说来,unsigned long大点也没有关系。至少在现在的大多数机器上,这种假设可以完全得到满足,所以,Ruby舒舒服服的运行着。所以,查看Ruby代码的时候,我们完全可以把VALUE视为一个普通的指针。

    有合自然要有分,纳入统一的体系没有问题,但在做实际处理的时候,我们还是要知道一些关于类的信息:可能是具体的类型,可能是需要执行的方法。如果需要了解具体的类型,直接转型是最为便捷的选择,对于内建的类型,Ruby提供了一些宏,用来简化代码的编写,比如:
    VALUE str = ... ;
    RSTRING(str)->len;

    这里就是把str转型为字符串进行处理。对了,我们还没看到字符串的结构体定义,那么下面就来看一下,顺便多看几个内建类型对应的C结构:
    struct RObject {
        struct RBasic basic;
        struct st_table *iv_tbl;
    };

    struct RString {
        struct RBasic basic;
        long len;
        char *ptr;
        union {
     long capa;
     VALUE shared;
        } aux;
    };

    struct RArray {
        struct RBasic basic;
        long len;
        union {
     long capa;
     VALUE shared;
        } aux;
        VALUE *ptr;
    };
    (ruby.h)

    再来看一下转型是如何做:
    #define R_CAST(st)   (struct st*)

    #define RSTRING(obj) (R_CAST(RString)(obj))
    (ruby.h)

    这几段代码合在一起印证了前面提到的把VALUE当指针的说法。实际上,在Ruby中,真正这么用的时候并不多,毕竟在C层次上的类是少数,多数的还是在Ruby层次上。所以,更多的时候,我们要知道类相关的信息,早在Java的时候,我们就知道了所谓元对象的存在,那我们不妨来看一下在Ruby中这是如何实现的。之所以要多列几个类,是为了发现共同点,抛开类的具体内容,我们发现在这几个类定义都是以struct RBasic开头,看一下它的定义:
    struct RBasic {
        unsigned long flags;
        VALUE klass;
    };
    (ruby.h)

    flags的作用自然是记录类的一些信息,让我们暂且略过,把目光放在klass上,没错,名字已经告诉我们,它就是要找的类对象。它对应C结构是
    struct RClass,其定义如下:

    struct RClass {
        struct RBasic basic;
        struct st_table *iv_tbl;
        struct st_table *m_tbl;
        VALUE super;
    };
    (ruby.h)

    一目了然,basic是所有类共有的定义,指向它的类,这里不必深究,那是另外的故事了。iv_tbl和m_tbl分别存放变量和方法,super指向超类。这样,只要我们知道对象就可以找到它的类信息了,一个庞大的系统就此运转起来。

  • 2006-07-29

    编译Ruby

    安装Cygwin
    编译Ruby,首先要选择编译环境。如果是Linux环境,通常已经包含了必要的工具,而我的机器是Windows环境,正如我在前面的blog中提到过的,Cygwin将对用户友好的Windows和对开发者友好的Unix很好的结合在一起,所以,Cygwin成为了这次编译Ruby活动的首选。

    1 到Cygwin的网站上下载安装程序,不要为setup.exe只有几百K而窃喜,因为它只是一个引导程序,真正的程序在后面。
    2 运行setup.exe,选择Downloading Without Installing。如果网速足够快的话,可以考虑Install from Internet。
    3 略过俗套的目录选择,选择一个自己顺眼的下载站点。
    4 选择需要的软件,如果想全部选择,点击All边上的小圈,我们可以看到后面内容的变化。有位实心的兄弟,为了全部安装,居然一项一项点选,佩服其耐性。我安装的成果是,全部下载需要几百M的空间,安装需要上G。如果不想全选,那就自便吧!
    5 最好满足依赖关系,否则后果自负。
    6 准备好了,漫长的下载过程开始了。如果运气好,一觉醒来,我们可以看到下载完成的提示。
    7 再次运行setup.exe,选择Install from Local Directory,剩下的就是人所共知的本地安装了。

    编译Ruby
    1 到Ruby的网站上,下载Ruby的源码,我下载的是最新的1.8.4的源码
    2 进到Cygwin的Shell中,解压缩源码:
    tar -xvf ruby-1.8.4.tar.gz
    3 进到ruby的目录中,有兴趣的话可以拜读一下README。
    4 ./configure,进行基本的配置,生成makefile
    5 make,真正的编译过程,正常的话,会生成一个Ruby的可执行文件。
    6 有兴趣的话,还可以验证一下生成的结果
    make test

    一个小问题
    我们肯定对新鲜出炉的Ruby有着无比浓厚的兴趣,于是写了一个Ruby版的“Hello,world”:
    puts "Hello, world!"

    然后满怀欣喜的运行这个程序,期待着Ruby的问候:
    ./ruby hello.rb

    可能结果会很不幸,我们得到了一个出乎我们意料的结果:
    ./ruby: no such file to load -- ubygems (LoadError)

    这个问题来自Ruby的环境变量RUBYOPT,它是用来记录附加命令行参数,也就是说,对于常用的参数,只要把它设置在RUBYOPT中,就不必每次输入了。究竟是怎样的参数,阻挡了我们使用Ruby的脚步呢?echo一下。
    echo $RUBYOPT
    结果是:-rubygems

    Rubygems是一个类似于RPM的包管理器,不过,在这里怎么会成了绊脚石。看一下帮助就知道了,原来-r表示需要在执行脚本之前加载一个特定的库,于是,这个参数就被解释成了需要加载ubygems这个库,没有这个库当然就会加载失败了。最简单的解决办法就是抛弃它,在BASH中,我们可以这么做:
    export RUBYOPT=

    这是一个很简单的问题,可我解决的过程中却走了好大弯路,居然是在顺着源码追进去很远的情况下才最终发现了问题所在。记得有问题先搜索!类似的问题在Windows上也有,如果你用的Ruby是One-Click的版本。不过,在那个版本中,它通过在Ruby的查找路径上添加了一个ubygems.rb的文件屏蔽了这个问题。

    好了,问题解决了,就让新鲜的Ruby和我们问好吧!
    ./ruby hello.rb
    Hello,world!