Ruby の拡張機能を C++ で実装する (Rice利用)
Ruby プログラムを書いていると, 時々速度が必要な部分でどう頑張っても遅い...ということがあり, その時試したときの備忘録 Ruby はメインで使っているプログラミング言語じゃないけど, 時々黒魔術コードに出会うことがあって面白い
tl;dr
Rice導入
Riceは Ruby Interface for C++ Extensions
の略で
C++で書いたプログラムをRubyで利用できるようにするためのgem
残念ながら Windowsは非対応
Riceを利用するには C++14
以上をサポートしたコンパイラが必要
Pythonで言えば pybind11に近い存在(だと思う) github.com
Riceはもともとは以下のリポジトリで開発されていたようだが github.com
今はfork先のこのリポジトリでメンテされているらしい github.com
Riceのインストール
Riceのインストールは簡単で以下のコマンドで一発
$ gem install rice
これでRiceの準備は完了
Riceを使ってみる
Riceを使った実装の流れはこんな感じ
2番目のインタフェースを書くのがC++での拡張機能を呼ぶ上でコアになるところ。 とはいえ、Riceのおかげで非常にシンプルに実装できる
1. C++ プログラムを書く
とりあえずの例として受け取った数を2倍する関数を実装する
/* simple.cpp */ // int型の数値を受け取って2倍にして返す関数 int twice(int num) { return num * 2; }
特に説明はいらないと思う
Ruby側で呼び出したとき、int型じゃないものや、intの範囲を超えるものを与えてしまったらどうなるか みたいな難しいことは今は考えないことにする
2. Rubyで呼び出すためのインタフェースを書く
シンプルな例なので、先程関数を定義したファイルに続けて書く
/* simple.cpp */ // int型の数値を受け取って2倍にして返す関数 int twice(int num) { return num * 2; } // 以下が追加した内容 extern "C" void Init_util(void) { RUBY_TRY { Rice::define_module("Util").define_module_function("twice", &twice); } RUBY_CATCH }
ここでやっていることは
Rice::define_module("Util")
でUtil
という名前のモジュールを作成.
define_module_function("twice", &twice)
で
Util
モジュールにtwice
という名前でtwice
関数を結びつけてる.
上記コードをビルドして util.so
(Macの場合は util.dylib
?)を作り、
これをRuby側で require "util"
といったように読み込むとInit_util
関数を探して実行しているらしく、
CRubyで呼び出せるようにするためextern "C"
する
(init_ほげ
を読み込むこの挙動がRice
独自の挙動なのかはよくしらない)
異なるライブラリ名をつけたい場合Init_Gem名
のように読み替えれば大丈夫
3. MakefileをRubyプログラムで生成してビルド
RiceではRubyでMakefileを生成する機能があり, 基本的にそれを使う. こんな感じ.
# extconf.rb require 'mkmf-rice' create_makefile('util')
複雑なことをしなければ, 上記の2行で済む
Rubyにはもともとmkmf
というRuby拡張ライブラリを作るためのMakefileを生成するGemがあるが、
Rice
ではmkmf-rice
を使う
mkmf-rice
を使うことでRiceライブラリや標準C++ライブラリと簡単にリンクすることができ、
ヘッダーがどこにあるのか, ライブラリがどこにあるのかを意識する必要がなく、
簡単にMakefileが生成できる
前節と同じく、異なるライブラリ名をつけたい場合はcreate_makefile('Gem名')
で読み替える
以下のようにMakefileを生成しビルドする.
# Makefile生成 $ ruby extconf.rb # ビルド $ make
ビルドするとutil.so
ファイルができていると思う.
4. RubyからC++で実装した機能を呼び出す
require 'util'
するためにはこのファイルがどこに有るのかをRuby側に教える必要があり
大まかに以下の3通り方法がある
require_relative
を使う- installする
RUBYLIB
にファイルへのパスを加えるか,RUBYLIB
のパスの通っている場所にファイルを置く
1版と2番は準備が必要なので、まず1番から
1. require_relative
を使う
require_relative
を使う場合はファイルからの相対パスを書けばいいだけなので
# test.rb # ビルドしたファイルをロードする require_relative './sample' # 簡単なサンプルコード p Util.twice(2) # -> 4
実行する
$ ruby test.rb
2. install する
一番シンプルな方法で make
コマンドでビルドしたあと普通にインストールするだけ
$ make install
以下のように実行できる
# test.rb require 'util' p Util.twice(2) # -> 4
個人的には環境を汚してしまう感?があって若干抵抗感...
3. RUBYLIB
にファイルへのパスを加えるか, RUBYLIB
のパスの通っている場所にファイルを置く
生成された util.so
を RUBYLIB
のパスの通っている場所に置くか
以下の用に設定し
export RUBYLIB="${RUBYLIB}:パスを通したいディレクトリの絶対パス"
パスを通したいディレクトリの絶対パス
に util.so
を置く.
あとはインストールした場合と同じく
# test.rb require 'util' p Util.twice(2) # -> 4
Classを作ってみる
クラスを作るときはRice::define_class
を使う
このとき必ずしもC++
プログラム上でもClassである必要はなくて
次のように関数を結びつけることもできる.
#include <iostream> #include <string> #include "rice/Class.hpp" void sayHello(Rice::Object) { std::cout << "Hello World" << std::endl; } void say(Rice::Object self, const std::string& str) { std::cout << str << std::endl; } extern "C" void Init_speaker(void) { RUBY_TRY { Rice::define_class("Speaker") .define_method("say", &say) .define_method("say_hello", &sayHello); } RUBY_CATCH }
Rice::define_class
の第一引数はRubyでのクラス名.
そして作成したクラスにdefine_method
でメソッドを追加する.
define_method
の第一引数はRubyで呼び出すときのメソッド名
このとき結びつける関数の第一引数はRice::Object
である必要があるらしく、
またこれを通してインスタンス変数の設定等ができるらしい
最初の例と同じくMakefileを生成する
require 'mkmf-rice' create_makefile('speaker')
下記のようにRuby側で呼ぶ
require 'speaker' speaker = Speaker.new speaker.say_hello() # -> Hello World speaker.say("hogehoge") # -> hogehoge
気が向いたら追記する予定...
c.f.
編集ログ
- 2019年10月29日 install での利用方法を追記
- 2020年7月11日 サンプルコードを修正
- 2020年8月22日 構成を修正 目次の追加
- 2020年8月23日 Classサンプル追加