C++プログラムにRubyを組み込む

Rubyの紹介

Rubyは、まつもとゆきひろ氏によって開発されているオブジェクト指向スクリプト言語である。最近ではスクリプト言語のような言語特性が「軽い」言語を総称して軽量級言語(Lightweight Language)、略してLLともいう。

Rubyは、クラスやMixin(継承を使用せず、動的に既存クラスのメソッドをカスタム・クラスに追加するための手段の一つ)のようなオブジェクト指向機能はもちろん、演算子のオーバーロード、例外処理、イテレータ、クロージャ、ガーベージ・コレクタ、正規表現などの機能も備えている。

これだけ強力な機能を持っていながら、Rubyの文法はとてもシンプルなものとなっている。例として、C++とRubyでそれぞれ簡単なクラスを定義してみよう。

#include <iostream>
using namespace std;

class Person
{
private:
  char *m_name;
public:
  Person(char *name) : m_name(name) {}
  char *GetName()
  {
    return m_name;
  }
};

void main()
{
  Person *person = new Person("Foo");
  cout << "Name: " << person->GetName() << endl;
  delete person;
}

これはC++のコードだが、これと同じ内容のコードをRubyで書くと次のようになる。

class Person
  def initialize(name)
    @name = name
  end
  def name
    @name
  end
end

person = Person.new('Foo')
p person.name

見ての通り、Rubyには変数の型がない。文字列を代入すれば文字列型になるし、整数を代入すれば整数型になる。このような言語を動的型付け言語という。静的な型の整合性チェックは行われないが、その分素早くコードを書くことができる。

また、変数の宣言やメモリ管理は必要ない。変数はいきなり使うことができるし、参照されなくなったらガーベージ・コレクタという仕組みによって自動的に回収してくれる。

そして、変数の先頭に@が付いているとメンバ変数に、$が付いているとグローバル変数に、どちらも付いていなければローカル変数になる。これによって、記述を見るだけでそのスコープが分かるので、コードを追いやすくなっている。

さて、このRubyを自分のC++プログラムに組み込んでみたいと思う。例えば、アプリケーションを拡張するマクロ機能にRubyを使えば、アプリケーションの自由度を高めることができるし、ゲームのイベントを記述するのに使えば、システムとデータの分離に役立つだろう。他にも面白い使い方ができるかもしれない。

Rubyの組み込み

では、C++プログラムにRubyインタプリタを組み込んで、簡単なスクリプトを実行してみよう。

まず、Rubyの公式サイトからソースをダウンロードして、ライブラリを作成する。尚、ここではVisual C++ 2005でコンパイルする方法を説明するが、他の環境でコンパイルする方法については、適宜Ruby インストールガイドを参照するなどしてほしい。

Visual Studio 2005 コマンド プロンプトを開いて、ソースを置いた場所(ここではC:\ruby-1.8.4に置く)にあるディレクトリwin32に移動し、configureを実行する。

Setting environment for using Microsoft Visual Studio 2005 x86 tools.

