RubyGems 導覽選單
指南

建立一個寶石,其中包含在安裝時建置的延伸功能。

許多寶石使用延伸功能來封裝以 C 編寫的函式庫,並使用 ruby 封裝器。範例包括 nokogiri,它封裝 libxml2 和 libxsltpg,它是 PostgreSQL 資料庫 的介面,以及 mysqlmysql2 寶石,它們提供 MySQL 資料庫 的介面。

建立使用延伸功能的寶石涉及多個步驟。本指南將重點放在您應該放入寶石規格中,以使此過程盡可能簡單且易於維護的內容。本指南中的延伸功能將封裝 C 標準函式庫中的 malloc()free()

寶石配置

每個寶石都應從一個 Rakefile 開始,其中包含開發人員處理寶石所需的工作。擴充套件的檔案應放入 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"

請參閱 mkmf 文件extension.rdoc,以取得有關建立 extconf.rb 和這些方法文件進一步的資訊。

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 也允許您建立一個 gem,其中包含針對多個版本的 ruby 預先建立的擴充套件。(Ruby 1.9.3 的擴充套件無法與 Ruby 2.0.0 的擴充套件一起使用)。lib/my_malloc.rb 可以選擇要安裝的正確共用程式庫。

Gem 規格

建立 gem 的最後一步驟是將 extconf.rb 加入 gemspec 中的擴充套件清單

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 擴充套件

延伸閱讀