====== バインド(XtalからC++のオブジェクトを操作する) ======
===== バインドとは =====
バインド(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
#include // Xidなど便利なマクロが定義されている
#include // CStdioStdStreamLibのため
#include // WinThreadLibのため
#include // WinFilesystemLibのため
#include // SJISChCodeLibのため
#include // bind_error_messageのため
void bind_xtal(){
using namespace xtal;
// 変数(定数ですけど)のバインド
lib()->def(Xid(testVar), "foo");
// 何も入っていないクラスのバインド
lib()->def(Xid(Foo), xnew());
// シングルトンにすることもできる
ClassPtr pSingleton = xnew();
pSingleton->set_singleton();
lib()->def(Xid(FooSingleton), pSingleton);
// Arrayをバインド
lib()->def(Xid(FooArray), xnew());
}
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に包んであげる必要があります。
* 引数
* 整数型・浮動小数点数型
* 値渡し
* クラス型
* 値渡し
* SmartPtr(及びそれのconst参照)
* ポインタ
* const参照
* 戻り値
* 整数型・浮動小数点数型
* 値渡し
* クラス型
* SmartPtr
* 生ポインタ((XTAL_PREBINDを忘れないように。))
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(&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());
// コンストラクタ登録
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());
lib()->def(Xid(Bar), cpp_class());
}
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());
lib()->def(Xid(Bar), cpp_class());
ここでは、libオブジェクトに「callするとインスタンスを生成できる、クラスオブジェクト(ClassPtr)」をバインドしています。これで、C++におけるFooクラスがXtalにおいてはどのような名前か、を指定しています。指定しないとXtalでどんな名前か、がわからないためインスタンスを生成することができません。
生成はC++で行って、Xtalからはメンバに触るだけで良い、となったらctor定義を抜いたりクラスオブジェクトのバインドを省くと良いでしょう。また、Xtalから触る必要が全く無い((関数から返したりはするけどそれを他のC++関数に渡したりするだけ、とか))のであればバインド作業をする必要はありません。
===== 型変換(2) AnyPtrとユーザー定義型 =====
前ページに引き続き、今度はAnyPtrとユーザー定義型との相互変換です。
関数から返す際にはユーザー定義型をAnyPtrに変換できる型で返す必要があるため、割と重要だったりします。
==== AnyPtrからユーザー定義型への変換 ====
AnyPtrから組み込み型への変換と同じ方法のほか、xtal::castや xtal::unchecked_castを用いる方法があります。\\
ptr_castに関しては[[fromcpp#型変換(1) AnyPtrとプリミティブ型・組み込み型]]と同じく、xtal::SmartPtrが返ります。記述法などはそちらの項を参照のこと。\\
もし生ポインタが必要であればxtal::SmartPtr::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 p = ptr_cast(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());
code->def_fun(Xid(GetPtr), &GetPtr);
code->call();
}
// test.xtal
foo : Foo(234);
GetPtr(foo);
しかし、この方法はいろいろな型が来得るかつそれぞれで異なる動作をしたいとき(例えばxtal::nullも受ける場合など((この場合に引数をユーザークラスのポインタにすると、nullを渡したときには型不整合でXtalの例外が飛びます)))に主に必要となるだけで、この変換部分を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){
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());
code->def_fun(Xid(GetPtr), &GetPtr);
code->def_fun(Xid(GetPtr2), &GetPtr2);
code->call();
}
// test.xtal
foo : Foo(234);
GetPtr(foo);
GetPtr2(foo);
==== ユーザー定義型からAnyPtr ====
[[fromcpp#型変換(1) AnyPtrとプリミティブ型・組み込み型]]でも触れたとおり、SmartPtrからAnyPtrへは暗黙で変換されます。つまり、ユーザー定義型をSmartPtrに変換できれば良いわけです。
まず、新しくオブジェクトを作ってXtalに渡す(Xtalに管理してもらう)場合、xtal::xnewを使うことができます。
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 CreateFoo(int x){
return xtal::xnew(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は代わりにSmartPtrを返します。
あと、これは詳細を確認していないのでなんともいえないのですが、xnewを用いる場合、xtal::Baseを継承していないとメモリアクセス例外を吐くことがある、かもしれません。とりあえず、Baseを継承していないクラスの生成にはnewとundeleterのセットを使うと良いと思います。
----
もう1つは、xnewで作られなかった(newを使った、スタックに作成した、静的変数、など)オブジェクトをSmartPtrに変換する方法です。
適切にSmartPtrに変換しなかった場合、オブジェクトが破棄されてメモリ不正アクセスが発生しますので、注意が必要です。
また、この方法ではオブジェクトを破壊するときに呼ばれる関数を指定できますが、公式リファレンスでも紹介されているデフォルトの「何もしない関数(xtal::undeleter)」を指定するとエラーでて止まります。まじかー。\\
と思っていたのですが、どうもXTAL_PREBINDをしていないとエラーが出る、ということらしいです。コンストラクタなど登録しないクラスでもXTAL_PREBINDは記述しましょう。
「newしたものなんだけどXtalにオブジェクト管理任せるわー」というときにxtal::deleterを指定するとき以外は第2引数は指定しなくても大丈夫だと思いますが、一応以下のデフォルト動作を確認して利用してください。
* xtal::RefCountingBase, xtal::Base, xtal::Any, xtal::AnyPtrおよびそれらのサブクラスの場合
* 参照カウンタが操作される
* その他のクラスの場合
* undeleterの指定
実際にいくつかSmartPtr構築法を実装してみます。
// xnewで直接作成
xtal::SmartPtr CreateFoo(int x){
return xtal::xnew(x);
}
// newされたオブジェクトからの変換
xtal::SmartPtr CreateFoo2(int x){
Foo* foo = new Foo(x);
return xtal::SmartPtr(foo, xtal::deleter);
}
// 静的変数から変換
xtal::SmartPtr CreateFoo3(int x){
static Foo foo(0);
foo.set_x(x);
return xtal::SmartPtr(&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つを変更したら全て変更されます。