write-ruby-extension-with-c

一个简单的例子

目录结构

首先介绍一下ruby项目的代码目录结构。通常情况下一个ruby扩展项目的目录结构如下:


NEWS
Rakefile
README.rdoc
doc/
ext/

COPYING为版权信息;NEWS包含了发行信息;Rakefile定义了rake任务;README.rdoc包含了用于生成RDoc文档的头部信息;doc目录下为该项目的文档;ext目录下为扩展程序的源代码以及extconf.rb文件。以上仅为参考,实际中还需根据自己的情况新增或者减少一些文件。

MKMF

MKMF是ruby扩展构建系统的一部分,它用于生成编译C程序所需的头文件和Makefile文件。通常该脚本的文件名为: extconf.rb,这个有点类似于python中的setup.py脚本。以下是一个简单的例子:

require 'mkmf'

RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC']

extension_name = 'example'

unless pkg_config('library')
    raise "library not found"
end

have_func('some_function', 'library/lib.h')
have_type('some_type', 'library/lib.h')

create_header
create_makefile(extension_name)

第1行导入了mkmf模块;

第5行定义了该扩展模块的名称;

第7到9行调用pkg-config检查所需的库是否存在;

第11行调用have_func方法检查在对应的库中some_function方法是否已定义,如果存在则会在生成的extconf.h文件中定义一个名为HAVE_的宏;

第12行调用have_type方法检查在对应的库中some_type结构体是否已定义,如果存在则会在生成的extconf.h文件中定义一个名为HAVE_TYPE_的宏;

第14行创建extconf.h头文件;

第15行创建Makefile文件。

extconf.rb脚本编写完成之后,可以执行如下命令使用:

$ cd ext
$ ruby extconf.rb
$ make

Rakefile

extconf 脚本创建完了,接下来是创建Rakefile。顾名思义Rakefile就是ruby的Makefile,即就是用ruby来编写Makefile的功能。以下是一个简单的例子:

require 'rake/clean'

EXT_CONF = 'ext/extconf.rb'
MAKEFILE = 'ext/Makefile'
MODULE = 'ext/example.so'
SRC = Dir.glob('ext/*.c')
SRC << MAKEFILE

CLEAN.include [ 'ext/*.o', 'ext/depend', MODULE ]
CLOBBER.include [ 'config.save', 'ext/mkmf.log', 'ext/extconf.h', MAKEFILE ]

file MAKEFILE => EXT_CONF do |t|
    Dir::chdir(File::dirname(EXT_CONF)) do
        unless sh "ruby #{File::basename(EXT_CONF)}"
            $stderr.puts "Failed to run extconf"
            break
        end
    end
end
file MODULE => SRC do |t|
    Dir::chdir(File::dirname(EXT_CONF)) do
        unless sh "make"
            $stderr.puts "make failed"
            break
        end
    end
end
desc "Build the native library"
task :build => MODULE

第9行和第10行分别设置了要删除的文件的列表。CLEAN变量中定义的文件列表会在rake clean命令中被删除;CLOBBER变量中定义的文件列表会在rake clobber命令中被删除。

从第12行到29行定义了build任务,用于生成扩展模块。

程序示例

接下来通过一个简单的例子来介绍C扩展的编写。创建一个新文件 my_test.c 内容如下:

#include "ruby.h"

static VALUE mTest;
static VALUE cTest;

static VALUE t_init(VALUE self)
{
    printf("\nCreate a MyTest instance.\n");

    return self;
}

void Init_my_test()
{
    mTest = rb_define_module("MyTest");
    cTest = rb_define_class_under(mTest, "MyTest", rb_cObject);
    rb_define_method(cTest, "initialize", t_init, 0);
}

第1行将ruby的头文件导入进来,以便能使用ruby的api。

在该程序代码中定义一个特殊的函数Init_my_test,它是该模块的初始化函数,在模块首次加载时执行。每个C扩展都需要定义一个名为Init_<name>的函数,这与Python的C扩展相似。

Init_my_test 函数内部使用rb_define_module 函数定义了一个名为MyTest的Module,然后再用rb_define_class_under函数为该Module定义了一个类MyTest。并且对应的初始化函数为t_init

  • rb_define_module: 定义一个Module;
  • rb_define_class_under: 在一个Module内定义一个Class;
  • rb_define_method: 定义一个实例方法,所需的参数依次为类对象、方法名、对应的C函数以及参数个数;

注意:所有被Ruby调用的C方法都必须返回一个VALUE类型的变量。

以上这段代码等效于如下ruby代码:

module MyTest
  class MyTest
    def initialize
      puts "\nCreate a MyTest instance."
    end
  end
end

程序写完之后运行命令: rake build 进行编译。

最后再通过一个小程序来进行测试:

# app.rb
require "./my_test.so"
require "test/unit"

class TestTest < Test::Unit::TestCase
  def test_test
    t = MyTest::MyTest.new
    assert_equal(Object, MyTest::MyTest.superclass)
    assert_equal(MyTest::MyTest, t.class)
  end
end