2
1

RubyでC++のバインディングを作成するRiceのドキュメントをChatGPTで日本語に翻訳しただけの記事

Last updated at Posted at 2024-08-24

RubyでC++のバインディングを作成するために、Riceの公式ドキュメントをChatGPTでにほんごに翻訳しました。

はじめに

Riceは、C++ 17対応のヘッダオンリーライブラリで、2つの目的を持っています。まず、既存のC++ライブラリに対するRubyのバインディングを簡単に作成することができます。次に、RubyのC APIに対してオブジェクト指向インターフェースを提供し、Rubyを埋め込んだり、C++でRubyの拡張機能を書くことを簡単にします。

Riceは、Boost.Pythonpybind11 と似ており、C++とインターフェースを作る際に必要な定型コードを最小限に抑えます。これにより、RubyオブジェクトをC++に、またその逆に変換するための型情報を自動で判定します。

Riceが提供するもの:

  • クラスのラップや定義のためのシンプルなC++ベースの構文
  • C++とRuby間の自動型変換
  • C++とRuby間の自動例外処理
  • ガベージコレクションを扱うためのスマートポインタ
  • ほとんどの組み込み型に対するサポート

バージョンの違い(3.xと4.x以降)

このドキュメントとmasterブランチは、ヘッダオンリー版のRice 4.x以降用です。3.x系のリリースについては3.xブランチのドキュメントとコードを使用してください。

ライブラリをRice 3から4にアップグレードするには、Migratingを参照してください。

3.x系のRiceのドキュメントは、こちらで閲覧できます。

プロジェクトの詳細

インストール方法

gem install rice

Riceはヘッダオンリーのライブラリなので、別途ビルドする必要はありません。代わりに、C++プロジェクトで#includeしてください。RiceはC++17以降を必要とし、Windows(MSVCとMingw64)、MacOS(Xcode/clang)、Linux(g++)でテストされています。

チュートリアル

はじめに

Riceを使った拡張の作成は、C APIを使った拡張の作成と非常に似ています。

まず最初に、Riceのgemをインストールします:

gem install rice

次に、extconf.rbファイルを作成します:

require 'mkmf-rice'
create_makefile('test')

ここで、mkmfの代わりにmkmf-riceを使用している点に注意してください。これにより、拡張機能が標準のC++ライブラリとリンクされ、Riceのヘッダーファイルにアクセスできるようになります。

Note: 上級者向け - mkmf-riceの代わりにCMakeなどの独自のビルドシステムを使用することもできます。その場合、rice.hppをGitHubからダウンロードして、ソースツリーに直接含めることをお勧めします。

次に、拡張を作成し、それをtest.cppに保存します:

extern "C"
void Init_test()
{
}

上記のextern "C"行に注意してください。これにより、コンパイラはInit_test関数がCリンクと呼び出し規約を持つべきであることを認識します。これにより、名前のマングリングがオフになり、Rubyインタプリタが関数を見つけられるようになります(RubyはCで書かれているためです)。

今のところ、拡張には何も入っていないので、特に役には立ちません。次のステップは、メソッドを追加できるようにクラスを定義することです。

クラスの定義

Riceでクラスを定義するのは簡単です:

#include <rice/rice.hpp>

using namespace Rice;

extern "C"
void Init_test()
{
  Class rb_cTest = define_class("Test");
}

これにより、Objectを継承したTestというクラスが作成されます。別のクラスを継承したい場合は、2番目のパラメータを使用します:

#include <rice/rice.hpp>

using namespace Rice;

extern "C"
void Init_test()
{
  Class rb_cMySocket = define_class("MySocket", rb_cIO);
}

クラス名にrb_cというプレフィックスが付いている点に注意してください。これは、Rubyインタプリタおよび多くの拡張で使用される命名規則であり、これはクラスであり他のタイプのオブジェクトではないことを示しています。よく使用されるその他の命名規則は次のとおりです:

  • rb_c:クラスの変数名のプレフィックス
  • rb_m:モジュールの変数名のプレフィックス
  • rb_e:例外タイプの変数名のプレフィックス
  • rb_:Ruby C APIの関数のプレフィックス
  • rb_f_:Rubyオブジェクトを引数として取るAPI関数とC引数タイプを取る関数を区別するための関数プレフィックス
  • rb_*_s_:関数がシングルトン関数であることを示すプレフィックス
  • *_m:関数が可変数の引数を取ることを示すサフィックス

また、「ruby.h」を直接インクルードしないことにも注意してください。Riceには「ruby.h」をラップするヘッダーがあり、プラットフォームやRubyバージョン間の互換性の問題を処理します。必ず「ruby.h」をインクルードする前にRiceのヘッダーをインクルードしてください。

メソッドの定義

次に、クラスにメソッドを追加しましょう:

#include <rice/rice.hpp>

using namespace Rice;

Object test_hello(Object /* self */)
{
  String str("hello, world");
  return str;
}

extern "C"
void Init_test()
{
  Class rb_cTest =
    define_class("Test")
    .define_method("hello", &test_hello);
}

ここでは、文字列「Hello, World」を返すメソッドTest#helloを追加しています。このメソッドはselfを暗黙のパラメータとして取りますが、使用しないため、コンパイラの警告を防ぐためにコメントアウトしています。

クラスに#initializeメソッドを追加することもできます:

#include <rice/rice.hpp>
#include <rice/stl.hpp>

using namespace Rice;

Object test_initialize(Object self)
{
  self.iv_set("@foo", 42);
}

Object test_hello(Object /* self */)
{
  String str("hello, world");
  return str;
}

extern "C"
void Init_test()
{
  Class rb_cTest =
    define_class("Test")
    .define_method("initialize", &test_initialize)
    .define_method("hello", &test_hello);
}

initializeメソッドはインスタンス変数@fooに値42を設定します。この数値は代入前に自動的にFixnumに変換されます。

Classオブジェクトに対するメソッド呼び出しをチェインしている点に注意してください。ModuleClassのほとんどのメンバー関数はselfへの参照を返すので、定義したいメソッドの数だけ呼び出しをチェインできます。

Note: コンパイラが「一致するオーバーロードされた関数が見つかりません」とエラーを出し、その後に「テンプレート引数'Function_T'を推測できません」と表示された場合、それはオーバーロードされたC++関数またはメソッドを使用していることを意味します。この場合、:ref:overloaded_methodsセクションに記載されているようにRiceに少し助けを提供する必要があります。

ラムダを使ったメソッドの定義

C++ラムダを使ってメソッドを定義することも可能です。define_methodと同様に、ラムダはselfを暗黙のパラメータとして取ります:

Class rb_cTest =
  define_class("Test")
  .define_method("hello", [](Object& object) {
    return test_hello;
  });

selfをコピーしたくないので、参照としてselfを渡している点に注意してください!

関数の定義

Rubyクラスにメソッドを追加するには、define_functionを使用することも可能です。違いは、暗黙のselfパラメータが渡されないことです。再び、関数ポインタやラムダを使用することができます:

void some_function()
{
  // do something
}

extern "C"
void Init_test()
{
  Class rb_cTest =
    define_class("Test")
    .define_function("some_function", &some_function)
    .define_function("some_function_lambda", []() {
      return some_function();
    });
}

C++タイプのラッピング

RubyクラスをC++スタイルで定義できるのは便利ですが、Riceの本当の強みは既に定義されたC++タイプをラップすることにあります。

次のC++クラスをラップしたいと仮定しましょう:

class Test
{
public:
  static std::string static_hello();
public:
  Test();
  std::string hello();
};

これをラップするために:

#include <rice/rice.hpp>
#include <rice/stl.hpp>

using namespace Rice;

extern "C"
void Init_test()
{
  Data_Type<Test> rb_cTest =
    define_class<Test>("Test")
    .define_constructor(Constructor<Test>())
    .define_function("static_hello", &Test::static_hello)
    .define_method("hello", &Test::hello);
}

この例では、Classの代わりにData_Type<>を使用し、非テンプレートバージョンのdefine_class()の代わりにテンプレートバージョンを使用しています。これにより、Riceライブラリ内でRubyクラスTestとC++クラスTestの間にバインディングが作成されます。

次に、C++静的メンバ関数によって実装される関数static_helloを定義します。静的関数は特定のオブジェクトに結びついていないため、selfパラメータはありません。そのため、define_methodの代わりに

define_functionを使用します。

最後に、C++メンバ関数によって実装されるメソッドhelloを定義します。Rubyがこの関数を呼び出すと、Riceは呼び出しを正しいC++ Testインスタンスに指示するため、暗黙のselfパラメータは渡されません。

属性の定義

C++構造体やクラスには、データを格納するためのパブリックメンバ変数がしばしばあります。Riceは、これらのメンバ変数をdefine_attrを使用して簡単にラップできます:

struct MyStruct
{
  int readOnly = 0;
  int writeOnly = 0;
  int readWrite = 0;
};

Data_Type<MyStruct> rb_cMyStruct =
  define_class<MyStruct>("MyStruct")
  .define_constructor(Constructor<MyStruct>())
  .define_attr("read_only", &MyStruct::readOnly, Rice::AttrAccess::Read)
  .define_attr("write_only", &MyStruct::writeOnly, Rice::AttrAccess::Write)
  .define_attr("read_write", &MyStruct::readWrite);
}

読み取り専用属性を定義するためのRice::AttrAccess::Readおよび書き込み専用属性を定義するためのRice::AttrAccess::Writeの使用に注目してください。AttrAccessの値を指定しない場合、Riceは属性を読み取りおよび書き込み可能にします。

これらの属性は、Rubyで次のようにしてアクセスできます:

my_struct = MyStruct.new
a = my_struct.read_only
my_struct.write_only = 5
my_struct.read_write = 10
b = my_struct.read_write

同様に、静的メンバをdefine_singleton_attrを使用してラップすることができます:

struct MyStruct
{
  static int readOnly = 0;
  static int writeOnly = 0;
  static int readWrite = 0;
};

Data_Type<MyStruct> rb_cMyStruct =
  define_class<MyStruct>("MyStruct")
  .define_constructor(Constructor<MyStruct>())
  .define_singleton_attr("read_only", &MyStruct::readOnly, Rice::AttrAccess::Read)
  .define_singleton_attr("write_only", &MyStruct::writeOnly, Rice::AttrAccess::Write)
  .define_singleton_attr("read_write", &MyStruct::readWrite);
}

これらの属性は、Rubyで次のようにしてアクセスできます:

a = MyStruct.read_only
MyStruct.write_only = 5
MyStruct.read_write = 10
b = MyStruct.read_write

型変換

RiceはほとんどのRubyおよびC++オブジェクト間の変換を行うことができます。例として、再び以下のクラスを見てみましょう:

class Test
{
public:
  Test();
  std::string hello();
};

クラスを記述するときに、hello()によって返されるstd::stringをRubyタイプに変換するコードを一行も書いていません。それでも変換は機能し、以下のコードを記述すると:

test = Test.new
puts test.hello

期待どおりの結果が得られます。

Riceには多くのC++タイプのデフォルトの特殊化が含まれています。独自の変換を定義するには、Type Conversionsのセクションを参照してください。

ラップされたC++タイプの変換

再びTestクラスのラッパーを見てみましょう:

extern "C"
void Init_test()
{
  Data_Type<Test> rb_cTest =
    define_class<Test>("Test")
    .define_constructor(Constructor<Test>())
    .define_method("hello", &Test::hello);
}

define_class<Test>を呼び出したとき、Riceは新しいクラスを自動的に型システムに登録したので、以下の呼び出し:

Data_Object<Foo> obj(new Foo);
Foo * f = detail::From_Ruby<Foo *>::convert(obj);
Foo const * f = detail::From_Ruby<Foo const *>::convert(obj);

が期待通りに動作します。

Data_ObjectクラスはC拡張のTypedData_Wrap_StructおよびTypedData_Get_Structマクロのラッパーです。これはData_Typeに割り当てられた任意のクラスをラップまたはアンラップするために使用できます。これはObjectから継承しているため、Object上で呼び出せる任意のメンバー関数をData_Object上でも呼び出すことができます:

Object object_id = obj.call("object_id");
std::cout << object_id << std::endl;

Data_Objectクラスは新しく作成されたオブジェクトをラップするために使用できます:

Data_Object<Foo> foo(new Foo);

または既に作成されたオブジェクトをアンラップするために使用できます:

