RubyGems 导航菜单
指南

创建一个包含在安装时构建的扩展的宝石。

许多宝石使用扩展来包装用 C 编写的库,并使用 Ruby 包装器。例如,nokogiri 包装了 libxml2 和 libxsltpgPostgreSQL 数据库 的接口,以及 mysqlmysql2 宝石,它们提供了 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 的建议

  1. ext/<name> 是包含源文件和 extconf.rb 的目录
  2. ext/<name>/<name>.c 是主源文件(可能还有其他文件)
  3. ext/<name>/<name>.c 包含一个函数 Init_<name>。(Init_ 函数后面的名称必须与扩展的名称完全匹配,才能被 require 加载。)
  4. ext/<name>/extconf.rb 仅当编译扩展所需的所有部分都存在时才调用 create_makefile('<name>/<name>')
  5. gemspec 设置了 extensions = ['ext/<name>/extconf.rb'] 并在 files 列表中包含所有必要的扩展源文件。
  6. 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 库的接口。
  • 扩展 RubyProgramming Ruby 书籍中关于构建 C 扩展的章节。请注意:此内容比较旧,一些 C 扩展 API 已经改变。