バインド(bind)とは、その意味どおり、「C++の関数やクラスを使えるようにするためにXtalから見えるところに関連付ける」ということです。
これを行うことによって、Xtalから、C++の関数の呼び出し、C++のクラスのインスタンス生成・メソッド呼び出し、などC++のオブジェクトの参照・操作ができるようになります。
LuaやSquirrelでは、C++とスクリプトはスタック操作によってやりとりすることができますが、いちいち面倒です。そのため、登録や関数呼び出し周りのごたごたをサポートするライブラリを使うことが多いです。それを「バインダ」と呼んでいます。
バインダとしてはluabindやSqplusといったものが有名ですが、Xtalは言語仕様にバインダを含んでおり、ライブラリなどを追加することなく簡単にバインドすることができということは特徴にも挙げられている通りです。さらに、その作業をさらに簡単にするマクロも多数定義されています。
バインドの基本は、xtal::Any::defです。これを使ってlibオブジェクトやClassオブジェクトにバインドします。
次項からは実際にさまざまなC++オブジェクトをバインドしていきます。
バインドする以外にもC++からXtalに渡す手段はあります。
まあ要するに引数として渡す以外に無いですね。たぶん。
まずはlibオブジェクトにバインドしてみます。
バインドなどを行うbind_xtal関数を追加したので、一応今一度全体を提示しておきます。
#include <xtal.h> #include <xtal_macro.h> // Xidなど便利なマクロが定義されている #include <xtal_lib/xtal_cstdiostream.h> // CStdioStdStreamLibのため #include <xtal_lib/xtal_winthread.h> // WinThreadLibのため #include <xtal_lib/xtal_winfilesystem.h> // WinFilesystemLibのため #include <xtal_lib/xtal_chcode.h> // SJISChCodeLibのため #include <xtal_lib/xtal_errormessage.h> // bind_error_messageのため void bind_xtal(){ using namespace xtal; // 変数(定数ですけど)のバインド lib()->def(Xid(testVar), "foo"); // 何も入っていないクラスのバインド lib()->def(Xid(Foo), xnew<Class>()); // シングルトンにすることもできる ClassPtr pSingleton = xnew<Class>(); pSingleton->set_singleton(); lib()->def(Xid(FooSingleton), pSingleton); // Arrayをバインド lib()->def(Xid(FooArray), xnew<Array>()); } void exec_xtal(){ using namespace xtal; load("test.xtal"); } int main(int argc, char* argv[]){ using namespace xtal; CStdioStdStreamLib std_stream_lib; // std*はCの標準ライブラリを使う WinThreadLib thread_lib; // Windowsのスレッドを使う WinFilesystemLib filesystem_lib; // Windowsのファイルシステムを使う SJISChCodeLib ch_code_lib; // SJISを使う // 環境依存である機能についてどれを使うかを設定 // 他にもアロケータなども設定できるが割愛 Setting setting; setting.std_stream_lib = &std_stream_lib; setting.thread_lib = &thread_lib; setting.filesystem_lib = &filesystem_lib; setting.ch_code_lib = &ch_code_lib; // ここで指定したstd_stream_libなどのポインタが示す先のオブジェクトは、 // uninitializeを呼び出すまで存在している必要がある。 // Xtalを初期化 initialize(setting); // エラーメッセージをバインド bind_error_message(); bind_xtal(); exec_xtal(); // Xtalを破棄 uninitialize(); return 0; }
// test.xtal lib::testVar.p; lib::Foo.p; lib::FooSingleton.p; (lib::FooSingleton.class() === lib::FooSingleton).p; lib::FooArray.push_back(1); lib::FooArray.push_back(2); lib::FooArray.p;
xtal::lib()->def(Xid([Name]), [xtal::AnyPtr]);
が大事です。lib()に[Name]を名前としてAnyPtrを定義しているんだ、という感覚です。これさえわかれば、AnyPtrにもっていくことさえできれば大体のオブジェクトをバインドすることができます。
他に、globalやfilelocalにバインドすることもできます。
void bind_xtal(){ using namespace xtal; // globalにバインド global()->def(Xid(Foo), 10); } void exec_xtal(){ using namespace xtal; CodePtr code = compile_file("test.xtal"); // filelocalにバインド code->def(Xid(Foo), 15); code->call(); }
// test.xtal global::Foo.p; filelocal::Foo.p; Foo.p;
関数をバインドするには、xtal::Any::defもしくはxtal::Any::def_funを用います。
関数の引数と戻り値に使える型及びそれの渡し方は制限されています。特に、ユーザー定義型は「Xtalの管理下に『こういう条件で』委任しますよー」という意味を込めてxtal::SmartPtrに包んであげる必要があります。
int twice(int x){ return x * 2; } int add(int x, int y){ return x + y; } void bind_xtal(){ using namespace xtal; lib()->def(Xid(twice), fun(&twice)); lib()->def_fun(Xid(add), &add); } void exec_xtal(){ using namespace xtal; load("test.xtal"); }
簡単なバインドのサンプルです。
lib()->def(Xid(twice), fun(&twice));
xtal::funというもので関数ポインタを包むことで、Xtalから関数と認識可能(呼び出しも当然可能)な*Ptr型のインスタンスにすることができます。
lib()->def_fun(Xid(add), &add);
これは、上のxtal::funを内部で呼び出しているラッパ関数です。どちらでも書きやすい方を使えばよいでしょう。
オーバーロードされた関数をバインドしようとすると、コンパイル時にはどちらを呼び出せばよいかわからないためにコンパイルエラーになってしまいます。
void func(int x){ xtal::stdout_stream()->print("int : "); xtal::stdout_stream()->println(x); } void func(float x){ xtal::stdout_stream()->print("float : "); xtal::stdout_stream()->println(x); } void bind_xtal(){ using namespace xtal; // コンパイルエラー lib()->def_fun(Xid(func), &func); } void exec_xtal(){ using namespace xtal; load("test.xtal"); }
要はコンパイル時にどちらを使いたいかを明示すればよいのです。それには関数ポインタのキャストを使います。
intの方を使いたいとするならば次のようにキャストします。
void func(int x){ xtal::stdout_stream()->print("int : "); xtal::stdout_stream()->println(x); } void func(float x){ xtal::stdout_stream()->print("float : "); xtal::stdout_stream()->println(x); } void bind_xtal(){ using namespace xtal; lib()->def_fun(Xid(func), static_cast<void (*)(int)>(&func)); } void exec_xtal(){ using namespace xtal; load("test.xtal"); }
Xtalへバインドできるクラスの要素は
となっています。
解放されるタイミングの関係でデストラクタだけprintf使いました……
// クラスの定義 class Foo{ public: Foo(int x) : x_(x){ xtal::stdout_stream()->println("Foo constructed"); } virtual ~Foo(){ printf("Foo destructed\n"); } virtual void Print() const{ xtal::stdout_stream()->print("Foo::x : "); xtal::stdout_stream()->println(x_); } private: int x_; }; class Bar : public Foo{ public: Bar(int new_x) : Foo(new_x), x(new_x){ xtal::stdout_stream()->println("Bar constructed"); } virtual ~Bar(){ printf("Bar destructed\n"); } virtual void Print() const{ xtal::stdout_stream()->print("Bar::x : "); xtal::stdout_stream()->println(x); } public: int x; }; // バインド // 継承構造やコンストラクタをバインドする XTAL_PREBIND(Foo){ // コンストラクタ登録 Xdef_ctor1(int); } // メソッドやインスタンス変数、クラス関数などをバインドする XTAL_BIND(Foo){ // メソッド登録 Xdef_method(Print); } XTAL_PREBIND(Bar){ // 継承構造を登録 it->inherit(xtal::cpp_class<Foo>()); // コンストラクタ登録 Xdef_ctor1(int); // デフォルト引数を登録 Xparam(y, 0); } XTAL_BIND(Bar){ // メソッド登録 Xdef_method(Print); // メンバ変数登録 Xdef_var(x); } void bind_xtal(){ using namespace xtal; // クラスをlibにバインドする // これでlib::Fooなどでクラスオブジェクトにアクセスできるようになる lib()->def(Xid(Foo), cpp_class<Foo>()); lib()->def(Xid(Bar), cpp_class<Bar>()); } void exec_xtal(){ using namespace xtal; load("test.xtal"); }
foo : lib::Foo(2); foo.Print(); bar : lib::Bar(); bar.Print(); bar2 : lib::Bar(10); bar2.x = 15; bar2.Print();
XTAL_PREBIND, XTAL_BINDとdef(Xid([Name], cpp_class<[Name]>())がポイントです。
// バインド // 継承構造やコンストラクタをバインドする XTAL_PREBIND(Foo){ // コンストラクタ登録 Xdef_ctor1(int); } // メソッドやインスタンス変数、クラス関数などをバインドする XTAL_BIND(Foo){ // メソッド登録 Xdef_method(Print); }
ここでは、XtalスクリプトでFooクラスがどのようなメンバを持っているかを設定しています。ctorを登録しないと、インスタンスを生成することができません。 XTAL_BINDの中で使える登録マクロは次のようなものがあります
Xdef_method(method_name) | メソッドを登録する |
---|---|
Xdef_method_alias(method_name, method_ptr) | メソッドを別名で登録する |
Xdef_fun(function_name) | クラス関数を登録する |
Xdef_fun_alias(function_name, function_ptr) | クラス関数を別名で登録する |
Xdef_getter(var_name) | メンバ変数のgetterを登録する |
Xdef_setter(var_name) | メンバ変数のsetterを登録する |
Xdef_var(var_name) | メンバ変数(のsetter/getter)を登録する |
Xparam(var_name, value) | デフォルト引数を設定する Xdef_method/Xdef_fun/Xdef_ctorNの直後に使う |
Xdef(name, AnyPtr) | 何か(AnyPtr)を登録する |
Xdef_const(const_name) | クラス定数を登録する |
// クラスをlibにバインドする // これでlib::Fooなどでクラスオブジェクトにアクセスできるようになる lib()->def(Xid(Foo), cpp_class<Foo>()); lib()->def(Xid(Bar), cpp_class<Bar>());
ここでは、libオブジェクトに「callするとインスタンスを生成できる、クラスオブジェクト(ClassPtr)」をバインドしています。これで、C++におけるFooクラスがXtalにおいてはどのような名前か、を指定しています。指定しないとXtalでどんな名前か、がわからないためインスタンスを生成することができません。
生成はC++で行って、Xtalからはメンバに触るだけで良い、となったらctor定義を抜いたりクラスオブジェクトのバインドを省くと良いでしょう。また、Xtalから触る必要が全く無い2)のであればバインド作業をする必要はありません。
前ページに引き続き、今度はAnyPtrとユーザー定義型との相互変換です。
関数から返す際にはユーザー定義型をAnyPtrに変換できる型で返す必要があるため、割と重要だったりします。
AnyPtrから組み込み型への変換と同じ方法のほか、xtal::cast<T>や xtal::unchecked_cast<T>を用いる方法があります。
ptr_castに関しては型変換(1) AnyPtrとプリミティブ型・組み込み型と同じく、xtal::SmartPtr<T>が返ります。記述法などはそちらの項を参照のこと。
もし生ポインタが必要であればxtal::SmartPtr<T>::get()とすることで得ることができます。ただし、この方法で得た生ポインタはXtalのガベージコレクションでオブジェクトが解放された後は無効なアドレスを指すのでメモリ不正アクセスとなるので、オブジェクトの寿命に注意しなくてはいけません。
class Foo{ public: Foo(int x): x_(x){} int x() const{ return x_; } void set_x(int x){ x_ = x; } void Print() const{ xtal::stdout_stream()->println(x_); } private: int x_; }; XTAL_PREBIND(Foo){ Xdef_ctor1(int); } void GetPtr(const xtal::AnyPtr& foo){ using namespace xtal; SmartPtr<Foo> p = ptr_cast<Foo>(foo); p->Print(); Foo* const pfoo = p.get(); pfoo->set_x(pfoo->x()+10); pfoo->Print(); } void bind_xtal(){ using namespace xtal; } void exec_xtal(){ using namespace xtal; const CodePtr code(compile_file("test.xtal")); code->def(Xid(Foo), cpp_class<Foo>()); code->def_fun(Xid(GetPtr), &GetPtr); code->call(); }
// test.xtal foo : Foo(234); GetPtr(foo);
しかし、この方法はいろいろな型が来得るかつそれぞれで異なる動作をしたいとき(例えばxtal::nullも受ける場合など3))に主に必要となるだけで、この変換部分をXtalに任せることもできます。
方法は、引数をユーザークラスのポインタかSmartPtrにするだけです。
class Foo{ public: Foo(int x): x_(x){} int x() const{ return x_; } void set_x(int x){ x_ = x; } void Print() const{ xtal::stdout_stream()->println(x_); } private: int x_; }; XTAL_PREBIND(Foo){ Xdef_ctor1(int); } void GetPtr(Foo* const foo){ xtal::stdout_stream()->println("@GetPtr"); foo->Print(); foo->set_x(foo->x()+10); foo->Print(); } void GetPtr2(const xtal::SmartPtr<Foo>& foo){ using namespace xtal; stdout_stream()->println("@GetPtr2"); foo->Print(); GetPtr(foo.get()); } void bind_xtal(){ using namespace xtal; } void exec_xtal(){ using namespace xtal; const CodePtr code(compile_file("test.xtal")); code->def(Xid(Foo), cpp_class<Foo>()); code->def_fun(Xid(GetPtr), &GetPtr); code->def_fun(Xid(GetPtr2), &GetPtr2); code->call(); }
// test.xtal foo : Foo(234); GetPtr(foo); GetPtr2(foo);
型変換(1) AnyPtrとプリミティブ型・組み込み型でも触れたとおり、SmartPtr<T>からAnyPtrへは暗黙で変換されます。つまり、ユーザー定義型をSmartPtr<T>に変換できれば良いわけです。
まず、新しくオブジェクトを作ってXtalに渡す(Xtalに管理してもらう)場合、xtal::xnew<T>を使うことができます。
class Foo{ public: Foo(int x): x_(x){} int x() const{ return x_; } void set_x(int x){ x_ = x; } void Print() const{ xtal::stdout_stream()->println(x_); } private: int x_; }; // XtalではFooをインスタンス化できなくした XTAL_PREBIND(Foo){} XTAL_BIND(Foo){ Xdef_method(Print); Xdef_method(set_x); Xdef_method(x); } xtal::SmartPtr<Foo> CreateFoo(int x){ return xtal::xnew<Foo>(x); } void bind_xtal(){ using namespace xtal; } void exec_xtal(){ using namespace xtal; const CodePtr code(compile_file("test.xtal")); code->def_fun(Xid(CreateFoo), &CreateFoo); code->call(); }
// test.xtal foo : CreateFoo(10); foo.x += 20; foo.Print();
new T がT*を返すのに対して、xnew<T>は代わりにSmartPtr<T>を返します。
あと、これは詳細を確認していないのでなんともいえないのですが、xnew<T>を用いる場合、xtal::Baseを継承していないとメモリアクセス例外を吐くことがある、かもしれません。とりあえず、Baseを継承していないクラスの生成にはnewとundeleterのセットを使うと良いと思います。
もう1つは、xnewで作られなかった(newを使った、スタックに作成した、静的変数、など)オブジェクトをSmartPtrに変換する方法です。
適切にSmartPtrに変換しなかった場合、オブジェクトが破棄されてメモリ不正アクセスが発生しますので、注意が必要です。
また、この方法ではオブジェクトを破壊するときに呼ばれる関数を指定できますが、公式リファレンスでも紹介されているデフォルトの「何もしない関数(xtal::undeleter)」を指定するとエラーでて止まります。まじかー。
と思っていたのですが、どうもXTAL_PREBINDをしていないとエラーが出る、ということらしいです。コンストラクタなど登録しないクラスでもXTAL_PREBINDは記述しましょう。
「newしたものなんだけどXtalにオブジェクト管理任せるわー」というときにxtal::deleterを指定するとき以外は第2引数は指定しなくても大丈夫だと思いますが、一応以下のデフォルト動作を確認して利用してください。
実際にいくつかSmartPtr構築法を実装してみます。
// xnewで直接作成 xtal::SmartPtr<Foo> CreateFoo(int x){ return xtal::xnew<Foo>(x); } // newされたオブジェクトからの変換 xtal::SmartPtr<Foo> CreateFoo2(int x){ Foo* foo = new Foo(x); return xtal::SmartPtr<Foo>(foo, xtal::deleter); } // 静的変数から変換 xtal::SmartPtr<Foo> CreateFoo3(int x){ static Foo foo(0); foo.set_x(x); return xtal::SmartPtr<Foo>(&foo); } // こちらのパターンでも大丈夫 Foo *CreateFoo4(int x){ static Foo foo(0); foo.set_x(x); return &foo; } void bind_xtal(){ using namespace xtal; } void exec_xtal(){ using namespace xtal; const CodePtr code(compile_file("test.xtal")); code->def_fun(Xid(CreateFoo), &CreateFoo); code->def_fun(Xid(CreateFoo2), &CreateFoo2); code->def_fun(Xid(CreateFoo3), &CreateFoo3); code->def_fun(Xid(CreateFoo4), &CreateFoo4); code->call(); }
foo : CreateFoo(10); foo.x += 20; foo.Print(); foo2 : CreateFoo2(20); foo2.x += 30; foo2.Print(); foo3_1 : CreateFoo3(10); foo3_1.Print(); foo3_2 : CreateFoo3(20); foo3_1.Print(); foo3_2.Print(); foo4_1 : CreateFoo4(10); foo4_1.Print(); foo4_2 : CreateFoo4(20); foo4_1.Print(); foo4_2.Print();
xnewとnewを使う方法は呼び出されるたびにオブジェクトを生成しているため、返すポインタの示す先は毎回新しいアドレスとなっています。それに対して、3番目の方法は実際は1つしかないオブジェクトのアドレスをポインタに格納しているために、返された変数は共有されているようなものです。どれか1つを変更したら全て変更されます。