C:\Program Files\Microsoft Visual Studio 8\VC>cd \ruby-1.8.4\win32
C:\ruby-1.8.4\win32>configure
Creating Makefile
type `"C:\Program Files\Microsoft Visual Studio 8\VC\BIN\nmake.exe"' to make rub
y for mswin32.

続いて、nmakeを実行する。

C:\ruby-1.8.4\win32>nmake

msvcr80-ruby18.lib(ダイナミック版)、msvcr80-ruby18-static.lib(スタティック版)、msvcrt-ruby18.dllという3つのファイルができただろう。

次に、ヘッダファイル群を適当な場所に置いて、そこをインクルード・ディレクトリに設定する。ヘッダファイルを直接プロジェクトに含めるようにしても構わないだろう。尚、必要なヘッダファイルは下記の通りだ。

これで最低限の準備は整った。早速、Hello Worldを出力するプログラムを書き、作成したライブラリをリンクしてビルドしてみよう。

#include <ruby.h>

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();

  // スクリプトの実行
  rb_eval_string("print 'Hello World!'");

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

外部ファイルを読み込んで、それを実行したい場合はrb_load()関数を使う。ここではtest.rbというファイルを読み込むようにする。rb_load()関数の第一引数はRuby独自の型VALUEで値を受け取るので、文字列をVALUE型に変換するためにrb_str_new2()関数を使う。

#include <ruby.h>

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();
  ruby_init_loadpath();

  // スクリプトをファイルから読み込んで実行
  rb_load(rb_str_new2("test.rb"), 0);

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

test.rbファイルの内容は、次のように書いてみた。

def hello
  print 'こんにちは!'
end

hello

これでC++プログラムからRubyのスクリプトを実行できるようになったが、このままではスクリプトでエラーが発生すると、プログラムが強制終了してしまう。この対処方法について見ていこう。

エラー処理

スクリプトで発生したエラーの情報をC++プログラム側で受け取りたいときは、rb_eval_string_protect()またはrb_load_protect()関数を使う。どちらの関数も、引数の最後に指定した変数に終了コードが入るようになっている。終了コードは正常終了なら0が、エラーが発生した場合は0以外の数値がそれぞれ入る。

発生したエラーの具体的な内容(エラー・メッセージ)は、グローバル変数のruby_errinfoに入っている。ruby_errinfoはVALUE型で情報が格納されているので、Cの文字列に変換するためにStringValueCStrマクロを使う。

まずrb_eval_string_protect()関数の場合だ。

#include <ruby.h>
#include <stdio.h>

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();

  // スクリプトの実行
  int state;
  rb_eval_string_protect("print 'Hello World!'", &state);
  if (state)
  {
    // エラーメッセージを出力
    printf(StringValueCStr(ruby_errinfo));
  }

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

rb_load_protect()関数の場合はこうなる。

#include <ruby.h>
#include <stdio.h>

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();
  ruby_init_loadpath();

  // スクリプトをファイルから読み込んで実行
  int state;
  rb_load_protect(rb_str_new2("test.rb"), 0, &state);
  if (state)
  {
    // エラーメッセージを出力
    printf(StringValueCStr(ruby_errinfo));
  }

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

これでスクリプトでエラーが発生しても、プログラムが落ちることはなくなった。

C++から関数を作成する

では、C++からRuby上に関数を作成してみたい。

C++からRubyのグローバル関数を作成するには、rb_define_global_function()関数を使う。rb_define_global_function()関数の第一引数には関数名、第二引数には実際の処理を行う関数へのポインタ、第三引数には関数の受け取る引数の数を指定する。 では、引数を2つ取り、その引数をprintfで出力する関数を定義してみよう。尚、ここでは関数名をtestにした。

#include <ruby.h>
#include <stdio.h>

// test関数の処理
VALUE func_test(VALUE self, VALUE arg1, VALUE arg2)
{
  // 出力
  printf(StringValueCStr(arg1), StringValueCStr(arg2));

  // 関数の戻り値としてnilを返す
  return Qnil;
}

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();
  ruby_init_loadpath();

  // test関数を定義
  rb_define_global_function("test", reinterpret_cast<VALUE(__cdecl *)(...)>(func_test), 2);

  // スクリプトをファイルから読み込んで実行
  int state;
  rb_load_protect(rb_str_new2("test.rb"), 0, &state);
  if (state)
  {
    // エラーメッセージを出力
    printf(StringValueCStr(ruby_errinfo));
  }

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

次のようなコードを走らせて、実際に関数がうまく機能しているか試してみよう。

test "%s\n", "Hello World!"

既に定義されているクラスや関数をオーバーライドすることもできる。試しにRuby組み込み関数のp関数をオーバーライドして、Win32 APIのMessageBoxを使って出力するようにしてみよう。p関数は引数可変なので、rb_define_global_functionの第三引数に-1を指定し、コールバック関数ではargcargvを使って引数を受け取る。argcに引数の数、argvには引数に渡されたオブジェクトが入る。

#include <ruby.h>
#include <windows.h>

// p関数の処理
VALUE func_p(int argc, VALUE *argv, VALUE self)
{
  // Rubyの文字列として初期化
  VALUE str = rb_str_new("", 0);

  // 引数の数だけループ
  for (int i = 0; i < argc; i++)
  {
    // 間にカンマを挿入
    if (i > 0)
    {
      rb_str_cat(str, ", ", 2);
    }

    // 引数に渡されたオブジェクトを文字列化して変数strに加える
    rb_str_concat(str, rb_inspect(argv[i]));
  }

  // 出力
  MessageBoxA(NULL, StringValueCStr(str), "Test", MB_OK);

  // 関数の戻り値としてnilを返す
  return Qnil;
}

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();
  ruby_init_loadpath();

  // p関数をオーバーライド
  rb_define_global_function("p", reinterpret_cast<VALUE(__cdecl *)(...)>(func_p), -1);

  // スクリプトをファイルから読み込んで実行
  int state;
  rb_load_protect(rb_str_new2("test.rb"), 0, &state);
  if (state)
  {
    // エラーメッセージを出力
    MessageBoxA(NULL, StringValueCStr(ruby_errinfo), "Test", MB_OK);
  }

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

テスト・コードは次のようにしてみた。

p "Hello", "World", "!"

C++からクラスを作成する

せっかくオブジェクト指向のスクリプト言語を使うのだから、ある程度処理のまとまりごとにクラス化していきたいところだろう。C/C++からRuby上にクラスやメソッドなどを定義するAPIには、主に次のものが用意されている。

rb_define_class()
クラスを定義する。
rb_define_class_under()
他のクラスやモジュール内にクラスを定義する。
rb_define_method()
メソッドを定義する。
rb_undef_method()
メソッドを削除する。
rb_define_alias()
エイリアスを定義する。
rb_define_attr()
アクセサを定義する。。
rb_define_protected_method()
protectedなメソッドを定義する。
rb_define_private_method()
privateなメソッドを定義する。
rb_define_singleton_method()
singletonのメソッドを定義する。
rb_singleton_class()
singletonのクラスを定義する。

クラスを定義するrb_define_class()関数は第一引数にクラス名、第二引数に継承するクラスを指定する。メソッドを定義するrb_define_method()関数は第一引数にクラス・オブジェクト(rb_define_class()関数の戻り値)、第二引数にメソッド名、第三引数に実際の処理を行う関数へのポインタ、第四引数にメソッドの引数の数を指定する。

では、コンストラクタに一つの引数を取り、その値を取得するメソッドを持つクラスを定義してみよう。

#include <ruby.h>
#include <stdio.h>

// Testクラス
class Test
{
public:
  Test(const char *name)
  {
    this->name = new char[strlen(name) + 1]();
    strcpy(this->name, name);
  }
  ~Test()
  {
    delete[] this->name;
  }
  char *GetName()
  {
    return this->name;
  }
private:
  char *name;
};

// クラスインスタンスを破棄する処理
void test_free(Test *test)
{
  delete test;
}

// クラスインスタンスを作成する処理
VALUE test_new(VALUE klass, VALUE name)
{
  Test *test = new Test(StringValueCStr(name));

  // クラスオブジェクトにインスタンス解放用の関数とC++上のクラスインスタンスを関連付ける
  // 関連付けたクラスインスタンスはData_Get_Structマクロで取得できる
  VALUE data = Data_Wrap_Struct(klass, 0, test_free, test);

  // 初期化メソッドの引数を配列にする
  VALUE argv[] = { name };

  // 初期化メソッドを呼び出す
  rb_obj_call_init(data, 1, argv);

  return data;
}

// 初期化メソッドの処理
VALUE test_init(VALUE self, VALUE name)
{
  // インスタンス変数@nameに変数nameの内容を保存する
  rb_iv_set(self, "@name", name);

  return self;
}

// 名前を取得するメソッドの処理
VALUE test_name(VALUE self)
{
  // インスタンス変数@nameの値を取得する
  return rb_iv_get(self, "@name");
}

void main()
{
  // Rubyインタプリタの初期化
  ruby_init();
  ruby_init_loadpath();

  // Testクラスを定義
  VALUE test = rb_define_class("Test", rb_cObject);
  rb_define_singleton_method(test, "new", reinterpret_cast<VALUE (__cdecl *)(...)>(test_new), 1);
  rb_define_method(test, "initialize", reinterpret_cast<VALUE (__cdecl *)(...)>(test_init), 1);
  rb_define_method(test, "name", reinterpret_cast<VALUE (__cdecl *)(...)>(test_name), 0);

  // スクリプトをファイルから読み込んで実行
  int state;
  rb_load_protect(rb_str_new2("test.rb"), 0, &state);
  if (state)
  {
    // エラーメッセージを出力
    printf(StringValueCStr(ruby_errinfo));
  }

  // Rubyインタプリタのクリーンアップ
  ruby_cleanup(0);
}

次のようなコードを走らせてテストしてみよう。

t = Test.new("Foo")
p t.name