创建一个包含在安装时构建的扩展的宝石。
许多宝石使用扩展来包装用 C 编写的库,并使用 Ruby 包装器。例如,nokogiri 包装了 libxml2 和 libxslt,pg 是 PostgreSQL 数据库 的接口,以及 mysql 和 mysql2 宝石,它们提供了 MySQL 数据库 的接口。
创建一个使用扩展的宝石需要几个步骤。本指南将重点介绍您应该在宝石规范中添加的内容,以使此过程尽可能简单和易于维护。本指南中的扩展将包装 C 标准库中的 malloc()
和 free()
。
宝石布局
每个宝石都应该从一个 Rakefile 开始,其中包含开发人员在宝石上工作所需的 task。扩展文件应该放在 ext/
目录中,在一个与扩展名称匹配的目录中。在本例中,我们将使用“my_malloc”作为名称。
一些扩展将部分用 C 语言编写,部分用 Ruby 语言编写。如果您要支持多种语言,例如 C 和 Java 扩展,则应将特定于 C 的 Ruby 文件放在 ext/
目录下,以及 lib/
目录下。
Rakefile
ext/my_malloc/extconf.rb # extension configuration
ext/my_malloc/my_malloc.c # extension source
lib/my_malloc.rb # generic features
构建扩展时,ext/my_malloc/lib/
中的文件将被安装到 lib/
目录中。
extconf.rb
extconf.rb 配置一个 Makefile,用于构建您的扩展。extconf.rb 必须检查您的扩展依赖的必要函数、宏和共享库。如果缺少任何这些,extconf.rb 必须以错误退出。
以下是一个 extconf.rb,它检查 malloc()
和 free()
,并创建一个 Makefile,用于将构建的扩展安装到 lib/my_malloc/my_malloc.so
。
require "mkmf"
abort "missing malloc()" unless have_func "malloc"
abort "missing free()" unless have_func "free"
create_makefile "my_malloc/my_malloc"
有关创建 extconf.rb 的更多信息,以及这些方法的文档,请参阅 mkmf 文档 和 extension.rdoc。
C 扩展
包装 malloc()
和 free()
的 C 扩展位于 ext/my_malloc/my_malloc.c
中。以下是列表
#include <ruby.h>
struct my_malloc {
size_t size;
void *ptr;
};
static void
my_malloc_free(void *p) {
struct my_malloc *ptr = p;
if (ptr->size > 0)
free(ptr->ptr);
}
static VALUE
my_malloc_alloc(VALUE klass) {
VALUE obj;
struct my_malloc *ptr;
obj = Data_Make_Struct(klass, struct my_malloc, NULL, my_malloc_free, ptr);
ptr->size = 0;
ptr->ptr = NULL;
return obj;
}
static VALUE
my_malloc_init(VALUE self, VALUE size) {
struct my_malloc *ptr;
size_t requested = NUM2SIZET(size);
if (0 == requested)
rb_raise(rb_eArgError, "unable to allocate 0 bytes");
Data_Get_Struct(self, struct my_malloc, ptr);
ptr->ptr = malloc(requested);
if (NULL == ptr->ptr)
rb_raise(rb_eNoMemError, "unable to allocate %ld bytes", requested);
ptr->size = requested;
return self;
}
static VALUE
my_malloc_release(VALUE self) {
struct my_malloc *ptr;
Data_Get_Struct(self, struct my_malloc, ptr);
if (0 == ptr->size)
return self;
ptr->size = 0;
free(ptr->ptr);
return self;
}
void
Init_my_malloc(void) {
VALUE cMyMalloc;
cMyMalloc = rb_const_get(rb_cObject, rb_intern("MyMalloc"));
rb_define_alloc_func(cMyMalloc, my_malloc_alloc);
rb_define_method(cMyMalloc, "initialize", my_malloc_init, 1);
rb_define_method(cMyMalloc, "free", my_malloc_release, 0);
}
此扩展很简单,只有几个部分
struct my_malloc
用于保存分配的内存my_malloc_free()
用于在垃圾回收后释放分配的内存my_malloc_alloc()
用于创建 Ruby 包装器对象my_malloc_init()
用于从 Ruby 分配内存my_malloc_release()
用于从 Ruby 释放内存Init_my_malloc()
用于在MyMalloc
类中注册函数。
现在我们可以创建实际的 MyMalloc
类,并在 Ruby 中绑定新定义的方法(lib/my_malloc.rb
是正确的位置),例如
class MyMalloc
VERSION = "1.0"
end
require "my_malloc/my_malloc"
您可以按如下方式测试构建扩展
$ cd ext/my_malloc
$ ruby extconf.rb
checking for malloc()... yes
checking for free()... yes
creating Makefile
$ make
compiling my_malloc.c
linking shared-object my_malloc.bundle
$ cd ../..
$ ruby -Ilib:ext -r my_malloc -e "p MyMalloc.new(5).free"
#<MyMalloc:0x007fed838addb0>
但这会变得很繁琐。让我们自动化它!
rake-compiler
rake-compiler 是一套用于自动化扩展构建的 rake 任务。rake-compiler 可用于同一项目中的 C 或 Java 扩展(nokogiri 就是这样使用的)。
首先安装 gem
$ gem install rake-compiler
将 rake-compiler 添加到 Rakefile
中非常简单
require "rake/extensiontask"
Rake::ExtensionTask.new "my_malloc" do |ext|
ext.lib_dir = "lib/my_malloc"
end
现在您可以使用 rake compile
构建扩展,并将编译任务挂钩到其他任务(例如测试)。
设置 lib_dir
会将共享库放在 lib/my_malloc/my_malloc.so
(或 .bundle
或 .dll
)中。这允许 gem 的顶层文件为 ruby 文件。这允许您用 ruby 编写最适合 ruby 的部分。
例如
class MyMalloc
VERSION = "1.0"
end
require "my_malloc/my_malloc"
设置 lib_dir
还允许您构建一个包含针对多个 ruby 版本的预构建扩展的 gem。(针对 Ruby 1.9.3 的扩展不能与针对 Ruby 2.0.0 的扩展一起使用)。lib/my_malloc.rb
可以选择要安装的正确共享库。
Gem 规范
构建 gem 的最后一步是在 gemspec 中将 extconf.rb 添加到扩展列表中
Gem::Specification.new "my_malloc", "1.0" do |s|
# [...]
s.extensions = %w[ext/my_malloc/extconf.rb]
end
现在您可以构建并发布 gem 了!
扩展命名
为了避免 gem 之间的意外交互,最好让每个 gem 将其所有文件都保存在一个目录中。以下是名为 <name>
的 gem 的建议
ext/<name>
是包含源文件和extconf.rb
的目录ext/<name>/<name>.c
是主源文件(可能还有其他文件)ext/<name>/<name>.c
包含一个函数Init_<name>
。(Init_
函数后面的名称必须与扩展的名称完全匹配,才能被 require 加载。)ext/<name>/extconf.rb
仅当编译扩展所需的所有部分都存在时才调用create_makefile('<name>/<name>')
。- gemspec 设置了
extensions = ['ext/<name>/extconf.rb']
并在files
列表中包含所有必要的扩展源文件。 lib/<name>.rb
包含require '<name>/<name>'
,它加载 C 扩展。
进一步阅读
- my_malloc 包含此扩展的源代码,并附带一些额外的注释。
- extension.rdoc 更详细地描述了如何在 ruby 中构建扩展。
- MakeMakefile 包含 mkmf.rb 的文档,extconf.rb 使用该库来检测 ruby 和 C 库的功能。
- rake-compiler 以一种流畅的方式将构建 C 和 Java 扩展集成到您的 Rakefile 中。
- 编写 C 扩展 第 1 部分 和 第 2 部分) 由 Aaron Patterson 撰写。
- 可以使用 ruby 和 fiddle(标准库的一部分)或 ruby-ffi 来编写 C 库的接口。
- 扩展 Ruby 是 Programming Ruby 书籍中关于构建 C 扩展的章节。请注意:此内容比较旧,一些 C 扩展 API 已经改变。