VALUE obj = ...;
Data_Object<Foo> foo(obj);

Data_Objectはスマートポインタのように機能します:

Data_Object<Foo> foo(obj);
foo->foo();
std::cout << *foo << std::endl;

VALUEObjectと同様に、Data_Objectに格納されたデータはData_Objectがスタックにある限り、ガベージコレクタによってマークされます。

例外

一般的にRiceは例外を自動的に処理します。たとえば、例のクラスのメンバ関数が例外をスローする可能性があるとしましょう:

class MyException
  : public std::exception
{
};

class Test
{
public:
  Test();
  std::string hello();
  void error();
};

extern "C"
void Init_test()
{
  Data_Type<Test> rb_cTest =
    define_class<Test>("Test")
    .define_constructor(Constructor<Test>())
    .define_method("hello", &Test::hello)
    .define_method("error", &Test::error);
}

この関数をRubyから呼び出すと、C++は例外を発生させます。Riceは自動的にそれをキャッチして、Rubyの例外に変換します:

test = Test.new
begin
  test.error()
rescue => e
  ..
end

例外についての詳細は、:ref:Exceptionsセクションを参照してください。

関数とメソッド

チュートリアルでは、C++の関数、静的メンバ関数、およびメンバ関数のラップについて触れました。ここでは、さらに詳しく説明します。

デフォルト引数

最初のC++クラスの例に戻り、hello()メソッドに追加の引数を追加し、そのうちの1つにデフォルト値を設定してみましょう:

class Test
{
public:
  Test();
  std::string hello(std::string first, std::string second = "world");
};

テンプレートを通してデフォルトのパラメータ値を利用することはできないため、Riceにデフォルト値を知らせるためにはRice::Argを使用する必要があります:

#include <rice/rice.hpp>

using namespace Rice;

extern "C"
void Init_test()
{
  Data_Type<Test> rb_cTest =
    define_class<Test>("Test")
    .define_constructor(Constructor<Test>())
    .define_method("hello",
       &Test::hello,
       Arg("hello"), Arg("second") = "world"
    );
}

構文はArg(nameOfParameter)[ = defaultValue]です。ここでパラメータの名前は重要ではありません(可読性のためです)が、operator=を使用して設定される値はパラメータの型と一致する必要があります。そのため、デフォルト値を明示的にキャストする必要があるかもしれません。

.define_method("hello",
   &Test::hello,
   Arg("hello"), Arg("second") = (std::string)"world"
);

これらのRice::Argオブジェクトは正しい位置に配置する必要があります。したがって、2番目の引数にデフォルト値がある場合、2つのArgオブジェクトが必要です。

これでRubyはデフォルトの引数について認識し、このラッパーは期待通りに使用できます:

t = Test.new
t.hello("hello")
t.hello("goodnight", "moon")

これはコンストラクタにも適用されます:

.define_constructor(Constructor<SomeClass, int, int>(),
    Arg("arg1") = 1, Arg("otherArg") = 12);

Return

Argクラスと同様に、RiceにはC++から返される値をどのように処理するかを指示するためのReturnクラスもあります。これは特にメモリ管理を正しく行うために重要です(詳細は:ref:cpp_to_rubyを参照)。

RubyのVALUE型(Rubyオブジェクトを表す)を処理する際にも役立ちます。ほとんどの場合、RiceはVALUEインスタンスを自動的に処理しますが、ネイティブメソッドがVALUE引数を取る場合やVALUEインスタンスを返す場合はRiceにその旨を伝える必要があります。

これは、VALUEがunsigned long longのtypedefであるためです。実際にはRubyオブジェクトへのポインタですが、RiceにとってはRubyの数値として変換すべき整数に過ぎません。そのため、メソッドがVALUEパラメータを取る場合、RiceはそれをC++のunsigned long long値に変換してしまいます。同様に、メソッドがVALUEを返す場合、Riceは単にそれをRubyオブジェクトとして返すのではなく、数値Rubyオブジェクトに変換してしまいます。

この不正な変換を避けるために、ArgおよびReturnクラスのsetValue()メソッドを使用します。例:

VALUE some_function(VALUE ary)
{
  VALUE new_ary = rb_ary_dup(ary);
  rb_ary_push(new_ary, Qtrue);
  return new_ary;
}

define_global_function("some_function", &some_function, Arg("ary").setValue(), Return.setValue());

ArgReturnオブジェクトを任意の順序で混在させることができます。例えば、次のコードも動作します:

define_global_function("some_function", &some_function, Return.setValue(), Arg("ary").setValue());

selfを返す

メソッドがselfを返す場合、つまり関数呼び出しのレシーバであった同じC++オブジェクトを返す場合、Riceは同じRubyオブジェクトが返されることを保証します。selfを返すことはRubyで一般的なパターンです。

例:

a = Array.new
a << 1 << 2 << 3 << 4

上記のコードは、<<メソッドがArray aを返すために機能します。この動作を模倣するために、C++クラスをラップするときにラムダを使用できます。例えば、Riceはstd::vectorを次のようにラップします:

define_vector<std::vector<int32_t>>().
define_method("<<", [](std::vector<int32_t>& self, int32_t value) -> std::vector<int32_t>&
{
  self.push_back(value);
  return self;  // 呼び出しのチェインを可能にする
});

ラムダの戻り値の型std::vector<int32_t>&に注意してください。戻り値の型が指定されていない場合、デフォルトでラムダは値で返します。それにより、std::vectorのコピーコンストラクタが呼び出され、2つstd::vector<int32_t>インスタンスと2つのRubyオブジェクトが生成されます。これは望ましくありません。

オーバーロードされたメソッド

C++はオーバーロードされたメソッドや関数をサポートしています。オーバーロードされた関数をラップしようとすると、C++コンパイラは「no matching overloaded function found」(一致するオーバーロードされた関数が見つかりません)というエラーメッセージを出します。

例えば、次のC++クラスを考えてみましょう:

class Container
{
public:
  size_t capacity()
  {
    return this->capacity_;
  }

  void capacity(size_t value)
  {
    this->capacity_ = value;
  }

private:
  size_t capacity_;
};

このクラスを次のようにラップしようとすると、コンパイラエラーが発生します:

Class c = define_class<Container>("Container")
  .define_constructor(Constructor<Container>())
  .define_method("capacity", &Container::capacity)
  .define_method("capacity=", &Container::capacity);

代わりに、どのオーバーロードされたメソッドを使用するかをC++コンパイラに伝える必要があります。以下に説明するいくつかの方法があります。

テンプレートパラメータ

define_methodはテンプレート関数です。したがって、一つの解決策は次のように呼び出そうとしているメソッドを指定することです:

Class c = define_class<Container>("Container")
  .define_constructor(Constructor<Container>())
  .define_method<size_t(Container::*)()>("capacity", &Container::capacity)
  .define_method<void(Container::*)(size_t)>("capacity=", &Container::capacity);

size_t(Container::*)()void(Container::*)(size_t) はC++のメンバ関数へのポインタです。

usingの使用

もう一つの解決策は、C++のusing機能を使用する方法です:

using Getter_T = size_t(Container::*)();
using Setter_T = void(Container::*)(size_t);

Class c = define_class<Container>("Container")
  .define_constructor(Constructor<Container>())
  .define_method<Getter_T>("capacity", &Container::capacity)
  .define_method<Setter_T>("capacity=", &Container::capacity);

または次のように書くこともできます:

using Getter_T = size_t(Container::*)();
using Setter_T = void(Container::*)(size_t);

Class c = define_class<Container>("Container")
  .define_constructor(Constructor<Container>())
  .define_method("capacity", (Getter_T)&Container::capacity)
  .define_method("capacity=", (Setter_T)&Container::capacity);

typedefの使用

古いスタイルを好み、やや難解な構文を使いたい場合は、typedefを使うこともできます:

extern "C"
void Init_Container()
{
    typedef size_t(Container::* Getter_T)();
    typedef void (Container::* Setter_T)(size_t);

    Class c = define_class<Container>("Container")
      .define_constructor(Constructor<Container>())
      .define_method("capacity", (Getter_T)&Container::capacity)
      .define_method("capacity=", (Setter_T)&Container::capacity);
}

Rubyでの例

このクラスをラップした後は、Rubyで簡単に使用できます:

container = Container.new
container.capacity = 6
puts container.capacity

表示される結果は7になります。

イテレータ

C++のイテレータは、コンテナに格納されている要素を順に操作するために使用されます。C++のイテレータは外部イテレータであり、開始イテレータと終了イテレータのペアで機能します。例えば、std::vectorにはbegin/endcbegin/cendrbegin/rendなどがあります。

Enumerableのサポート(内部イテレータ)

Riceを使用すると、C++クラスに簡単にEnumerableモジュールのサポートを追加できます。Enumerableモジュールは、Rubyクラスがeachインスタンスメソッドを定義している限り、そのクラスに内部イテレータのサポートを追加します。

Riceはdefine_iteratorメソッドを使ってこれを簡単にします。define_iteratoreachメソッドを作成し、さらにEnumerableモジュールをミックスインします。

例えば、std::vectorの簡単なラッパーを作成してみましょう(完全なサポートについては、:ref:std_vectorを参照してください)。

#include <vector>
#include <rice/rice.hpp>

using namespace Rice;

extern "C"
void Init_IntVector()
{
  using IntVector = std::vector<int>;
  define_class<IntVector>("IntVector")
    .define_constructor(Constructor<IntVector>())
    .define_method<void(IntVector::*)(const IntVector::value_type&)>("push_back", &IntVector::push_back)
    .define_iterator<IntVector::iterator(IntVector::*)()>(&IntVector::begin, &IntVector::end);
}

ここでは、push_backbeginendのどのオーバーロードバージョンを公開するかをRiceに伝える必要があります。詳細については、:ref:overloaded_methodsを参照してください。

イテレータが定義されると、次のような標準のRubyコードを書くことができます:

intVector = IntVector.new
intVector.push_back(1)
intVector.push_back(2)
intVector.push_back(3)

result = intVector.map do |value|
  value * 2
end

ここで、result[2, 4, 6]になります。

また、std::vectorの逆方向イテレータをRubyにreachというメソッド名で公開したいとします。これは、define_iterator呼び出しに3番目のパラメータを追加することで行います。この場合、reachに設定します:

extern "C"
void Init_IntVector()
{
  define_class<IntVector>("IntVector")
    .define_iterator<IntVector::reverse_iterator(IntVector::*)()>(&IntVector::rbegin, &IntVector::rend, "reach");
}

Rubyの例は次のようになります:

intVector = IntVector.new
intVector.push_back(1)
intVector.push_back(2)
intVector.push_back(3)

result = Array.new
intVector.reach do |value|
  result << value * 2
end

ここで、result[6, 4, 2]になります。

Enumeratorのサポート(外部イテレータ)

RubyはEnumeratorクラスを使用して外部イテレータをサポートしています。define_iteratorメソッドは、Enumeratorのサポートを自動的に追加します。

Enumeratorは、イテレータメソッドをブロックなしで呼び出すことによって作成できます。これは、Array#eachなどのメソッドをブロックなしで呼び出す方法と同じです。例:

intVector = IntVector.new
intVector.push_back(1)
intVector.push_back(2)
intVector.push_back(3)

# Enumeratorを取得
enumerator = intVector.each

# 使用例
enumerator.map do |i|
  i * 2
end

Enumerators

Rubyはあまり頻繁には使用されませんが、内部および外部の反復処理を可能にする列挙子(Enumerator)をサポートしています。列挙子を作成する最も簡単な方法は、ブロックを渡さずに列挙可能なメソッドを呼び出すことです。例えば:

a = [1, 2, 3, 4, 5]

# 一般的な反復処理
a.each do |i|
  puts i
end

# 列挙子を取得
enumerator = a.each

# 使用方法
enumerator.map { |i| i * 2 }

Riceは、:ref:std_vector, :ref:std_map, および :ref:std_unordered_mapのようなSTLコンテナのための列挙子を返すためのビルトインサポートを提供しています。

列挙子を実装するのは難しく、実際には多くのRiceの機能が必要です。ここでは、std::vectorに対する列挙子のサポートがどのように実装されているかを見てみましょう。

Implementation

まず、コードを見てみましょう:

