RubyでC++のバインディングを作成するために、Riceの公式ドキュメントをChatGPTでにほんごに翻訳しました。
はじめに
Riceは、C++ 17対応のヘッダオンリーライブラリで、2つの目的を持っています。まず、既存のC++ライブラリに対するRubyのバインディングを簡単に作成することができます。次に、RubyのC APIに対してオブジェクト指向インターフェースを提供し、Rubyを埋め込んだり、C++でRubyの拡張機能を書くことを簡単にします。
Riceは、Boost.Python や pybind11 と似ており、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のドキュメントは、こちらで閲覧できます。
プロジェクトの詳細
- ソースコードはGitHubでホストされています: http://github.com/ruby-rice/rice
- バグ追跡: http://github.com/ruby-rice/rice/issues
- APIドキュメント: http://ruby-rice.github.io/4.x
インストール方法
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
オブジェクトに対するメソッド呼び出しをチェインしている点に注意してください。Module
とClass
のほとんどのメンバー関数は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;
VALUE
やObject
と同様に、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());
Arg
とReturn
オブジェクトを任意の順序で混在させることができます。例えば、次のコードも動作します:
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
/end
、cbegin
/cend
、rbegin
/rend
などがあります。
Enumerableのサポート(内部イテレータ)
Riceを使用すると、C++クラスに簡単にEnumerable
モジュールのサポートを追加できます。Enumerableモジュールは、Rubyクラスがeach
インスタンスメソッドを定義している限り、そのクラスに内部イテレータのサポートを追加します。
Riceはdefine_iterator
メソッドを使ってこれを簡単にします。define_iterator
はeach
メソッドを作成し、さらに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_back
、begin
、end
のどのオーバーロードバージョンを公開するかを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オブジェクトのライフタイムを別のオブジェクトに結びつける必要があります。これはコンテナでよく発生します。例えば、Listener
とListenerContainer
クラスを考えてみましょう。
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つのユースケースがあります:
- 両言語間でタイプを変換(コピー)すること(:doc:
Type conversion <type_conversions>
を参照) - RubyがC++コードにアクセスできるように、Rubyラッパーを介して提供すること(
define_class
、define_enum
など) - 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_enum
、define_class
などを介して作成されます。
最後に、C++からRubyオブジェクトを操作したい場合、RubyのC APIではなく、Riceのオブジェクト指向の:ref:api
を使用することをお勧めします。
タイプ変換
Riceでは、RubyとC++の間で変換(コピー)すべきタイプをビルトインタイプと呼びます。ビルトインタイプはC++からRubyに直接マップされるタイプです。例として、nullptr
、bool
、数値型(整数、浮動小数点、倍精度、複素数)、char
型、文字列などがあります。
ビルトインタイプはコピーされるため、そのインスタンスは切り離されます。したがって、Rubyの文字列がstd::string
に変換される場合、2つの文字列は独立しており、一方の変更が他方に反映されることはありません。また、C++で新しいchar*
を割り当ててRubyに渡すと、Rubyはchar*
の内容をコピーしますが、元のバッファを解放しないため、メモリリークが発生します。
Riceはすべての一般的なビルトインタイプを標準でサポートしています。一般的に、新しいC++タイプをRubyに追加するには、define_class
やdefine_enum
などを使ってラップする必要があります。新しいビルトインタイプを追加するのはかなり珍しいケースです。
ビルトインタイプの追加
例として、std::deque<int>
をRubyに公開したいとしましょう。RiceのSTL(標準テンプレートライブラリ)サポートを使用せずにこれを行います。また、ラッパーを提供するのではなく、データを2つの言語間でコピーしたいとします。これを行うには、次のステップが必要です:
-
Type
テンプレートを特殊化する -
To_Ruby
テンプレートを特殊化する -
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::optional
のverify
メソッドを示します:
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番目のテンプレートパラメータを追加することで行われます。このパラメータは、Derived
がBase
を継承していることを指定します。
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);
}
このコードには新しい点がいくつかあります。
-
define_director
呼び出しが追加され、VirtualBaseProxy
をテンプレートパラメータとして取ります。 -
Constructor
テンプレートパラメータもVirtualBaseProxy
である必要があり、派生オブジェクトの適切なオブジェクトの生成と破棄を可能にします。 -
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_pair
、define_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_map
やdefine_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::map
やstd::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_ptr
とstd::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_view
はchar
のシーケンスへの読み取り専用の参照です。これにより、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_map
やdefine_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_vector
やdefine_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の高レベルの抽象化です。