コンパイルする

C言語のコードを、Rubyの拡張としてロードできるようにコンパイルするには、 ちょっとしたコンパイラのオプションが要ります。 Rubyの mkmf 標準ライブラリがあれば、 適切なmakefileを生成してくれるので、 コンパイルが楽になります。

準備

まず、C言語のコードがあるのと同じディレクトリに extconf.rb というファイルをつくります。

#!/usr/bin/env ruby
require 'mkmf'

# コンパイルのための準備がここに来ます

create_header
create_makefile 'foobar'

準備する部分では、標準的なUNIXでの configure スクリプトでするようなことをします。 例えば次のようなことです。

  • 現在のプラットフォームの特徴を確認する
  • 必要なライブラリと関数があることを確認する
  • ビルドするのに必要なプログラムがあることを確認する

よくある機能は mkmf で提供されます(でも、必要とあらばRubyをまったく使わなくてもいいです)。 例えば、SDL2を使っていて、それとは別に int の大きさを知らなければいけないような拡張であれば、 こんな感じで呼び出すことになります。

check_sizeof('int')
have_library('SDL2')
have_func('SDL_Init', 'SDL2/SDL.h')

create_header は、それより前に呼んだ mkmf の関数の結果に基づいて、 前処理器の定義を含む extconf.h ファイルをつくります。 この例では extconf.h は次のような内容を含みます。

#define SIZEOF_INT 4
#define HAVE_SDL_INIT 1

このヘッダはC言語のファイルに取り込むとよいでしょう。 そうすれば様々なプラットフォームでコードが使えるようになります。 気に留めておいてほしいことは、 mkmf 関数がビルドの失敗を示す値を返したら、 extconf.rb スクリプトを頓挫させられますし、そうするべきだということです。 例えば、ある拡張にSDL2が 必須 であれば、 have_library('SDL2')false を返したときは、 何らかの有意味なエラーメッセージを吐いて終了するようなスクリプトにしましょう。 とりあえずMakefileを生成して、 よくわからないコンパイルエラーで利用者を置き去りにするよりはマシです。

create_makefile はもちろんMakefileをつくりますが、 その引数が特に大切です: C言語のコードの入口を決めるものであり、 コンパイルされたライブラリの名前であり、 Rubyで require するときの引数でもあるのです! これは拡張の名前であるべきです。

生成されるMakefileの変数のいくつかを変更するには、 Rubyでの対応する大域変数を変更します: $CFLAGS$CPPFLAGS$LDFLAGS がそうです。 自動的な生成方法がうまくいかないときは、 Makefileでのオブジェクトファイルのリストを $objs 大域変数で指定することもできます。

全ての mkmf 関数とそれぞれのオプションはオンラインで充分に文書化されています。 extconf.h を生成する関数に加えて、 また違ったソースファイルの配置や依存関係を制御する様々な関数があります。

実行

あとはもう、これ以上簡単になりようがありません

$ ruby extconf.rb
$ make

1 もちろん、C言語のコードがコンパイルできないことには話が進まないですけどね。

初期化

C言語のコードでは ruby.h を含めてAPIを使えるようにしたいのではないでしょうか。 その他には、 require したときにRubyの仮想機械がライブラリを呼べるように、 関数を定義するだけでよいです。 関数の名前は、 extconf.rbcreate_makefile に渡した引数で決まります。 ここまでの例で "foobar" を使ってきたので、 次の内容の foobar.c ファイルをつくります。

#include <ruby.h>
#include "extconf.h"

void Init_foobar()
{
	/* `require`により走るコード */
}

ファイル名

拡張に1つのC言語のファイルだけしかないなら、 さっきやったように拡張の名前と同じファイル名にしましょう。 逆に複数のC言語のファイルからなる場合、 拡張に 同じ名前を使わない ようにしましょう。 リンクする段階でMakefileが拡張名と同じ .o ファイルを生成すると、 それと同じファイル名にコンパイルされる .c ファイルがある場合に衝突してしまうからです。

また、 conftest.c というファイル名も、 mkmf によって書き込まれるかもしれないので避けます。

やったね

これにてmakeで.so(またはその他のライブラリ)にコンパイルして、Rubyでrequireできるようになりました。 純粋なC言語のコードでInit関数を実装していくこともできますが、たぶんいったん戻って、もっと面白いことをするためにC APIを学ばれるのではないでしょうか。

Gem

いい感じに拡張が動くようになったら、簡単に配布できるRubyのgemとしてまとめたいはずです。 rubygems.orgにはgemをつくる上での詳細な手引きがありますが、C拡張に関して言えば extconf.rb についてspecに記すだけでよいです2

Gem::Specification.new do |spec|
  # usual gem spec stuff...

  spec.extensions = ['ext/extconf.rb']
end

gemが複数の拡張に依存していたら、 ext/ のサブディレクトリにそれぞれ固めて、 specに各々の extconf.rb を記します。

脚註

  1. 公式の文書は、--vendor のようなコマンドライン引数を mkmf がパースできると仄めかしている ようです。 でも、このことはどこにも文書化されていないようです。 

  2. http://guides.rubygems.org/gems-with-extensions/