simotin13's message

simotin13といいます。記事の内容でご質問やご意見がありましたらお気軽にコメントしてください\^o^/

低レイヤプログラミングのためのRubyの拡張モジュール開発入門

この記事は、低レイヤのコードをRubyで書きたいという人のための拡張モジュール作成入門記事です。

Rubyが好きでC言語がある程度分かる方には読んで頂けると参考になるかと思います。※ただし、私が真面目に拡張モジュールを書いていたのはもう5年以上前なので知識が古くなっている可能性があります。

この記事を書いた動機ですが、RXマイコンのリンカをRubyで書いていて、Rubyで書いているとC言語の構造体へのアクセスが辛くなってきたので、FFIや拡張モジュールを使うべきだったと感じました。

久しぶりに拡張モジュールの書き方など復習してみたので記事にしてみました。

FFIも拡張モジュールもちょこちょこと触っているのですが、私のように

「やる気はないけど、進捗は欲しい」

という方には、RubyC言語を上手く組み合わせて開発をするというのは悪くない実装方針ではないでしょうか*1
この記事を含めて、できれば低レイヤのプログラミングの為に拡張モジュールやFFIを上手く使う方法として整理したいと思います。

Rubyの拡張モジュールとは何か

拡張モジュールはC言語C++言語で実装したRuby用のライブラリです。
誰かに挨拶するコードをRubyで書く場合、

def hello name
  puts "hello " + name + "!"
end

hello("simotin") # => hello simotin!

のように .rb の拡張子のファイルに Ruby の文法に則って書くのが普通のRubyのプログラミングです。

これに対して拡張モジュールで実装する場合、

void hello(char *name)
{
  printf("hello %s!\n", name);
}

のように使いたい機能をC言語の関数として実装し、

require "./extlib"
hello("simotin")

のようなRubyのコード書いて実行する形になります。

この例だと拡張モジュールを書くメリットはよくわからないと思いますが、C言語で開発できるということは、

  • C言語のコード・ライブラリ資産が活用できる
  • ライブラリの動作を高速化できる
  • 低レイヤのプログラミングができる <- 今回の私の目的はこれ!

といったメリットがあります。

デメリットとして、

  • 移植性が下がる
  • コーディング量が増える
  • メモリ管理を意識する必要がある

といった点が挙げられます。

簡単な拡張モジュールを書いてみる

それでは早速、シンプルな拡張モジュールを書いてみましょう。サンプルコードを Github に上げていますので、サンプルコードはこちらからどうぞ。

github.com

事前準備

拡張モジュールを開発するにあたってビルドできる用意する必要があります。
Rubyコンパイルできる環境があれば問題ありませんが、ビルドできない場合は、
Debian系だと


sudo apt install ruby-dev

で開発に必要なヘッダファイル等がインストールされるはずです。また、C言語での開発になるので当然GCCなどのコンパイラは必要になります。

ビルドと実行手順

Githubから落としてきた場合のビルドと実行例です。


$ git clone https://github.com/simotin/rb_extmod_sample.git
$ cd rb_extmod_sample/
$ ruby extconf.rb
creating Makefile

$ compiling rb_extlib.c
linking shared-object extlib.so

$ ruby test.rb
Hello simotin!
One year is 365 Days.

解説

extconf.rb

まず、 extconf.rb は Makefile を生成するためのスクリプトです。

特にビルド時のオプション指定が必要なければ、
create_makefile( 'extlib' ) の1行を書くだけです。
create_makefileの引数はビルドしたいライブラリ名になります。

Makefileの設定を追加したいとき

extconf.rb の内容に従ってMakefileが生成されるのでコンパイルのオプション関係はこのファイルで指定する必要があります。

例えば、よくあるCFLAGSでワーニングレベルを設定したい場合は


$CFLAGS += ' -Wall'
の一行を追加すればMakefileコンパイラに対するCFLAGSに -Wall が指定されるようになります。

その他にも、


$INCFLAGS += ' -I..//include'
$LIBRUBYARG_SHARED += ' -L/usr/local/lib/'

と書くことでインクルードやライブラリパスを追加できます。

ロスコンパイルしたいとき

場合によっては拡張モジュールをクロスコンパイルしたいときがあるかもしれません。
その様な場合は CONFIG['CC'] に格納されているコンパイラのコマンド名で設定を切り替えたりします。
以下、具体例です。


require 'mkmf'
require 'rbconfig'

if CONFIG['CC'] =~ /arm-none-linux-gnueabi-gcc/
# for cross build
$INCFLAGS += ' -I../arm/include'
$LIBRUBYARG_SHARED += ' -L/usr/lib/arm -Wl,-rpath-link /usr/lib/arm'
else
# for self build
$INCFLAGS += ' -I../../../../include'
$LIBRUBYARG_SHARED += ' -L/usr/lib/'
end