define_method("each", [](T& vector) -> const std::variant<std::reference_wrapper<T>, Object>
{
  if (!rb_block_given_p())
  {
    auto rb_size_function = [](VALUE recv, VALUE argv, VALUE eobj) -> VALUE
    {
      // 上記のベクターをキャプチャできないので(そうするとこのラムダをrb_enumeratorize_with_sizeに送ることができなくなる)
      // recvから抽出します
      T* receiver = Data_Object<T>::from_ruby(recv);
      return detail::To_Ruby<size_t>().convert(receiver->size());
    };

    return rb_enumeratorize_with_size(detail::selfThread, Identifier("each").to_sym(), 0, nullptr, rb_size_function);
  }

  for (Value_T& item : vector)
  {
    VALUE element = detail::To_Ruby<Value_T>().convert(item);
    detail::protect(rb_yield, element);
  }

  return std::ref(vector);
});

以下の各セクションで詳細を見ていきます。

Method Signature

まず、RiceはRubyのenumerableモジュールをサポートするためにeachメソッドを定義します。そのシグネチャは次のようになります:

define_method("each", [](T& vector) -> const std::variant<std::reference_wrapper<T>, Object>

std::vectorにはeachメソッドがないため、Riceは代わりにベクターと対話するラムダ関数を作成します。ベクターはコピーを避けるために参照T&で渡されます。

さらに興味深いのは、戻り値の型がstd::variantであることです。これは、メソッドがRubyの列挙子またはベクターのいずれかを返す必要があるためです。

最初のケースでは、ベクターを返すことはC++のメンバ関数からthisを返すのと同じです。またはRuby関数からselfを返すのと同様です。これにより、例えばvector.a.bのようにメソッドを連鎖して使用することが可能になります。

ベクターの参照を返さなければならず、コピーを返してはいけません。コピーを返すと、新しいRubyオブジェクトが作成されてしまい、メモリが無駄になるだけでなく、selfがもはやselfでなくなり、非常に予想外の結果になります。しかし、std::variantは参照を含むことができないため、返す必要があるのはstd::reference_wrapper<T>です。

2番目のケースでは、新しいRubyの列挙子を返したいので、その型はVALUEです。しかし、VALUEを直接返すことはできません。なぜならRiceはそれをunsigned long long(実際それが本質です)として解釈してしまうからです。代わりに、Rice::Objectを返します。詳細については:ref:returnクラスを参照してください。

Creating an Enumerator

次に、列挙子を返すコードを見てみましょう:

if (!rb_block_given_p())
{
  auto rb_size_function = [](VALUE recv, VALUE argv, VALUE eobj) -> VALUE
  {
    // 上記のベクターをキャプチャできないので(そうするとこのラムダをrb_enumeratorize_with_sizeに送ることができなくなる)
    // recvから抽出します
    T* receiver = Data_Object<T>::from_ruby(recv);
    return detail::To_Ruby<size_t>().convert(receiver->size());
  };

  return rb_enumeratorize_with_size(detail::selfThread, Identifier("each").to_sym(), 0, nullptr, rb_size_function);
}

ユーザーがブロックを提供しない場合、メソッドは列挙子を返すべきです。列挙子は次のように作成されます:

return rb_enumeratorize_with_size(detail::selfThread, Identifier("each").to_sym(), 0, nullptr, rb_size_function);

rb_enumeratorize_with_sizeの最初のパラメータはC++インスタンスではなくRubyインスタンスを必要とすることに注意してください。正しいRubyインスタンスは、C++インスタンスをラップしているもので、スレッドローカル変数selfThreadに保存されています。

Supporting Enumerator Size

rb_enumeratorize_with_size呼び出しには、列挙されるオブジェクトのサイズを返す関数へのオプションのポインタが含まれています。この場合、ベクターのサイズです。それは別のラムダ関数として実装されています:

auto rb_size_function = [](VALUE recv, VALUE argv, VALUE eobj) -> VALUE
{
  // 上記のベクターをキャプチャできないので(そうするとこのラムダをrb_enumeratorize_with_sizeに送ることができなくなる)
  // recvから抽出します
  T* receiver = Data_Object<T>::from_ruby(recv);
  return detail::To_Ruby<size_t>().convert(receiver->size());
};

このラムダはCコードに送られているため、ローカル変数をキャプチャできません。そのため、T& vectorパラメータに直接アクセスできません。代わりに、ベクターをラップしているRubyオブジェクトから抽出する必要があります:

T* receiver = Data_Object<T>::from_ruby(recv);

次に、ベクターのサイズを決定し、それをRubyオブジェクトとして返す必要があります:

return detail::To_Ruby<size_t>().convert(receiver->size());

Yielding to a Block

最後に、はるかに一般的なユースケースである、ブロックに値を引き渡すことについて説明します:

for (Value_T& item : vector)
{
  VALUE element = detail::To_Ruby<Value_T>().convert(item);
  detail::protect(rb_yield, element);
}

このコードは非常にシンプルです。ベクター内の各アイテムを参照によって反復処理し(コピーなし!)、それをRubyオブジェクトでラップし、ブロックに返します。rb_yieldの呼び出しは、Rubyが例外を発生させる場合に備えてdetail::protectを介して行われます。

Returning Self

最後に、メソッドチェーンを可能にするためにselfを返すことがRubyでは一般的な慣行です。この場合のselfはベクターをラップしているRubyオブジェクトです。ベクターへの参照を返すことによって、Riceはそれを元のRubyオブジェクトにマッピングするのに十分賢いです。

return std::ref(vector);

上述したように、戻り値のバリアントに含めるために、ベクターをstd::reference_wrapperに入れる必要があります。

メモリ管理

C++ APIをラップする際に最も難しい部分は、C++とRuby間で共有されるメモリを正しく管理することです。これを正しく行わないと、プログラムは必ずクラッシュします。正しく行うための鍵は、各メモリ片の所有者が誰であるかを明確にすることです。

Riceはネイティブタイプをビルトインタイプと外部タイプに分けます。ビルトインタイプはC++とRubyの間でコピーされるのに対し、外部タイプはラップされます。ビルトインタイプに関する追加情報については、:doc:Type Conversions <type_conversions>セクションを参照してください。

このセクションの残りでは、外部タイプのメモリ管理について説明します。

C++からRubyへの移行

デフォルトでは、RiceはRubyに渡されたC++インスタンスがC++によって引き続き所有されると仮定します。したがって、所有権の移転はありません。この場合、所有権の移転は次のルールに従います:

メソッドの戻り値の型 (T) C++からRubyへ クリーンアップ
値 (T) コピーコンストラクタ Rubyがコピーを解放し、C++が元のオブジェクトを解放
参照 (T&) コピーなし C++がC++インスタンスを解放
ポインタ (T*) コピーなし C++がC++インスタンスを解放

しかし、多くのAPIでは、返された値の所有権を呼び出し元に移転します。この場合、Rubyがその値の所有権を引き継ぎます。この場合の所有権の移転は次のルールに従います:

メソッドの戻り値の型 (T) C++からRubyへ クリーンアップ
値 (T) コピーコンストラクタ Rubyがコピーを解放し、C++が元のオブジェクトを解放
参照 (T&) ムーブコンストラクタ RubyがC++インスタンスを解放
ポインタ (T*) コピーなし RubyがC++インスタンスを解放

所有権の移転

Rubyが返された値の所有権を取得すべきことをRiceに伝えるには、:ref:returnを使用します。例を見てみましょう:

class MyClass
{
}

class Factory
{
public:
  static MyClass* create()
  {
    return new MyClass();
  }
}

extern "C"
void Init_test()
{
  Data_Type<MyClass> rb_cMyClass = define_class<MyClass>("MyClass");

  Data_Type<Factory> rb_cFactory = define_class<Factory>("Factory")
      .define_function("create", &Factory::create); // 間違い、メモリリークを引き起こす
}

RubyからFactory#createが呼び出されるたびに、MyClassの新しいC++インスタンスが作成されます。Riceのデフォルトのルールを使用すると、これによりメモリリークが発生します。なぜなら、それらのインスタンスは解放されないからです。

1_000.times do
  my_class = Factory.create
end

これを修正するには、返されたインスタンスの所有権を取得するようにRiceに指示する必要があります:

define_function("create", &Factory::create, Return().takeOwnership());

ここでReturn().takeOwnership()が追加されていることに注意してください。これにより、Returnクラスのインスタンスが作成され、C++から返されたインスタンスの所有権を取得するように指示されます。

RubyからC++への移行

時には、あるRubyオブジェクトのライフタイムを別のオブジェクトに結びつける必要があります。これはコンテナでよく発生します。例えば、ListenerListenerContainerクラスを考えてみましょう。

class Listener
{
};

class ListenerContainer
{
  public:
    void addListener(Listener* listener)
    {
      mListeners.push_back(listener);
    }

    int process()
    {
      for(const Listener& listener : mListeners)
      {
      }
    }

  private:
    std::vector<Listener*> mListeners;
};

これらのクラスがRiceでラップされていると仮定すると、次のコードはクラッシュします:

@handler = ListenerContainer.new
@handler.add_listener(Listener.new)
GC.start
@handler.process # クラッシュ!!!

RubyのガベージコレクタはListener.newオブジェクトが孤立していることに気付き、それを解放します。結果として、基盤となるC++のListenerオブジェクトも解放され、processが呼び出されるとクラッシュします。

これを防ぐために、Rubyのリスナーインスタンスのライフタイムをコンテナに結びつけたいと思います。これは引数リストでkeepAlive()を呼び出すことで行います:

define_class<ListenerContainer>("ListenerContainer")
  .define_method("add_listener", &ListenerContainer::addListener, Arg("listener").keepAlive())

この変更により、リスナーがコンテナに追加されると、コンテナはそれへの参照を保持し、rb_gc_markを呼び出してそれを保持します。これはArrayやHashなどのRubyのコレクションクラスが行うのと全く同じことです。Listenerオブジェクトはコンテナ自体がスコープ外になるまで解放されません。

別の例として、返されたオブジェクトが元のオブジェクトに依存している場合があります。例えば:

class Column;

class Database
{
public:
  Database()
  {
    // データベースに接続
  }

  ~Database()
  {
    // データベースから切断
  }

  Column getColumn(uint32_t index)
  {
     return Column(*this, index);
  }

  std::string lookupName(uint32_t index)
  {
    return some_name;
  }
};

class Column
{
public:
  Column(Database& database, uint32_t index): database_(database), index_(index)
  {
  }

  std::string getName()
  {
    return this->database_.lookupName(this->index_);
  }

private:
  Database& database_;
  uint32_t index_;
};

これらのクラスがRiceでラップされていると仮定すると、次のRubyコードはクラッシュします:

def get_column(column_index)
  database = Database.new(...)
  column = database.get_column(column_index)
end

column = get_column(0)
puts column.name

問題は、get_columnで作成されたDatabaseクラスのインスタンスが、メソッドの戻り時にガベージコレクションされる可能性が高いことです。その結果、Column#nameが呼び出されると、もはや有効でないデータベースオブジェクトへの参照が残っていることになります。

このコードは、プログラム全体でデータベースオブジェクトが存続するように書き直すこともできますが、代わりにRiceにDatabaseオブジェクトのライフタイムをColumnオブジェクトに結びつけるように指示することもできます。これにより、Columnが解放されるまでDatabaseも解放されません:

define_class<Database>("Database")
  .define_method("get_column", &Database::getColumn, Return().keepAlive())

Return().keepAlive()は外部タイプでのみ動作します。ビルトインタイプでこれを使用しようとすると、実行時に例外が発生します。

C++からRubyオブジェクトを参照する

C++からRubyオブジェクトを参照する場合、それらが早期にガベージコレクションされないようにRubyに知らせる必要があります。

これを行う方法はいくつかあります:

スタック

スタックに格納されているVALUEやObjectを操作する場合、Rubyのガベージコレクタはそれらを自動的に見つけようとします。しかし、最適化コンパイラがそれを妨げる可能性があります。そのため、Rubyの[RB_GC_GUARD](https://docs.ruby-lang.org/en/3.2

/extension_rdoc.html#label-Appendix+E.+RB_GC_GUARD+to+protect+from+premature+GC)マクロを使用する必要があるかもしれません。

ヒープ

ヒープ上にオブジェクトを割り当てる場合、またはそれがヒープ上に割り当てられるオブジェクトのメンバである場合、Rice::Address_Registration_Guardを使用してそのオブジェクトをガベージコレクタに登録してください。

メンバ変数

Rubyオブジェクトを参照するクラスや構造体を作成する場合、カスタムのruby_mark関数を実装する必要があります:

class MyClass
{
  VALUE value_;
};

namespace Rice
{
  template<>
  ruby_mark(const MyClass* myClass)
  {
    rb_gc_mark(myClass->value_);
  }
}

Data_Type<MyClass> class = define_class<MyClass>("MyClass")
          .define_constructor(Constructor<MyClass>());

例外処理

Riceは自動的に例外を処理します。これにより、C++の例外がRubyコードに伝播せず、Rubyの例外がC++に伝播しないようにします。もしこれが起こると、プログラムはクラッシュします。

ただし、C++コードがRubyコードを呼び出し、そのRubyコードがさらにC++コードを呼び出す場合もあります。Riceはそのような状況に対処するのを簡単にしてくれます。以下でその方法を説明します。

例外の変換

RubyコードがC++の関数やメソッドを呼び出すか、C++の属性を読み書きする場合、Riceは例外ハンドラをインストールして、発生したC++の例外をキャッチします。そのハンドラはC++の例外をRubyの例外に変換し、再度発生させることで、呼び出し元のRubyコードで処理できるようにします。

C++の例外をRubyの例外にマッピングする方法を以下の表にまとめます:

C++ 例外 Ruby 例外
std::bad_alloc NoMemoryError
std::domain_error FloatDomainError
std::exception RuntimeError
std::invalid_argument ArgumentError
std::filesystem::filesystem_error IOError
std::length_error RuntimeError
std::out_of_range IndexError
std::overflow_error RangeError
std::range_error RangeError
std::regex_error RegexpError
std::system_error SystemCallError
std::underflow_error RangeError
Rice::Exception RuntimeError
その他の例外 RuntimeError

Rice::Exceptionクラスは、Rice自体が例外を発生させる必要がある場合に使用するカスタム例外タイプです。

カスタムハンドラ

Riceでは、カスタム例外ハンドラを登録することもできます。次のように行います:

extern "C"
void Init_test()
{
  register_handler<MyException>(handle_my_exception);

  Data_Type<Test> rb_cTest =
    define_class<Test>("Test")
    .define_constructor(Constructor<Test>())
    .define_method("hello", &Test::hello)
    .define_method("error", &Test::error);
}

handle_my_exceptionは任意の型の例外を処理できます。例えば、C++の例外を投げることも可能です:

void handle_my_exception(const MyException& ex)
{
  throw std::runtime_error(ex.what());
}

より便利な例として、C++の例外をRubyの例外に変換することがあります。これはRice::Exceptionクラスを使用して行います:

void handle_my_exception(const MyException& ex)
{
  throw Rice::Exception(rb_eRuntimeError, ex.what_without_backtrace());
}

ハンドラの順序

例外ハンドラは登録された順に適用されます。したがって、ハンドラA、B、Cを登録すると、最初にAがチェックされ、次にB、最後にCがチェックされます。

例外ハンドラはグローバルであり、RubyがC++関数を呼び出したり、属性を読み書きするときに使用されます。また、cpp_protectを使用する場合にも適用されます(詳細は:ref:c++_exceptionsを参照してください)。

Rubyの例外

C++コードがRuby APIを呼び出す場合、Rubyの例外をキャッチするためにその呼び出しを保護する必要があります。Riceはこれを行うためのprotectメソッドを提供しています。例えば、カスタムC++クラスにEnumerableのサポートを追加するためにeachメソッドを実装したとします。eachメソッドは、ユーザー指定のブロックに値を渡すためにrb_yieldを使用すべきです。しかし、rb_yieldを直接呼び出してRubyコードが例外を発生させた場合、プログラムはクラッシュします。代わりに、protect関数を使用します:

.define_method("each", [](T& vector) -> Object
{
  for (Value_T& item : vector)
  {
    VALUE element = detail::To_Ruby<Value_T>().convert(item);
    detail::protect(rb_yield, element);
  }

  return vector;
}

ほとんどのケースでは、protectメソッドは関数パラメータを呼び出されるRuby APIに正しくマップします。しかし、まれに、正しいパラメータタイプを推論するのを手助けする必要があります。詳細な例については、以下のC++例外セクションを参照してください。

RiceはRubyのthrow/catchやRuby VM内部の他の非ローカルジャンプから投げられるシンボルを処理するために、Jump_Tagと呼ばれる同様のクラスを使用します。

C++例外

C++コードがRuby APIを呼び出し、その後RubyコードがC++コードを呼び出す場合、潜在的なC++例外をキャッチする必要があります。これはあまり一般的ではありませんが、C++からRubyコレクションを繰り返すときに発生する可能性があります。例えば:

static int convertPair(VALUE key, VALUE value, VALUE user_data)
{
  // マップを取得
  std::map<T, U>* result = (std::map<T, U>*)(user_data);

  // このメソッドはRubyから呼び出されているので、C++例外をRubyに伝播させてはいけません
  return cpp_protect([&]
  {
    result->operator[](From_Ruby<T>().convert(key)) = From_Ruby<U>().convert(value);
    return ST_CONTINUE;
  });
}

static std::map<T, U> createFromHash(VALUE value)
{
  std::map<T, U> result;
  VALUE user_data = (VALUE)(&result);

  // MSVCはここで助けが必要だが、g++は不要
  using Rb_Hash_ForEach_T = void(*)(VALUE, int(*)(VALUE, VALUE, VALUE), VALUE);
  detail::protect<Rb_Hash_ForEach_T>(rb_hash_foreach, value, convertPair, user_data);

  return result;
}

このコードは、Rubyのハッシュから新しい:ref:std_mapを作成します。これを行うために、rb_hash_foreachを使用してハッシュを反復処理します。rb_hash_foreach関数はC++関数へのポインタを取ります。この場合、convertPairと呼ばれる関数です。これは、protect呼び出しが呼び出すメソッドの型を理解するために助けが必要なまれなケースです。この場合、rb_hash_foreachの関数シグネチャはvoid(*)(VALUE, int(*)(VALUE, VALUE, VALUE), VALUE)です。

ハッシュ内の各アイテムに対して、RubyはconvertPair関数を呼び出します。つまり、C++からRuby、そして再びC++に戻っています。convertPair関数は発生したC++例外をキャッチしなければなりません。それは、関数のコードをcpp_protectラムダ内にラップすることで行われます:

// このメソッドはRubyから呼び出されているので、C++例外をRubyに伝播させてはいけません
return cpp_protect([&]
{
  result->operator[](From_Ruby<T>().convert(key)) = From_Ruby<U>().convert(value);
  return ST_CONTINUE;
});

タイプの概要

Riceの目的は、ネイティブC++コードとRubyコードが一緒に動作できるようにすることです。これには、両方の言語間でタイプを簡単に変換できるようにすることが必要です。

主に3つのユースケースがあります:

  1. 両言語間でタイプを変換(コピー)すること(:doc:Type conversion <type_conversions>を参照)
  2. RubyがC++コードにアクセスできるように、Rubyラッパーを介して提供すること(define_classdefine_enumなど)
  3. C++がC++ラッパーを介してRubyコードにアクセスできるようにすること(:ref:apiを参照)

:doc:Type conversion <type_conversions>は、ブール型や数値型などのプリミティブ型に対してはうまく機能します。例えば、C++の符号なし32ビット整数はRubyのFixnumインスタンスにコピーされます(その逆も同様)。プリミティブ型の変換は、プログラマーにとってなじみがあるため理解しやすいです。ブール型や整数をメソッドに渡すとき、そのメソッドがそれを変更するとは考えないで、単にコピーを受け取ると考えます。

しかし、より複雑な型については、通常、型変換は意味を成しません。簡単なC++構造体のインスタンスをRubyにコピーしたいとはあまり思わないでしょうし、C++クラスのインスタンスをコピーしたいことはほとんどありません。その理由は多くありますが、いくつか挙げると:

  • C++オブジェクトは、データベース接続や開いたファイルハンドルのようにコピーできない内部状態を持っているかもしれません。
  • C++には、オブジェクトの生成、コピー、破棄の方法を制御する複雑なオブジェクトライフタイムルールがあり、これらはRubyには翻訳できません。
  • C++オブジェクトは、例えば100万要素のベクターのように大量のメモリを使用する可能性があり、それをRubyにコピーするのは現実的ではありません。
  • データをコピーすることは、定義により2つの別々のバージョンを作成することであり、2つの言語間でデータを共有することができなくなります。

その結果、より実用的なアプローチは、RubyがC++オブジェクトにアクセスし、C++がRubyオブジェクトにアクセスできるようにするための薄いラッパーを提供することです。Rubyラッパーは、他のドキュメントで説明されているように、define_enumdefine_classなどを介して作成されます。

最後に、C++からRubyオブジェクトを操作したい場合、RubyのC APIではなく、Riceのオブジェクト指向の:ref:apiを使用することをお勧めします。

タイプ変換

Riceでは、RubyとC++の間で変換(コピー)すべきタイプをビルトインタイプと呼びます。ビルトインタイプはC++からRubyに直接マップされるタイプです。例として、nullptrbool、数値型(整数、浮動小数点、倍精度、複素数)、char型、文字列などがあります。

ビルトインタイプはコピーされるため、そのインスタンスは切り離されます。したがって、Rubyの文字列がstd::stringに変換される場合、2つの文字列は独立しており、一方の変更が他方に反映されることはありません。また、C++で新しいchar*を割り当ててRubyに渡すと、Rubyはchar*の内容をコピーしますが、元のバッファを解放しないため、メモリリークが発生します。

Riceはすべての一般的なビルトインタイプを標準でサポートしています。一般的に、新しいC++タイプをRubyに追加するには、define_classdefine_enumなどを使ってラップする必要があります。新しいビルトインタイプを追加するのはかなり珍しいケースです。

ビルトインタイプの追加

例として、std::deque<int>をRubyに公開したいとしましょう。RiceのSTL(標準テンプレートライブラリ)サポートを使用せずにこれを行います。また、ラッパーを提供するのではなく、データを2つの言語間でコピーしたいとします。これを行うには、次のステップが必要です:

  1. Typeテンプレートを特殊化する
  2. To_Rubyテンプレートを特殊化する
  3. From_Rubyテンプレートを特殊化する

ステップ1 - Typeの特殊化

まず、std::deque<int>が既知のタイプであることをRiceに伝える必要があります。これにより、:doc:タイプ検証 <type_verification>を通過します。これはTypeテンプレートを特殊化することで行います:

namespace Rice::detail
{
  template<>
  struct Type<std::deque<int>>
  {
    static bool verify()
    {
      return true;
    }
  };
}

特殊化は必ずRice::detail名前空間で行う必要があります。タイプにサブタイプが含まれている場合は、それらも検証することを忘れないでください。例として、std::optionalverifyメソッドを示します:

namespace Rice::detail
{
  template<typename T>
  struct Type<std::optional<T>>
  {
    static bool verify()
    {
      return Type<T>::verify();
    }
  };
}

std::optionalは格納するタイプが有効な場合にのみ有効であることに注意してください。

ステップ2 - To_Rubyの特殊化

次に、std::deque<int>をRubyオブジェクトに変換するC++コードを書く必要があります。最も明らかなRubyオブジェクトのマッピング先は配列です。

namespace Rice::detail
{
  template<>
  class To_Ruby<std::deque<int>>
  {
  public:
    VALUE convert(const std::deque<int>& deque)
    {
      // Rubyが例外を投げた場合に備えて、Ruby APIの呼び出しをprotectでラップしている点に注意
      // protectを使用せずにRubyが例外を投げると、プログラムは**クラッシュします**
      VALUE result = protect(rb_ary_new2, deque.size());

      for (int element : deque)
      {
        // C++のintをRubyの整数に変換
        VALUE value = To_Ruby<int>::convert(element);
        // Ruby配列に追加
        detail::protect(rb_ary_push, result, value);
      }
      return result;
    }
  };
}

定義は必ずRice::detail名前空間で行う必要があります。

上記のような生のRuby C APIを使用する代わりに、Ruby配列のためのC++ラッパーを提供するRice::Arrayを使用する方が良いでしょう。

ステップ3 - From_Rubyの特殊化

最後に、Ruby配列をstd::deque<int>に変換するためのC++コードも記述する必要があります。

namespace Rice::detail
{
  template<>
  class From_Ruby<std::deque<int>>
  {
  public:
    std::deque<int> convert(VALUE ary)
    {
      // 配列が本当に配列であることを確認します。そうでない場合、この呼び出しはRuby例外を投げるので、保護する必要があります
      detail::protect(rb_check_type, ary, (int)T_ARRAY);

      long size = protect(rb_array_len, ary);
      std::deque<int> result(size);

      for (long i = 0; i < size; i++)
      {
        // 配列要素を取得
        VALUE value = protect(rb_ary_entry, ary, i);

        // RubyのintをC++のintに変換
        int element = From_Ruby<int>::convert(value);

        // デックに追加
        result[i] = element;
      }

      return result;
    }
  };
}

通常通り、定義は必ずRice::detail名前空間で行う必要があります。

デフォルト引数のサポート

RiceはC++の:ref:default_argumentsをサポートしています。このサポートをカスタムタイプで有効にするには、From_Rubyの特殊化に次の変更を加える必要があります:

  • detail::Argポインタを受け取り、それをメンバ変数に格納する追加のコンストラクタを追加する
  • デフォルトコンストラクタを追加する
  • convertメソッドで、Rubyの値がnil(つまりQnil)であり、argが設定されている場合、デフォルト値を返す

上記の例を拡張すると:

namespace Rice::detail
{
  template<>
  class From_Ruby<std::deque<int>>
  {
  public:
    From_Ruby() = default;

    explicit From_Ruby(Arg* arg) : arg_(arg)
    {
    }

    std::deque<int> convert(VALUE ary)
    {
      if (ary == Qnil && this->arg_ && this->arg_->hasDefaultValue())
      {
        return this->arg_->defaultValue<std::deque<int>>();
      }
      else
      {
        // .... 上記の例のコードと同じ
      }
    }

  private:
    Arg* arg_ = nullptr;
  };
}

タイプ検証

Riceはタイプに非常に厳密です。定義されていないタイプに遭遇すると、起動時に例外を投げます。これはほとんどのC++開発者にとって驚くべきことではないでしょう。

例えば、次のような例を考えてみましょう:

class MyClass
{
};

using Fancy_T = std::vector<std::unique_ptr<std::pair<std::string, MyClass>>>;

class Factory
{
  Fancy_T& make_fancy_type();
};

void setupRice()
{
  define_class<Factory>("Factory").
    define_method("make_fancy_type", &Factory::make_fancy_type);
}

上記のRice拡張がRubyにロードされると、例外が発生します。その理由は、MyClassがまだRiceに定義されていないからです。

define_methodが呼び出されると、Riceは&Factory::make_fancy_typeの戻り値のタイプがFancy_Tであることを確認します。次に、ベクターからユニークポインタ、ペア、そしてMyClassへと掘り下げていきます。MyClassに到達したとき、Riceは内部のTypeRegistryをチェックし、まだ定義されていないことに気づいてstd::runtime_exceptionを投げます。

これを修正するには、まずMyClassを次のように定義する必要があります:

void setupRice()
{
  define_class<MyClass>("MyClass");

  define_class<Factory>("Factory").
    define_method("make_fancy_type", &Factory::make_fancy_type);
}

MyClassを定義することで、RiceはTypeRegistryにこのクラスが存在することを認識し、例外を避けることができます。

継承

Riceは、define_classメソッドを使用して子クラスを作成することをサポートしています:

class Base
{
public:
  virtual void foo();
};

class Derived : public Base
{
};

extern "C"
void Init_test()
{
  Data_Type<Base> rb_cBase =
    define_class<Base>("Base")
    .define_method("foo", &Base::foo);

  Data_Type<Derived> rb_cDerived =
    define_class<Derived, Base>("Derived");
}

これは、define_classに2番目のテンプレートパラメータを追加することで行われます。このパラメータは、DerivedBaseを継承していることを指定します。

Riceは多重継承をサポートしていません。

ディレクター

ラップされたC++クラスから継承するRubyクラスを作成したい場合、継承はさらに複雑になります。これにはいくつかの問題があります:

  • RubyクラスはC++の仮想メソッドをオーバーライドできる必要があります。
  • オーバーライドされた仮想メソッドはsuperを呼び出して、オーバーライドされたC++メソッドを呼び出せる必要があります。
  • C++コードが仮想メソッドを呼び出すとき、Rubyでオーバーライドされたバージョンを呼び出す必要があります。

Riceはこれらのユースケースを「ディレクター」クラスを使ってサポートします。ディレクターは、クラス階層を上下に正しくメソッド呼び出しをディスパッチできるプロキシです。

Note: ディレクターという名前はSWIGに由来します。詳細はSWIGのドキュメントを参照してください。

次のクラスを考えてみましょう:

class VirtualBase
{
public:
  VirtualBase();
  virtual int doWork();
  virtual int processWorker() = 0;
};

このクラスの抽象的な性質のため、C++コンパイラは仮想クラスをインスタンス化しようとしてエラーを出すため、直接Riceでラップすることはできません。純粋仮想関数がなくても、VirtualBase::doWorkの呼び出しはC++レベルで停止し、実行はRubyのサブクラスには渡りません。

これらのメソッドを適切にラップするには、プロキシとしてRice::Directorサブクラスを使用し、この新しいプロキシクラスをdefine_classでラップするタイプとして使用します:

#include <rice/rice.hpp>

class VirtualBaseProxy : public VirtualBase, public Rice::Director
{
public:
  VirtualBaseProxy(Object self) : Rice::Director(self)
  { }

  virtual int doWork()
  {
    int result = getSelf().call("do_work");
    return detail::From_Ruby<int>().convert(result);
  }

  int default_doWork()
  {
    return VirtualBase::doWork();
  }

  virtual int processWorker()
  {
    int result = getSelf().call("process_worker");
    return detail::From_Ruby<int>().convert(result);
  }

  int default_processWorker()
  {
    raisePureVirtual();
  }
};

ここでは多くのことが行われているので、それぞれの部分について説明します。

class VirtualBaseProxy : public VirtualBase, public Rice::Director { }

まず、このクラスは対象の仮想クラスとRice::Directorの両方をサブクラス化する必要があります。

public:
  VirtualBaseProxy(Object self) : Rice::Director(self) { }

Rice::Directorがその魔法を発揮するには、このクラスの各インスタンスが対応するRubyインスタンスへのハンドルを持っている必要があります。コンストラクタは最初の引数としてRice::Objectを受け取り、それをRice::Directorに渡す必要があります。

次に、doWorkを実装します。ディレクタークラスはメソッド呼び出しをRubyインスタンスに転送することでオーバーライドします。

virtual int doWork()
{
  int result = getSelf().call("do_work");
  return detail::From_Ruby<int>().convert(result);
}

int default_doWork()
{
  return VirtualBase::doWork();
}

また、default_doWorkも実装しており、これによりRubyがオーバーライドされた仮想C++メソッドを呼び出すことができます。default_プレフィックスは、どのメソッドがどの機能を実行するかを明確にするための命名規則です。

RubyがC++メソッドを呼び出してはならない場合、default_の実装はraisePureVirtual()を呼び出す必要があります:

int default_processWorker()
{
  raisePureVirtual();
}

メソッドraisePureVirtual()は、純粋仮想メソッドをRubyにラップできるようにするため(そしてコンパイルが可能であることを確認するため)に存在しますが、同時にこの拡張のユーザーに、C++側で呼び出せるものがない場合はすぐに通知します。

ディレクタークラスが構築されたら、それをRubyにラップします:

extern "C"
void Init_virtual() {
  define_class<VirtualBase>("VirtualBase")
    .define_director<VirtualBaseProxy>()
    .define_constructor(Constructor<VirtualBaseProxy, Rice::Object>())
    .define_method("do_work", &VirtualBaseProxy::default_doWork)
    .define_method("process_worker", &VirtualBaseProxy::default_processWorker);
}

このコードには新しい点がいくつかあります。

  1. define_director呼び出しが追加され、VirtualBaseProxyをテンプレートパラメータとして取ります。
  2. ConstructorテンプレートパラメータもVirtualBaseProxyである必要があり、派生オブジェクトの適切なオブジェクトの生成と破棄を可能にします。
  3. define_method呼び出しはdefault_*実装を指す必要があります。

インスタンスレジストリ

Rice 4.1では、RubyオブジェクトでラップされたC++オブジェクトを追跡するインスタンスレジストリが追加されました。これは、Riceによって維持されるグローバルなstd::mapを介して行われます。

有効化

インスタンスレジストリが有効な場合、RiceはC++インスタンスがRubyインスタンスによってラップされているかどうかをチェックします。ラップされている場合は、既存のRubyインスタンスが返されます。

デフォルトでは、インスタンストラッキングは無効になっています。これを有効にするには、次のように設定します:

detail::Internal::instance_registry.isEnabled = true;

無効化

インスタンスレジストリが無効になっている場合、RiceはC++インスタンスがすでにRubyインスタンスによってラップされているかどうかに関係なく、新しいRubyインスタンスでC++インスタンスをラップします。したがって、参照またはポインタを介して毎回同じC++オブジェクトを返すC++メソッドに複数回呼び出すと、複数のラッピングされたRubyオブジェクトが作成されます。デフォルトでは、複数のRubyオブジェクトがC++オブジェクトをラップすることは問題ありません。なぜなら、RubyオブジェクトはC++オブジェクトを所有していないからです。詳細については、:ref:cpp_to_rubyのトピックをよくお読みください。

このルールには1つの例外があります。それは、C++メソッドが自分自身を返す場合です。Riceは、C++オブジェクトが呼び出しを行っているRubyオブジェクトによってラップされていることを認識し、したがってselfを返しています(詳細は:ref:return_selfを参照してください)。

トラッキングが無効な理由

インスタンスレジストリを有効にすると、パフォーマンスが大幅に向上する可能性があります。トラッキングはわずかなオーバーヘッドを追加しますが、重複するRubyオブジェクトとC++ラッパーオブジェクトの作成を避けることができます。

しかし、トラッキングが完全に信頼できるかどうかは不明です。いくつかの潜在的な問題があります。

まず、実装はスレッドセーフではありません。RubyのGIL(グローバルインタープリタロック)のおかげで、これは問題と見なされていません。

次に、ペアはRubyオブジェクトがガベージコレクタによって解放されるときにグローバルマップから削除されます。Rubyオブジェクトが削除のためにマークされているが、基礎となるC++オブジェクトが再びRubyに返される可能性があるウィンドウが存在する可能性があります。その場合、Rubyオブジェクトは解放され、クラッシュが発生します。これは実際に発生するかどうかは不明ですが、観察されたことはありません。

第三に、RubyインスタンスによってラップされたC++インスタンスがC++側で解放される可能性があります。所有権のルールが正しく設定されている限り、これは問題ありません。しかし、削除されたC++オブジェクトと同じアドレスを持つ新しいC++インスタンスが作成され、それがRubyに渡されると、インスタンストラッカーは古い削除されたオブジェクトを返します。これはRiceのテストで観察されています。これがテストの書き方によるものなのか、より一般的な問題なのかは不明です。

STL

RiceはC++の標準テンプレートライブラリ(STL)を部分的にサポートしています。このサポートを有効にするには、rice/rice.hppの後にrice/stl.hppヘッダーファイルをインクルードする必要があります。

自動生成されるRubyクラス

STLクラスはテンプレートクラスであることを覚えておいてください。そのため、実際のC++クラスを定義するには具体的な型でインスタンス化する必要があります。例えば、std::vector<int>std::vector<std::string>とは異なるクラスです。これにより、C++コードベースには多くのSTLインスタンス化クラスが存在する可能性があります。

これらのクラスを手動で定義する手間を避けるために、RiceはRubyラッパークラスを自動的に生成します。これらのRubyクラスはRice::Stdモジュールに追加されます。自動クラスは、RubyコードがC++で作成されたラップされたオブジェクトにアクセスしたり、それを変更したりする場合によく機能します。

ただし、Rubyがこれらのクラスの新しいインスタンスを作成する必要がある場合、自動クラス作成はあまりうまく機能しません。その理由は、生成されたクラス名がC++のランタイム型情報から生成されるため、非常に長くて見た目が悪くなるためです。例えば、型std::pair<std::string, double>の名前は次のようになります:

Rice::Std::Pair__basic_string__char_char_traits__char___allocator__char_____double__

うわぁ!したがって、ルールオブサムは、Rubyからこれらのクラスのインスタンスを作成する必要がない場合は生成されたクラスを使用し、そうでない場合は手動で作成されたクラスを使用することです。

クラス名を手動で作成するのは簡単です。これを行う方法はSTLタイプごとに異なりますが、単純な命名規則に従います—define_pairdefine_vectorなどです。各サポートされているSTLタイプのドキュメントを参照してください。

手動で作成されたクラス名は、自動生成されたクラスの後に定義することもできます。何が起こるかというと、クラスはまだ1つしかないが、それが複数の定数によって参照されることです。例えば、ペアが登録された後にdefine_pair<std::pair<std::string, double>>(StringDoublePair)を呼び出すと、Rubyではクラスを指す2つの定数があります:

Rice::Std::Pair__basic_string__char_char_traits__char___allocator__char_____double__
Object::StringDoublePair

std::complex

std::stringと同様に、Riceはstd::complexをビルトインタイプと見なします。つまり、std::complexの値はRubyのComplexインスタンスに変換され、その逆も同様です。

std::map

std::vectorの外では、std::mapはC++で最も一般的に使用されるコンテナの1つです。std::mapとRubyのHashの間には直接的な概念的マッピングがありますが、Riceはマップをハッシュにコピーしません。代わりに、std::mapをラップします。

これにはいくつかの理由があります:

  • std::mapのインスタンスは1つのキーと値のタイプしか含められませんが、Rubyのハッシュは異なるタイプのキーと値を含むことができます。
  • std::mapのインスタンスは非常に大きくなることがあります。
  • std::mapのインスタンスには、コピーやムーブセマンティクスが複雑なC++クラスが一般的に含まれます。
  • C++とRubyで切り離されたデータのコピーが2つ存在するのは通常望ましくありません。

Riceは、見つかったstd::mapの各インスタンス化に対して、自動的にRubyクラスを定義します。また、define_mapdefine_map_underメソッドを使用してRubyクラスを手動で定義することもできます。ただし、Riceが自動的にそれらを作成する前に定義する必要があります。

例:

std::map<std::string, int> makeStringIntMap()
{
   return std::map {{"one", 1}, {"two", 2}, {"three", 3}};
}

define_map<std::map<std::string, int>>("StringIntMap");
define_global_function("make_string_int_map", &makeStringIntMap);

このRubyクラスを定義したら、次のようにして新しいインスタンスを作成できます:

map = StringIntMap.new
map["value 1"] = 1
map["value 2"] = 2

ハッシュからマップへ

マップ引数を取るC++メソッドの場合、Rubyから新しいマップをインスタンス化できます(詳細は:ref:stl_class_namesを参照してください)。

例えば、次のC++コードを考えてみましょう:

void passMap(std::map<std::string, int> stringIntMap)
{
}

define_map<std::map<std::string, int>>("StringIntMap");
define_global_function("pass_map", &passMap);

これをRubyから呼び出す1つの方法は次のとおりです:

map = StringIntMap.new
map["thirty seven"] = 37
pass_map(map)

この場合、RubyはC++のマップをラップしています。したがって、C++でマップに行った変更はRubyに反映されます。

しかし、Rubyのハッシュを渡す方が便利な場合もあります。これは、特にRiceの:ref:automatic <stl_class_names> STLクラスを使用している場合に当てはまります。

したがって、Riceはこの使い方もサポートしています:

hash = {"three" => 3, "five" => 5, "nine" => 9}
pass_map(hash)

この場合、RiceはRubyのハッシュをコピーします(ラップするのではなく)。したがって、C++で行った変更はRubyには反映されません。

Ruby API

Riceは、std::mapをRubyのハッシュのように見せるために、HashのサブセットとなるAPIを提供しようとしています。ただし、いくつかの違いがありますので、注意が必要です。

まず、以下のメソッドはマップのタイプがコピー可能である場合にのみ動作します(コピーはC++で行われます):

  • Map#copy(other)

次に、以下のメソッドはマップのタイプがC++の等号演算子 operator== を実装している場合にのみ動作します:

  • Map#value?

最後に、マップのタイプがC++ストリームをサポートしている場合、次のメソッドは動作します。サポートしていない場合は「Not Printable」を返します:

  • Map#to_s

std::optional

C++17で導入されたstd::optionalは、関数から設定されていない可能性のある値を返すためのC++コードのもう一つの方法を提供します(ポインタ以外に)。

Rubyにはこれに相当するタイプがないため、Riceはstd::optionalインスタンスをアンラップします。オプショナルが空である、つまりstd::nulloptの値を持つ場合、RiceはこれをRubyのnilに変換します。オプショナルに値があり、その値がビルトインタイプである場合、それは適切なRubyタイプに変換されます。ビルトインタイプでない場合、その値はRubyによってラップされます。

Rubyインスタンスをstd::optionalに渡す際、Riceはnilの値をstd::nulloptに、nilでない値を適切なC++タイプに変換します。

std::pair

std::pairは、2つの値を関連付けるための簡単なコンテナを提供するC++のコードです。std::mapstd::unordered_mapは、キーとそれに関連する値を保持するためにstd::pairを使用します。

Rubyにはペアの概念がないため、Riceはstd::pairをラップします。これにより、データがC++とRubyの間でコピーされることはありません。

std::pairは2つのタイプのテンプレートであるため、各std::pairのインスタンス化はそれぞれ独自のC++クラスであり、したがって独自のRubyクラスでもあります。ペアクラスを手動で定義することも、Riceに自動的に定義させることもできます。Rubyクラスを手動で定義するには、define_pairまたはdefine_pair_underメソッドを使用します。

例:

std::pair<std::string, uint32_t> makeStringIntPair(std::string key, uint32_t value)
{
   return std::make_pair(key, value);
}

define_pair<std::pair<std::string, uint32_t>>("StringIntPair");
define_global_function("make_string_int_pair", &makeStringIntPair);

このRubyクラスを定義した後、新しいインスタンスを次のように作成できます:

pair = StringIntPair.new("key 2", 33)

Ruby API

std::pair用に公開されているRuby APIは非常にわかりやすく、以下のメソッドで構成されています(ここでは、Pairという名前のRubyクラスを作成したと仮定しています):

  • Pair#new(value1, value2)
  • Pair#first
  • Pair#first=(value)
  • Pair#second
  • Pair#second=(value)

基になるstd::pairがコピー可能なタイプを持っている場合(コピーはC++で行われます)、次のメソッドは動作します。そうでない場合は例外が発生します:

  • PairClass#copy(other)

基になるstd::pairがC++のストリームによってサポートされているタイプを持っている場合、次のメソッドは動作します。そうでない場合は「Not Printable」を返します:

  • PairClass#to_s

std::reference_wrapper

C++11で導入されたstd::reference_wrapperは、C++の参照をコピー可能で代入可能なオブジェクトにラップします。これにより、std::vectorのようなコンテナや、std::variantのような他のタイプに格納することが可能になります。

Rubyにはこれに相当するタイプがないため、Riceはstd::reference_wrapperインスタンスをアンラップします。std::reference_wrapperがビルトインタイプを指している場合、それは適切なRubyタイプに変換されます。ビルトインタイプではない場合、その値はRubyによってラップされます。

Rubyインスタンスをstd::reference_wrapperに渡す際、RiceはRubyタイプを適切なC++タイプに変換し、それをラッパー内に格納します。これは危険である場合があります。というのも、Riceは参照を有効に保つために元のメモリ位置を保持する必要があるからです。ラップされたタイプの場合、これはラッピングされたRubyオブジェクトが有効であり、ガベージコレクションされない限り、参照が有効であることを意味します。ビルトインタイプについては、Riceはメソッド呼び出しのライフタイムを通じてのみ参照の有効性を保証します。したがって、呼び出されたC++コードが参照を格納し、後でそれを使用しようとすると、例外が発生します。

スマートポインタ

スマートポインタは、現代のC++でメモリ安全なコードを書くための重要なツールです。Riceはstd::unique_ptrstd::shared_ptrをサポートしています。また、Riceはカスタムスマートポインタタイプをサポートするように簡単に拡張できます。

std::unique_ptr

ネイティブメソッドがstd::unique_ptrを返すと、Riceはそれをムーブコンストラクタを介してコピーします。したがって、Riceはstd::unique_ptrの所有権を取得し、その基礎となるオブジェクトの所有権も持ちます。

std::unique_ptrの使用はRubyコードに対して透過的です。Rubyから見れば、ユニークポインタによって管理されている型をラップしているだけなので、std::unique_ptr::element_typeのようになります。その結果、std::unique_ptr自体に対するRubyから見えるAPIは存在しません。

Riceはstd::unique_ptrを標準でサポートしており、追加作業は必要ありません。"rice/stl.hpp"ヘッダをインクルードするだけです。例を見てみましょう:

class MyClass
{
public:
  int flag = 0;

public:
  void setFlag(int value)
  {
    this->flag = value;
  }
};

class Factory
{
public:
  std::unique_ptr<MyClass> transfer()
  {
    return std::make_unique<MyClass>();
  }
};

int extractFlagUniquePtrRef(std::unique_ptr<MyClass>& myClass)
{
  return myClass->flag;
}

void setupRice()
{
  define_class<MyClass>("MyClass").
    define_method("set_flag", &MyClass::setFlag);

  define_class<Factory>("Factory").
    define_constructor(Constructor<Factory>()).
    define_method("transfer", &Factory::transfer);

  define_global_function("extract_flag_unique_ptr_ref", &extractFlagUniquePtrRef);
}

Rubyでの使用例:

factory = Factory.new
my_instance = factory.transfer
my_instance.set_flag(5)
flag = extract_flag_unique_ptr_ref(my_instance)

my_instanceがスコープを外れ、ガベージコレクションされると、それがラップしているstd::unique_ptrも解放され、それが管理しているC++のMyClassインスタンスも解放されます。

注意:Riceは、ネイティブ関数のパラメータとしてstd::unique_ptrを渡して、C++に所有権を戻すことをサポートしていません。つまり、std::unique_ptrがRubyに転送されると、Rubyによって解放されます。ただし、上記の例のように参照を返すことは可能です。std::unique_ptrの参照を渡すことは、その所有権を移さないことに注意してください。

std::shared_ptr

ネイティブメソッドがstd::shared_ptrを返すと、Riceはそれをムーブコンストラクタを介してコピーし、共有ポインタのインスタンスを1つ所有します。当然ながら、他のオブジェクトが所有するstd::shared_ptrのコピーが存在する可能性があります。

std::shared_ptrの使用もRubyコードに対して透過的です。Rubyから見れば、共有ポインタによって管理されている型をラップしているだけなので、std::shared_ptr::element_typeのようになります。その結果、std::shared_ptr自体に対するRubyから見えるAPIは存在しません。

Riceはstd::shared_ptrを標準でサポートしており、追加作業は必要ありません。"rice/stl.hpp"ヘッダをインクルードするだけです。例を見てみましょう:

class MyClass
{
public:
  int flag = 0;

public:
  void setFlag(int value)
  {
    this->flag = value;
  }
};

class Factory
{
public:
  std::shared_ptr<MyClass> share()
  {
    if (!instance_)
    {
      instance_ = std::make_shared<MyClass>();
    }
    return instance_;
  }

public:
  static inline std::shared_ptr<MyClass> instance_;
};

int extractFlagSharedPtr(std::shared_ptr<MyClass> myClass)
{
  return myClass->flag;
}

int extractFlagSharedPtrRef(std::shared_ptr<MyClass>& myClass)
{
  return myClass->flag;
}

void setupRice()
{
  embed_ruby();

  define_class<MyClass>("MyClass").
    define_method("set_flag", &MyClass::setFlag);

  define_class<Factory>("Factory").
    define_constructor(Constructor<Factory>()).
    define_method("share", &Factory::share);

  define_global_function("extract_flag_shared_ptr", &extractFlagSharedPtr);
  define_global_function("extract_flag_shared_ptr_ref", &extractFlagSharedPtrRef);
}

Rubyでの使用例:

factory = Factory.new
my_instance = factory.share
my_instance.set_flag(5)
flag = extract_flag_shared_ptr(my_instance)
flag = extract_flag_shared_ptr_ref(my_instance)

my_instanceがスコープを外れ、ガベージコレクションされると、それがラップしているstd::shared_ptrも解放されます。それが基礎となるC++のMyClassインスタンスを解放するかどうかは、他のstd::shared_ptrインスタンスがそれを管理しているかどうかによります。

std::unique_ptrとは異なり、関数パラメータを介してstd::shared_ptrのコピーをネイティブコードに戻すことができます。ただし、RubyはラッパーRubyオブジェクトが解放されるまで、常に共有ポインタの1つのコピーを保持します。

カスタムスマートポインタ

Riceは追加のスマートポインタタイプをサポートするように拡張することができます。まず、stl/smart_ptr.hppを確認してください。これはスマートポインタを格納するために使用される以下のテンプレートクラスを定義しています:

namespace Rice::detail
{
  template <template <typename, typename...> typename SmartPointer_T, typename...Arg_Ts>
  class WrapperSmartPointer : public Wrapper
  {
  public:
    WrapperSmartPointer(SmartPointer_T<Arg_Ts...>& data);
    void* get() override;
    SmartPointer_T<Arg_Ts...>& data();

  private:
    SmartPointer_T<Arg_Ts...> data_;
  };
}

スマートポインタがこのテンプレートクラスに収まると仮定すると、Riceに次の3つのことを伝える必要があります:

  • それをRubyオブジェクトにラップする方法
  • Rubyオブジェクトからそれを抽出する方法
  • その管理対象タイプにアクセスする方法

まず、スマートポインタをどのようにラップするかをRiceに伝えます。std::unique_ptrの場合は次のようにします:

namespace Rice::detail
{
  template <typename T>
  struct To_Ruby<std::unique_ptr<T>>
  {
    static VALUE convert(std::unique_ptr<T>& data, bool takeOwnership = true)
    {
      std::pair<VALUE, rb_data_type_t*> rubyTypeInfo = detail::Registries::instance.types.figureType<T>(*data);

      // Use custom wrapper type 
      using Wrapper_T = WrapperSmartPointer<std::unique_ptr, T>;
      return detail::wrap<std::unique_ptr<T>, Wrapper_T>(rubyTypeInfo.first, rubyTypeInfo.second, data, true);
    }
  };
}

最初にdetail::To_Rubyをスマートポインタタイプに対して特殊化します。この場合、std::unique_ptrです。次に、ポインタをRiceのTypeRegistryに渡し、どのRubyクラスがC++クラスをラップしているかを確認します。その後、Wrapper_TクラスをWrapperSmartPointerからインスタンス化します。最後に、タイプ情報とポインタをヘルパーメソッドに渡し、まずWrapper_Tのインスタンスを作成し、次にそれを格納するRubyオブジェクトを作成します。

次のステップは、Rubyからスマートポインタをどのように抽出するかをRiceに伝えることです。

namespace Rice::detail
{
  template <typename T>
  struct From_Ruby<std::unique_ptr<T>&>
  {
    static std::unique_ptr<T>& convert(VALUE value)
    {
      Wrapper* wrapper = detail::getWrapper(value, Data_Type<T>::ruby_data_type());

      using Wrapper_T = WrapperSmartPointer<std::unique_ptr, T>;
      Wrapper_T* smartWrapper = dynamic_cast<Wrapper_T*>(wrapper);
     

 if (!smartWrapper)
      {
        std::string message = "Invalid smart pointer wrapper";
        throw std::runtime_error(message.c_str());
      }
      return smartWrapper->data();
    }
  };
}

上記と同様に、スマートポインタタイプに対してdetail::From_Rubyを特殊化します。この場合、std::unique_ptrです。

次に、ヘルパーメソッドを使用して、detail::To_Rubyメソッドで作成したWrapperインスタンスへのポインタを取得します。注意すべき点は、detail::Wrapperはカスタムラッパータイプの基底クラスであるため、これをWrapper_Tにキャストする必要があることです。最後に、そのdata()メソッドを呼び出して、格納されたスマートポインタにアクセスします。

最後に、Riceにスマートポインタが含むタイプを検証する方法を伝える必要があります。これは次のようにして行います:

namespace Rice::detail
{
  template<typename T>
  struct Type<std::unique_ptr<T>>
  {
    static bool verify()
    {
      return Type<T>::verify();
    }
  };
}

std::string

おそらくstd::stringはC++標準テンプレートライブラリで最も一般的に使用されるクラスでしょう。Riceはstd::stringをビルトインタイプとして扱います。つまり、文字列はC++とRubyの間でコピーされます。したがって、std::stringをRubyに渡してRuby側で変更しても、C++側にはその変更は反映されません(その逆も同様です)。

Rubyとは異なり、C++はエンコーディングのサポートが非常に少ないです。そのため、C++とRubyの間で文字列を正しく変換するのは推測ゲームのようなもので、正しくエンコードするかどうかはユーザー次第です。

Riceがstd::stringをRubyに変換する際、そのエンコーディングはEncoding.default_externalで指定されたエンコーディングであると仮定されます。WindowsではおそらくUTF-8で、LinuxやMacOSではオペレーティングシステムのロケールに基づきます。外部エンコーディングが指定されていない場合、変換された文字列のエンコーディングはASCII-8BIT(Rubyの「エンコーディングがまったくない」という意味)になります。エンコーディングが間違っている場合は、RubyでString#force_encodingを呼び出して修正する必要があります。

対照的に、RiceがRubyの文字列をstd::stringに変換する際には、単純に元のcharバッファをstd::stringに渡してコピーします。したがって、C++に文字列を渡す前に、Rubyでエンコーディングが正しく設定されていることを確認するのは再びあなた次第です。

なお、Riceはstd::wstringをサポートしていません。

std::string_view

std::string_viewcharのシーケンスへの読み取り専用の参照です。これにより、std::stringをコピーするオーバーヘッドなしで文字列を渡す方法を提供します。

入力時、Riceはstd::string_viewをビルトインタイプとして扱います。つまり、C++のstd::string_viewが参照するcharシーケンスの部分をRubyにコピーします。Riceがエンコーディングをどのように扱うかについては、:ref:std_stringのドキュメントを参照してください。

出力時、RiceはRuby文字列の基礎となるcharバッファを参照するstd::string_viewを作成します。注意:これは非常に危険です。Rubyの文字列は最終的にガベージコレクションされるか、コンパクションの一部として移動されるため、charバッファが無効になる可能性があります。この機能を使用する際は十分注意してください。

std::unordered_map

std::unordered_mapはC++のハッシュテーブルです。しかし、Riceはunordered_mapをハッシュテーブルにコピーせず、ラップします。

これにはいくつかの理由があります:

  • std::unordered_mapのインスタンスは一種類のキーと値しか含められないのに対し、Rubyのハッシュは異なる種類のキーと値を含むことができます。
  • std::unordered_mapのインスタンスは非常に大きくなることがあります。
  • std::unordered_mapのインスタンスには、コピーやムーブセマンティクスが複雑なC++クラスが一般的に含まれます。
  • データの2つの切り離されたコピー(C++とRubyでそれぞれ1つずつ)が存在することは通常望ましくありません。

Riceは見つけた各std::unordered_mapのインスタンス化に対して、自動的にRubyクラスを定義します。また、define_unordered_mapdefine_unordered_map_underメソッドを使用してRubyクラスを手動で定義することもできます。ただし、Riceが自動的にそれらを作成する前に定義する必要があります。

例:

std::unordered_map<std::string, int> makeStringIntMap()
{
   return std::unordered_map {{"one", 1}, {"two", 2}, {"three", 3}};
}

define_unordered_map<std::unordered_map<std::string, int>>("StringIntMap");
define_global_function("make_string_int_map", &makeStringIntMap);

このRubyクラスを定義した後、新しいインスタンスを次のように作成できます:

map = StringIntMap.new
map["value 1"] = 1
map["value 2"] = 2

ハッシュからマップへ

マップ引数を取るC++メソッドの場合、Rubyから新しいマップをインスタンス化できます(詳細は:ref:stl_class_namesを参照してください)。

例えば、次のC++コードを考えてみましょう:

void passMap(std::unordered_map<std::string, int> stringIntMap)
{
}

define_unordered_map<std::unordered_map<std::string, int>>("StringIntMap");
define_global_function("pass_map", &passMap);

これをRubyから呼び出す1つの方法は次のとおりです:

map = StringIntMap.new
map["thirty seven"] = 37
pass_map(map)

この場合、RubyはC++のマップをラップしています。したがって、C++でマップに行った変更はRubyに反映されます。

しかし、Rubyのハッシュを渡す方が便利な場合もあります。これは、特にRiceの:ref:automatic <stl_class_names> STLクラスを使用している場合に当てはまります。

したがって、Riceはこの使い方もサポートしています:

hash = {"three" => 3, "five" => 5, "nine" => 9}
pass_map(hash)

この場合、RiceはRubyのハッシュをコピーします(ラップするのではなく)。したがって、C++で行った変更はRubyには反映されません。

Ruby API

Riceはstd::unordered_mapをRubyのハッシュのように見せるために、HashのサブセットとなるAPIを提供しようとしています。ただし、いくつかの違いがありますので、注意が必要です。

まず、以下のメソッドはマップのタイプがコピー可能である場合にのみ動作します(コピーはC++で行われます):

  • Map#copy(other)

次に、以下のメソッドはマップのタイプがC++の等号演算子 operator== を実装している場合にのみ動作します:

  • Map#value?

最後に、マップのタイプがC++ストリームをサポートしている場合、次のメソッドは動作します。サポートしていない場合は「Not Printable」を返します:

  • Map#to_s

std::variant

C++17で導入されたstd::variantは、型安全なユニオンです。std::variantは、サポートされているタイプの中から1つの値を保持できます。

Rubyの変数はどんなタイプの値でも指すことができるため、Rubyにはstd::variantに相当するタイプは必要ありませんし、存在しません。そのため、Riceはstd::variantインスタンスをアンラップし、格納された値を適切なRubyタイプに変換します。

Rubyインスタンスをstd::variantに渡す場合、RiceはRubyのタイプを適切なC++タイプに変換し、それをバリアントの中に格納します。

std::vector

std::stringと同様に、std::vectorも多くのC++コードベースで多用される重要なクラスです。std::vectorとRubyのArrayには直接的な概念的対応がありますが、Riceはベクターを配列にコピーせず、代わりにstd::vectorをラップします。

これにはいくつかの理由があります:

  • std::vectorのインスタンスは1種類の型しか含められませんが、Rubyの配列は異なる型を含むことができます。
  • std::vectorのインスタンスは非常に大きくなることがあります。
  • std::vectorのインスタンスには、コピーやムーブセマンティクスが複雑なC++クラスが一般的に含まれます。
  • データの2つの切り離されたコピー(1つはC++、もう1つはRuby)は通常望ましくありません。

Riceは見つけた各std::vectorのインスタンス化に対して、自動的にRubyクラスを定義します。また、define_vectordefine_vector_underメソッドを使用してRubyクラスを手動で定義することもできます。ただし、Riceが自動的にそれらを作成する前に定義する必要があります。

std::vector<std::string> makeStringVector()
{
   return std::vector {"one", "two", "three"};
}

define_vector<std::vector<std::string>>("StringVector");
define_global_function("make_string_vector", &makeStringVector);

このRubyクラスを定義した後、新しいインスタンスを次のように作成できます:

vector = StringVector.new
vector.push("value 1")
vector.push("value 2")

配列からベクターへ

ベクター引数を取るC++メソッドの場合、Rubyから新しいベクターをインスタンス化できます(詳細は:ref:stl_class_namesを参照してください)。

例えば、次のC++コードを考えてみましょう:

void passVector(std::vector<int> ints)
{
}

define_vector<std::vector<int>>("IntVector");
define_global_function("pass_vector", &passVector);

これをRubyから呼び出す1つの方法は次のとおりです:

vector = IntVector.new
vector.push(37)
pass_vector(vector)

この場合、RubyはC++のベクターをラップしています。したがって、C++でベクターに行った変更はRubyに反映されます。

しかし、Rubyの配列を渡す方が便利な場合もあります。これは、特にRiceの:ref:automatic <stl_class_names> STLクラスを使用している場合に当てはまります。

したがって、Riceはこの使い方もサポートしています:

array = [3, 5, 9]
pass_vector(array)

この場合、RiceはRubyの配列をコピーします(ラップするのではなく)。したがって、C++で行った変更はRubyには反映されません。

Ruby API

Riceはstd::vectorをRubyの配列のように見せるために、ArrayのサブセットとなるAPIを提供しようとしています。ただし、いくつかの違いがありますので、注意が必要です。

まず、以下のメソッドはベクターのタイプがコピー可能である場合にのみ動作します(コピーはC++で行われます):

  • Vector#copy(other)
  • Vector#resize

次に、以下のメソッドはベクターのタイプがC++の等号演算子 operator== を実装している場合にのみ動作します:

  • Vector#delete
  • Vector#include?
  • Vector#index

最後に、ベクターのタイプがC++ストリームをサポートしている場合、次のメソッドは動作します。サポートしていない場合は「Not Printable」を返します:

  • Vector#to_s

Ruby C++ API

Riceは、多くのビルトインRubyタイプのためのラッパーを提供しています。これには以下が含まれます:

  • Object
  • Module
  • Class
  • String
  • Array
  • Hash
  • Struct
  • Symbol
  • Exception

Riceは可能な限りRubyのクラス階層を模倣しています。

例えば:

Object object_id = obj.call("object_id");
std::cout << object_id << std::endl;

Class rb_cTest = define_class<Test>("Test");
Object object_id = rb_cTest.call("object_id");
std::cout << object_id << std::endl;

ArrayおよびHashタイプは、STLコンテナをイテレートするのと同じ方法でイテレートすることもできます:

Array a;
a.push(detail::To_Ruby<int>().convert(42));
a.push(detail::To_Ruby<int>().convert(43));
a.push(detail::To_Ruby<int>().convert(44));
Array::iterator it = a.begin();
Array::iterator end = a.end();
for(; it != end; ++it)
{
  std::cout << *it << std::endl;
}

STLのアルゴリズムもArrayおよびHashコンテナで期待通りに動作するはずです。

移行ガイド:バージョン3から4への変更点

以前のバージョンのRiceでは、gem install時にいくつかの初期コードのコンパイルが必要でした。これにより、HerokuやGitHub Actionsのような、適切な共有ライブラリやビルドシステムが許可されていない、または利用できないプラットフォームでRiceやRiceを使用するライブラリの利用が難しくなっていました。

Rice 4では、ヘッダーのみのライブラリに移行し、Riceを使用するライブラリがバイナリビルドを提供しやすくなりました。しかし、この移行には多くの作業が必要で、いくつかの後方互換性のない変更が導入されました。このページでは、Rice 3を使用している人がRice 4以降でライブラリを動作させるために必要な主な変更点を記載しています。

#include

最初の変更点は、Riceが単一の結合されたヘッダーファイルとして公開されるようになったことです。そのため、すべてのインクルードをこの1つに変更できます。他にはSTLラッパー定義を含むヘッダーファイルが1つあり、それを取得するには #include <rice/stl.hpp> を使用します。詳細については、:doc:stl/stlセクションを参照してください。

to_ruby / from_ruby

最も顕著な後方互換性のない変更は、RubyとC++の間のタイプ変換の定義方法です。

Rice 3では、to_rubyおよびfrom_rubyメソッドを次のように定義していました:

template<>
Foo from_ruby<Foo>(Rice::Object x)
{
  // ...
}

template<>
Rice::Object to_ruby<Foo>(Foo const & x)
{
  // ...
}

Rice 4では、これらの関数の構造が大幅に変更されました。これらのメソッドは、convert関数として構造体内で定義し、Rice::detail名前空間内に配置する必要があります:

namespace Rice::detail
{
  template<>
  class From_Ruby<Foo>
  {
    Foo convert(VALUE x)
    {
      // ...
    }
  };

  template<>
  class To_Ruby<Foo>
  {
    VALUE convert(Foo const & x)
    {
      // ...
    }
  };
}

さらに、これらの関数はRiceのObject型ではなく、RubyのVALUE型を使用します。この変更は、C++とRubyの間でオブジェクトを変換する際に余分なコピーを作成しないようにするために行われました。詳細については、:doc:type conversion <bindings/type_conversions>セクションを参照してください。

関数とメソッドの違い

Riceでは、オブジェクト上のメソッドを定義するか通常の関数を定義するかに応じて異なるdefine_メソッドを使用します。selfが必要な場合は、define_methodまたはdefine_singleton_methodを使用します。そうでない場合は、define_functionおよびdefine_singleton_functionを使用する必要があります。詳細は:ref:the tutorial <Defining Methods>で読むことができます。

デフォルト引数

RiceがC++の:ref:default_argumentsを定義する方法が微妙に変更されました。これは見逃しやすく、バグ(デフォルト値が正しく設定されない)を引き起こす可能性があります。以前はRiceはC++のカンマ演算子に依存して引数を組み合わせていました:

define_method("hello", &Test::hello, (Arg("hello"), Arg("second") = "world"));

2つのArgsが括弧で囲まれていることに注意してください。Riceはこのスタイルをサポートしなくなりました。代わりに、より自然な方法で括弧なしでArgsを渡します:

define_method("hello", &Test::hello, Arg("hello"), Arg("second") = "world");

メモリ管理

Rice 4では、Rubyのガベージコレクタによって管理されるべきオブジェクトを明示的に定義するためのツールをさらに提供しています。詳細は:ref:Memory Managementをお読みください。

Why Rice?

Motivation

RubyのためのCまたはC++拡張を作成する際に、いくつかの共通の問題があります:

型の安全性。IDやVALUEのような整数型を混同しやすいです。Ruby APIの関数の中には、受け取る型が一貫していないものもあります(例:rb_const_definedはIDを取り、rb_mod_remove_constはシンボルを取る)。

DRY(Don't Repeat Yourself)原則。ラップされた関数が受け取る引数の数を指定するのは間違いやすいです。関数に新しい引数を追加すると、rb_define_methodに渡す引数の数も更新する必要があります。

型変換。データをRubyの型に変換するための関数は多数あり、その多くは異なるセマンティクスや形式を持っています。例えば、文字列を変換するにはStringValueマクロを使うかもしれませんが、整数を変換するにはFIX2INTを使います。以前にラップされたCデータをアンラップするにはさらに別の形式を使用します。

例外の安全性。C++の例外がCコードに到達しないようにすること、およびRubyの例外が非自明なデストラクタを持つオブジェクトがスタック上にある間に逃げないようにすることが重要です。どの例外をいつ使って良いかのルールは、特にコードが時間をかけて維持されると、正しくするのが難しいです。

スレッドの安全性。Rubyインタプリタはスレッドセーフではないため、複数のスレッドから実行してはいけません。GCやスケジューラがCスタックで行うトリックのため、あるスレッドでインタプリタが一度実行されたら、将来的にはそのスレッドからのみ実行されるべきです。また、Rubyがスレッドを切り替える際にスタックをコピーするため、あるRubyスレッドで作成されたオブジェクトを別のRubyスレッドでアクセスしないようにする必要があります。

CベースのAPI。RubyのAPIは、HashやArrayなどのRubyデータ構造にアクセスする際に必ずしも便利ではありません。特にC++コードを書くときには、これらのコンテナのインターフェースが標準のコンテナと一貫していないためです。

呼び出し規約。Ruby APIに渡される関数ポインタはCの呼び出し規約に従わなければなりません。これにより、テンプレート関数や静的メンバ関数へのポインタを渡すことができなくなります(つまり、いくつかのプラットフォームでは動作しますが、移植性はありません)。

継承。C++オブジェクトをラップするとき、派生クラスへのポインタを格納するのは簡単ですが、基底クラスのメソッドはオブジェクトをアンラップするために派生クラスについての知識を持たなければなりません。基底クラスへのポインタを常に格納し、必要に応じて派生型へのポインタをdynamic_castすることも可能ですが、これは遅くて面倒であり、複数の継承にはうまく機能しない可能性があります。すべての隅々まで継承を適切に処理するシステムは簡単ではありません。

複数継承。C++は真の複数継承をサポートしていますが、Rubyのオブジェクトモデルはミックスインを用いた単一継承を使用しています。複数継承を使用するライブラリをラップする際には、マッピングを構築する際に注意が必要です。

GCの安全性。すべてのライブRubyオブジェクトは、ガベージコレクタのマークフェーズ中にマークされなければなりません。そうでないと、早期に破棄されてしまいます。一般的なルールとして、ヒープ上に保存されているオブジェクトの参照はrb_gc_register_addressで登録するか、データオブジェクトのマーク関数でマークするべきです。スタック上に保存されているオブジェクトの参照は、Rubyインタプリタが適切に初期化されていれば、自動的にマークされます。

コールバック。Cは関数ポインタを通じてコールバックを実装しますが、Rubyは通常プロックを通じてコールバックを実装します。プロックを呼び出すアダプタ関数を書くのは難しくありませんが、エラーの機会が多くあります(特に例外の安全性に関して)。

データのシリアル化。デフォルトでは、Cレイヤーで定義されたデータオブジェクトはマシュアラブルではありません。ユーザーはメンバーごとにデータをシリアル化する関数を明示的に定義しなければなりません。

Riceはこれらの問題に対して多くの方法で対応しています:

  • 型の安全性:RiceはObject、Identifier、Class、Module、Stringなどのすべてのビルトイン型をカプセル化します。オブジェクトのインスタンスをラップする前に動的型を自動的にチェックします。
  • DRY原則:Riceはテンプレートと関数オーバーロードを使用して、関数の引数の数と型を自動的に決定します。ただし、デフォルト引数は明示的に処理する必要があります。
  • 型変換:Riceはto_ruby<>from_ruby<>のキャストスタイルのテンプレート関数を提供し、明示的な型変換を簡素化します。すべてのラップされた関数のパラメータと戻り値の自動型変換が生成されます。
  • 例外の安全性:Riceは一般的な例外を自動的に変換し、ユーザー定義の例外タイプを変換するためのメカニズムを提供します。Riceはまた、Rubyコードにコールバックする際に例外を変換するための便利な関数も提供します。
  • スレッドの安全性:Riceはスレッドの安全性を扱うためのメカニズムを提供していません。多くの一般的なスレッドの安全性の問題は、POSIXスレッドをサポートするYARVによって軽減されるはずです。
  • C++ベースのAPI:RiceはRuby C APIの多くの共通関数に対してオブジェクト指向のC++スタイルのAPIを提供します。
  • 呼び出し規約:Riceは、Ruby APIに渡されるすべての関数ポインタに対してCの呼び出し規約を自動的に使用します。
  • 継承:Riceは、基底クラスでラップされたメンバー関数が呼び出されたときに、基底クラスの型に自動的に変換を行います。
  • 複数継承:Riceは複数継承のメカニズムを提供していません。複数継承はミックスインを通じてシミュレートすることができますが、これはまだ十分に簡単ではありません。
  • GCの安全性:Riceはガベージコレクタと対話するためのいくつかの便利なクラスを提供します。オブジェクトが適切に破棄されるようにするために従わなければならない基本的なルールはまだあります。
  • コールバック:Riceはコールバックを処理するためのいくつかの便利なクラスを提供します。
  • データのシリアル化:Riceはデータシリアル化のメカニズムを提供していませんが、将来のリリースで追加される可能性があります。

History

Riceは、サウスカロライナ州マウントプレザントにあるAutomated Trading DeskでC++ベースのトレーディングソフトウェアとインターフェースするためのプロジェクトとしてExcrubyとして始

まりました。当時、SwigのRubyバインディングは現在ほど成熟しておらず、プロジェクトのニーズに適していませんでした。

ExcrubyはRuby APIのラッパーとしてではなく、例外安全な方法でRubyインタプリタとインターフェースするためのヘルパー関数とクラスのセットとして書かれました。5年間の間に、プロジェクトはAPIの一部のラッパーに成長しましたが、元のヘルパー関数はパブリックインターフェースの一部として残っていました。

これはライブラリのユーザーに混乱を招きました。なぜなら、ほとんどのタスクを達成するための複数の方法があったからです。直接C APIを使う方法、C APIの低レベルラッパーを使う方法、そして低レベルのインターフェースの高レベル抽象化を使う方法です。

そこでRiceはインターフェースを整理する試みとして誕生しました。Riceは低レベルのラッパーを保持していますが、それは実装の詳細としてです。パブリックインターフェースは真にRuby C APIの高レベルの抽象化です。

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1