ネコと和解せよ

技術的なあれこれの備忘録のつもり

Ruby の拡張機能を C++ で実装する (Rice利用)

Ruby プログラムを書いていると, 時々速度が必要な部分でどう頑張っても遅い...ということがあり, その時試したときの備忘録 Ruby はメインで使っているプログラミング言語じゃないけど, 時々黒魔術コードに出会うことがあって面白い

tl;dr

  • Rice導入
  • C++ プログラムを書く
  • Rubyから実装した機能を呼ぶ

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を使った実装の流れはこんな感じ

  1. C++プログラムを書く
  2. Rubyで呼び出すためのインタフェースを書く
  3. MakefileRubyプログラムで生成してビルド
  4. RubyからC++で実装した機能を呼び出す

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. MakefileRubyプログラムで生成してビルド

RiceではRubyMakefileを生成する機能があり, 基本的にそれを使う. こんな感じ.

# 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通り方法がある

  1. require_relative を使う
  2. installする
  3. 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.soRUBYLIBのパスの通っている場所に置くか

以下の用に設定し

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.

www.ibm.com

編集ログ

  • 2019年10月29日 install での利用方法を追記
  • 2020年7月11日 サンプルコードを修正
  • 2020年8月22日 構成を修正 目次の追加
  • 2020年8月23日 Classサンプル追加