• 2006-09-24

    扩展Ruby

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://www.blogbus.com/dreamhead-logs/3403516.html

    这个例子来自《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

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

    分享到:

    历史上的今天:

    引用地址: