C++ で Ruby 拡張ライブラリ ( Data_Wrap_Struct とか )

とりあえず以下ダラダラのアウトプットはこちら

gist に置いといた https://gist.github.com/1437782

作りたいもの

  • 僕は神になる。ヒト(クラス)を作ってみるぞ
    • 人とは、名前と年齢を持っている存在である
    • 人とは、名前と年齢を伝える為、ちゃんとご挨拶ができる存在である

作り方とか

  1. C++ で、ヒトクラスを作成する
    • デフォルトコンストラクタ使わない、常に引数 (名前, 年齢) を受けるようにする
  2. C++ 製ヒトクラスを元に Ruby 拡張ライブラリを書く

C++ でヒトクラスを作る

human.hxx
/*
 * human.hxx
 */

#ifndef _INCLUDE_HUMAN_H_
#define _INCLUDE_HUMAN_H_

#include <string>
#include <sstream>

class Human
{
  private:
    static const int _signature = 0x123f3f7c;

  public:
    Human(const char* name, int age);
    virtual ~Human() {}

    bool isLegal() const { return this->_sig == _signature; }

    std::string toString() const;
    std::string greet() const;

  private:
    int _sig;
    std::string _name;
    int _age;
};

#endif // _INCLUDE_HUMAN_H_
human.cxx
/*
 * human.cxx
 */

#include "human.hxx"

Human::Human(const char* name, int age) {
  this->_sig  = _signature;
  this->_name = name;
  this->_age  = age;
}

std::string Human::toString() const {
  std::ostringstream ret;
  ret << "Human [name="
      << this->_name
      << ", age="
      << this->_age
      << "]";
  return ret.str();
}

std::string Human::greet() const {
  std::ostringstream ret;
  ret << "My name is "
      << this->_name
      << ". I'm "
      << this->_age
      << " years old.";
  return ret.str();
}
C++ 製ヒトクラスはこんな感じで使える
#include <iostream>
#include "human.hxx"

void show(Human* p) {
  std::cout << p << std::endl;
  if (p->isLegal()) { // 正しいオブジェクトの場合のみ出力する
    std::cout << "\t" << p->toString() << std::endl
              << "\t" << p->greet()    << std::endl;
  }
}

int main() {
  Human h("hamajyotan", 18); // ごく普通にインスタンス生成
  show(&h);

  Human* p; // 初期化されていないポインタを与える
  show(p);  // この場合、アドレスのみしか出力されない

  p = new Human("foo", 20); // new でインスタンス生成
  show(p);
  delete p;

  return 0;
}

実行結果例。 2回目の show 呼び出しではアドレスしか出力されていない

$ ./a.out
0xbfcc104c
        Human [name=hamajyotan, age=18]
        My name is hamajyotan. I'm 18 years old.
0xbfcc1078
0x8d00028
        Human [name=foo, age=20]
        My name is foo. I'm 20 years old.
C++ 製ヒトクラスのメソッドまとめ
  • コンストラク
    • 引数 2つ、 name と age
  • toString()
    • オブジェクトの文字列表現を返す
  • greet()
    • オブジェクトの挨拶文を返す
  • isLegal()
    • オブジェクトが初期化されている (コンストラクタを経由している)場合にのみ true を返す
    • つまり、初期化されていない等の理由による不正なアドレスでは false を返す

C++ 製ヒトクラスを元に Ruby 拡張ライブラリを書く

humanwrap.cxx
/**
 * humanwrap.cxx
 */

#include <new> // for replacement new
#include "ruby.h"
#include "human.hxx"

/**
 * Ruby 製 Human クラスのインスタンスから
 * C++ 製 Human クラスのインスタンスのために割り当てたメモリ領域のポインタを得ます.
 */
static Human* getHuman(VALUE self) {
  Human* p;
  Data_Get_Struct(self, Human, p);
  return p;
}

/**
 * GC によりメモリが解放される際に呼び出されます.
 *
 * wrap_Human_init で replacement new を使い
 * C++ 製 Human の初期化を実施するため明示的なデストラクタ呼び出しが必要になります。
 * 
 * C++ 製クラスのためにメモリ領域が割り当てられたにも関わらず、
 * #initialize の引数が不正だった場合など、 C++ 製 Human のインスタンスが作成されない経路も存在します
 * この場合ポインタは不正なアドレスを指しているため、デストラクタを呼び出さないようにします。
 */
static void wrap_Human_free(Human* p) {
  if (p->isLegal()) p->~Human();
  ruby_xfree(p);
}

/**
 * 初期化時にメモリを割り当てます.
 */
static VALUE wrap_Human_alloc(VALUE klass) {
  // Human インスタンスのためのメモリ空間を確保し self に関連付けます
  // 更に、 GC が実行された際のハンドラも登録します
  return Data_Wrap_Struct(klass, NULL, wrap_Human_free, ruby_xmalloc(sizeof(Human)));
}

/**
 * 
 */
static VALUE wrap_Human_init(VALUE self, VALUE _name, VALUE _age) {
  // 以下 2行で #initialize に渡された引数の検証を行います
  // 引数が正しくない場合、例外が投げられるために C++ 製 Human クラスは初期化されません。
  // 
  // この場合も GC の際に wrap_Human_free が呼ばれますが、
  // wrap_Human_free に渡されるポインタは初期化されていないために不正なアドレスを指します
  const char* name = StringValuePtr(_name); // 文字列でなければ例外が投げられます
  int age = NUM2INT(_age); // 数値でなければ例外が投げられます

  Human* p = getHuman(self); // メモリ領域を得ます
  new (p) Human(name, age);  // replacement new により既存のメモリ空間にインスタンスを初期化します

  return Qnil; // 使われません、適当な値を返しておきます
}

/**
 * toString() を実行して結果を Ruby の String クラスに変換して返します.
 */
static VALUE wrap_Human_toString(VALUE self) {
  return rb_str_new2(getHuman(self)->toString().c_str());
}

/**
 * greet() を実行して結果を Ruby の String クラスに変換して返します.
 */
static VALUE wrap_Human_greet(VALUE self) {
  return rb_str_new2(getHuman(self)->greet().c_str());
}

/**
 * require 'human' の際に最初に実行されます.
 */
extern "C" void Init_human() {
  // Ruby の Human クラスを定義する
  VALUE c = rb_define_class("Human", rb_cObject);

  rb_define_alloc_func(c, wrap_Human_alloc); // 初期化時にメモリを確保する関数を登録する
  rb_define_private_method(c, "initialize", RUBY_METHOD_FUNC(wrap_Human_init), 2); // #initialize
  rb_define_method(c, "inspect", RUBY_METHOD_FUNC(wrap_Human_toString), 0);        // #inspect
  rb_define_method(c, "to_s", RUBY_METHOD_FUNC(wrap_Human_toString), 0);           // #to_s
  rb_define_method(c, "greet", RUBY_METHOD_FUNC(wrap_Human_greet), 0);             // #greet
}
extconf.rb
require "mkmf"
$libs += " -lstdc++ " # for STL
create_makefile("human")

拡張ライブラリを作成してみる

特定のディレクトリに 4 ファイルを集めて ruby extconf.rb そして make
human.so が出来上がる。

$ ls /home/hamajyotan/tmp/
extconf.rb  human.cxx  human.hxx  humanwrap.cxx
$ ruby extconf.rb
creating Makefile
$ make
g++ -I. -I/usr/local/include/ruby-1.9.1/i686-linux -I/usr/local/include/ruby-1.9.1/ruby/backward -I/usr/local/include/ruby-1.9.1 -I. -D_FILE_OFFSET_BITS=64  -fPIC -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wpointer-arith -Wwrite-strings -Wno-missing-field-initializers -Wno-long-long   -o humanwrap.o -c humanwrap.cxx
g++ -I. -I/usr/local/include/ruby-1.9.1/i686-linux -I/usr/local/include/ruby-1.9.1/ruby/backward -I/usr/local/include/ruby-1.9.1 -I. -D_FILE_OFFSET_BITS=64  -fPIC -O3 -ggdb -Wextra -Wno-unused-parameter -Wno-parentheses -Wpointer-arith -Wwrite-strings -Wno-missing-field-initializers -Wno-long-long   -o human.o -c human.cxx
g++ -shared -o human.so humanwrap.o human.o -L. -L/usr/local/lib -Wl,-R/usr/local/lib -L.  -rdynamic -Wl,-export-dynamic   -lstdc++  -lpthread -lrt -ldl -lcrypt -lm   -lc
$ ls
Makefile  extconf.rb  human.cxx  human.hxx  human.o  human.so  humanwrap.cxx  humanwrap.o
$

使ってみる

human.so の作られたディレクトリでそのまま irb 叩いてみる

irb(main):001:0> require './human'
=> true
irb(main):002:0> h = Human.new 'hamajyotan', 18
=> Human [name=hamajyotan, age=18]
irb(main):003:0> h.greet
=> "My name is hamajyotan. I'm 18 years old."
irb(main):004:0>

(゚∀゚)キタコレ!!