# 警告レベルを追加
$CFLAGS += ' -Wall'
create_makefile( 'extlib' )

上記は、Arm用クロスコンパイラを使用する場合にビルドのオプションを切り替えています。

Makefileの生成

extconf.rb の記述ができれば、


ruby extconf.rb
とするとMakefileが生成されます。
ビルド対象には、 extconf.rb と同じディレクトリに配置したファイルが自動で指定されます。

拡張モジュールのビルド

ということで、Makefileが生成されたらあとは、makeを実行すると拡張モジュールがビルドされます。
ビルドされた拡張モジュールは .so の共有ライブラリの形で生成されます。
rubyのファイルからは、 .so を除いた require "./extlib" のようにしていると .rb のスクリプトと同様にロードされます。

コード(extlib.c)解説

test.rb では extlib をロードした後、ExtLib モジュールの hello メソッドを呼び出しています(引数は"simotin")
これは完全にRubyのコードですね。

では、拡張モジュール側の実装を見てみましょう。
拡張モジュールは require されるとまず void Init_モジュール名(void) 関数がRuby側から実行されます。
要するに Init_モジュール名(void)が拡張モジュール側のエントリポイントになります。*2

このInit関数では、ライブラリとして必要な初期化処理を行います。
具体的には、

  • モジュールの登録
  • クラスの登録
  • クラスの関数の登録
  • 定数の登録

といった処理です。

サンプルでは、

	module = rb_define_module( "ExtLib" );

により ExtLibというモジュールを定義しています。
この一行はRubyの文法でいうところの

module Extlib
end

相当です。

続けて、

	// モジュールへのメソッドの追加
	rb_define_module_function( module, "hello", rb_extlib_hello, 1 );

	// 定数の追加
	rb_define_const( module, "DAYS_YEAR", INT2FIX( DAYS_YEAR ) );

によりメソッドと定数を定義しています。
rb_define_module_function関数は、

  • 関数を定義するモジュールオブジェクト
  • 定義する関数名
  • 関数の実体(アドレス)
  • 引数の数

を指定する必要があります。

rb_define_module_functionで定義した関数の実体は rb_extlib_hello になります。

この関数を見てみると、

static VALUE rb_extlib_hello(VALUE self, VALUE name)
{
	Check_Type( name, T_STRING );		// 型チェック

	// Ruby の文字列は StringValuePtr でchar* として取り出せる
	char *str = StringValuePtr(name);
	printf("Hello %s!\n", str);
	return Qnil;
}

となっています。

まず、引数と戻り値ですが、拡張モジュールでRubyから呼び出される関数の引数と戻り値はVALUE型である必要があります。
また、第一引数は必ずメソッドのレシーバオブジェクトになります。

ユーザが指定する引数は第2引数以降で指定します。*3
rb_define_module_function 関数の呼び出しで、最後に1という数字を指定していますが、これはレシーバーオブジェクトを除いた引数の数になります。
従って引数の指定が必要のない関数では、rb_define_module_functionの第4引数は0になります。

関数の中身を見てみましょう。
拡張モジュールの実装では、拡張モジュール特有のコードが出てきます。
まず、Check_Typeは型チェックです。変数が指定した型かどうかチェックします。
一致しなかったらTypeError例外が発生します。


関数では 名前の文字列を期待する name は VALUEです。
C言語では、文字列は char * のポインタですので、ポインタを取得する必要があります。
これは StringValuePtr 関数で取得できます。
char *が取れればあとはprintfするだけです。

注意点として、サンプルのように画面に文字列を表示させるだけであれば戻り値は必要ありません。
この場合、拡張モジュールの関数では、 Qnil を指定します。
Qnilは文字通り nil に相当する値です。*4

以上の実装は、rubyのコードだと

modulbe ExtLib
  DAYS_YEAR = 365
  
  def hello name
    puts "hello #{name}!"
  end

  module_function :hello
end

相当の実装になります。

まとめ

ということで駆け足で拡張モジュールの書き方について説明してみました。
今回はモジュールにメソッドを定義する方法について書きました。低レイヤは今のところ関係ありませんが、次回はクラスやCの構造体を扱う方法について触れてみたいと思います。

*1:私も試行錯誤中な感じですが・・・

*2:Linuxカーネルモジュールと似てる気がしますね

*3:引数が無くてもレシーバオブジェクトは必要

*4:なぜQnilかというとlispがそうなっていたからだという話がRHGに書いてあった気がします。