首先介绍一下ruby项目的代码目录结构。通常情况下一个ruby扩展项目的目录结构如下:
NEWS
Rakefile
README.rdoc
doc/
ext/
COPYING为版权信息;NEWS包含了发行信息;Rakefile定义了rake任务;README.rdoc包含了用于生成RDoc文档的头部信息;doc目录下为该项目的文档;ext目录下为扩展程序的源代码以及extconf.rb文件。以上仅为参考,实际中还需根据自己的情况新增或者减少一些文件。
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
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。
注意:所有被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