
今回はバインドについて。書いてみて思った以上の分量になってしまったが、一回やってしまえば後はコピペで済む問題なので気楽に読むといいと思う。
0.バインドを始める前に。
AngelCodeでは、バインドをする際、そのプロトタイプや、型は全て文字列で指定する。これによって変な問題(いつも、イライラさせてくれるtemplate絡みの問題とか。)がかなり解消されている。一方で、他のバインダではコンパイル時に判明するような非常に(程度が低いという意味での)低レベルな問題も実行時にならないとわからない。そのため、AngelScriptはバインドコードの戻り値を毎回assertにかけることを推奨している。
r = asEngine->RegisterObjectMethod("ISomeObj", "void Func( )", asFUNCTION(Bind::Func),asCALL_CDECL_OBJLAST); assert( r >= 0 );
本当にどうでもいい部分で、登録が失敗したりするのでバインドコードの後ろには必ずassertを入れたほうがいいと思う。
#ここら辺は動的言語 vs 静的言語的な宗教戦争とかでよくある、その人の嗜好に寄る部分だと思う。
#多少コンパイル時間がかかってでも、「コンパイルタイムは正義!」で、
#実行する前に問題を全部洗い出すようなものが好きな人は、
#文字列でプロトタイプを渡したり、ユーザ定義クラスの引数はvoid*だったり
#そもそもバインドされたクラスメンバの型のチェックがなかったりする
#AngelScriptは?かもしれない。
#個人的には、C++側の型チェックはルーズ、
#スクリプトの型チェックはそれなりに厳密なスタンスはすごく好み。
1.グローバル関数
何も考える必要はない。次のように行う。
r = asEngine->RegisterGlobalFunction("bool Func()", asFUNCTIONPR(Func, void, bool), asCALL_CDECL); assert( r >= 0 );
ただ型が、ユーザー定義の場合はその型がバインドされた後でないとエラーが帰るので、順番としては、クラス型の登録が全て終わったあとにやるといいかもしれない。余談だが、実はOverloadをサポートしている。次のように、引数が違う二つの関数を登録してもちゃんと動く。
r = asEngine->RegisterGlobalFunction("bool Func()", asFUNCTIONPR(Func, void, bool), asCALL_CDECL); assert( r >= 0 );
r = asEngine->RegisterGlobalFunction("bool Func(int)", asFUNCTIONPR(Func, int, bool), asCALL_CDECL); assert( r >= 0 );
#id softwareのコード規約にもあるように、個人的には、
#オーバーロードをあまり好ましいと思っていないが。
2.グローバル変数
グローバル変数に、C/C++の変数を置くことができる。
r = asEngine->RegisterGlobalProperty("int g_SomeNum", &g_SomeNum); assert( r >= 0 );
ユーザ定義型もおくことができる。
r = asEngine->RegisterGlobalProperty("SomeObj @g_SomeObj", &g_SomeObj ); assert( r >= 0 );
#このユーザ定義型の配置はあまり上手くいかない場合がある。(単なるミスかもしれないが)
#それに、個人的にあまり好きではないので私は利用していない。
3.クラス、構造体の登録
クラスのメンバ関数や、メンバ変数を登録する前に、クラス、構造体をまず型として登録する。メンバ関数、メンバ変数にその型がでてくるような場合、事前にその型を登録していないと、エラーになるため先に全てのバインドする型を登録し、その後にメンバ関数、メンバ変数をしたほうがいいと思う。クラス、構造体の登録には値型として登録する方法と、参照として登録する方法がある。
3.1 値型としてのクラス、構造体の登録
スクリプト内で値として生成できるようなクラス、構造体 ~ベクトルクラス、行列クラスなど~ は、以下のようにその型を登録する。
/*Vec2の登録*/
class Binds
{
public:
static void Constructor(void *memory)
{
new(memory) Vec2();
}
static void Constructor2(float x, float y, Vec2 *self )
{
new(self) Vec2(x,y);
}
static void CopyConstructor(const Vec2 &other, Vec2 *self)
{
new(self) Vec2(other);
}
static void Destructor(void *memory)
{
((Vec2*)memory)->~Vec2();
}
};
r = engine->RegisterObjectType("Vec2", sizeof(Vec2), asOBJ_VALUE); assert( r >= 0 );
r = engine->RegisterObjectBehaviour("Vec2", asBEHAVE_CONSTRUCT, "void f()",
asFUNCTION(Binds::Constructor), asCALL_CDECL_OBJLAST); assert( r >= 0 );
r = engine->RegisterObjectBehaviour("Vec2", asBEHAVE_CONSTRUCT, "void f(float, float)",
asFUNCTION(Binds::Constructor2), asCALL_CDECL_OBJLAST); assert( r >= 0 );
r = engine->RegisterObjectBehaviour("Vec2", asBEHAVE_CONSTRUCT, "void f(const Vec2 &in)",
asFUNCTION(Binds::CopyConstructor), asCALL_CDECL_OBJLAST); assert( r >= 0 );
r = engine->RegisterObjectBehaviour("Vec2", asBEHAVE_DESTRUCT, "void f()",
asFUNCTION(Binds::Destructor), asCALL_CDECL_OBJLAST); assert( r >= 0 );
最低限、オブジェクトのサイズ、コンストラクタ、デストラクタを登録しないとエラーが発生する。特にこだわりがない場合は、コピーコンストラクタなど必要なものも登録する(クラス関数を使ったのは、グローバル関数を書いてコードを汚したくないので、ローカル関数の立替として書いただけなので、特に他意はない)。このように登録した型はAngelScript内で次のように普通に生成することができる。
Vec2 value(0.0f,0.0f);
3.2 参照型としてのクラスの登録
スクリプト内では生成される事を意図しないクラス、構造体のインターフェイス系は 値型として登録せず、参照型として登録する。AddRef、Releaseが登録されていることからわかるように、参照型は、その参照カウンタを増減させる方法を提供しないとならない。これは、スクリプトとC/C++をまたぐインスタンスの管理を一元的に行うため、このようになっている。他のスクリプトが結構うやむやにしている点ではあるので、きっちりかっちり行われるのは、大変ありがたい。これでC/C++でもスクリプトでもどっちでも利用されるインスタンスに関して一元的な管理が可能になる。AddRef(),Release()の登録は、メンバ関数の登録の項でとりあげるべきだったが、参照型として登録する以上この関数がないとエラー(それも非常に判明しづらい)でスクリプトが動かないので、クラスの登録と同時にやるといいと思う。
r = asEngine->RegisterObjectType("ISomeObj", 0, asOBJ_REF); assert( r >= 0 );
r = asEngine->RegisterObjectBehaviour("ISomeObj", asBEHAVE_ADDREF, "void f()",
asMETHOD(ISomeObj,AddRef), asCALL_THISCALL); assert( r >= 0 );
r = asEngine->RegisterObjectBehaviour("ISomeObj", asBEHAVE_RELEASE, "void f()",
asMETHOD(ISomeObj,Release), asCALL_THISCALL); assert( r >= 0 );
asMETHODマクロはメンバ関数に対して、asFUNCTIONは普通の関数(グローバル関数、クラス関数)に対して利用する。フックとして、asFUNCTIONを利用することも可能だ。
4. プロパティの登録
プロパティは次のように登録する。
r = asEngine->RegisterObjectProperty("Vec2", "float x_", offsetof(Vec2, x_ )); assert( r >= 0 );
ここで気をつけないといけないのが、プロパティに参照型がある場合、それがNULLになっていはいけないということだ。そうでないとアクセス時(単にそれに代入されるだけでも)に、エラーが出る。NullObjectをデフォルトで設定するなどの工夫が必要だが、非常に面倒なので、このような場合、私はわざわざ追加でメンバ関数を作成している。
/*これはtex_にアクセス時にnullとなっているとエラーとなってしまうのでお勧めしない。*/
//r = asEngine->RegisterObjectProperty("SpriteRenderDesc", "ITexture@ tex_", offsetof(SpriteRenderDesc, tex_ )); assert( r >= 0 );
/*多少不恰好だが、tex_ == NULL でも大丈夫なようにこうする。*/
r = asEngine->RegisterObjectMethod("SpriteRenderDesc", "void SetTexture( ITexture@ )", asFUNCTION(Bind::SetTexture), asCALL_CDECL_OBJLAST); assert( r >= 0 );
5. クラス、構造体のメンバ関数の登録
5.1 そのままメンバ関数を登録する場合
次のようにする。
r = asEngine->RegisterObjectMethod("SceneGraph", "void AddSprite( const SpriteRenderDesc &in )", asMETHODPR(SceneGraph, AddSprite,(const SpriteRenderDesc&), void ), asCALL_THISCALL); assert( r >= 0 );
しかし仮想関数の登録で書くように、実はメンバ関数呼び出しを関数呼び出しに置換することが可能なので、使い分けるのが面倒ならメンバ関数のバインドは5.2の方法で統一しても問題ない。ただ呼び出しのオーバーヘッドが問題になるようなレベルになるなら、直接呼び出すような形にしたほうがいいと思う。
5.2 仮想関数の登録
知らないときはかなりはまったのだが、AngelScriptには、バインドしたクラスの仮想関数の呼び出し先を解決する方法はない。実行時に失敗する。ので、一度呼び出す前のフックを作成する必要がある。これは仮想関数でない場合も利用でき、何か別の処理を噛ませたり(ちゃんと解放されているかなど確認するなど)することが可能だ。
class Bind
{
public:
static void SetTexture( ITexture* texture, SpriteRenderDesc* desc )
{
texture->Release();
desc->SetTexture( texture );
}
};
r = asEngine->RegisterObjectMethod("SpriteRenderDesc", "void SetTexture( ITexture@ )", asFUNCTION(Bind::SetTexture), asCALL_CDECL_OBJLAST); assert( r >= 0 );
#参照オブジェクトを作る時点で仮想関数があることはわかりきっていることなのに、
#いちいちフックを作るようにした意義がわからないが、多分色んな事を含めて
#こうしたほうがいいという結論に達したんだろう。多分。
6. @,+@,&in,&out,&inout
まずは &in,&out,&inoutについて。これらは、引数への修飾子(?)として用いることができる。ほとんど見たまんまだが、&inはその参照を参照のみすることを、&outはその参照を参照せず外部への戻り値として利用することを、&inoutはその両方を表現する。
そしてもうひとつ用意されているのが@。これが若干厄介だ。関数の引数として@が出てきたときの動作についてちゃんと理解していないと、バインドした関数を呼び出すたびに参照カウントが増えつづけ、解放できなくなってしまったりReleaseを呼びまくったりしてしまう。考え方としては、@がつくとカウントが増え/増えることが期待され、その参照がスクリプトからアクセスできなくなったら参照が減るということだ。たとえばC関数
void SomeFunc(ISomeObj*)
を
void SomeFunc(ISomeObj@)
としてバインドする。ISomeObjは参照型として登録されている。このときAS側からSomeFunc(someObj)として呼び出すとすると、C側のSomeFunc(ISomeObj*)では参照カウンタがひとつ増えている。参照カウンタの管理をCのSomeFunc()側で行っている場合は要注意だ。その場合は、一つ下げないといけない。これは5.2の例でやっているようにフックの中身でやるといい。ドキュメントにあった例題を沢山貼り付けておく。
/*"obj@ CreateObject()"として登録した場合*/
obj *CreateObject()
{
/*(まともな実装なら)参照カウントがC/C++側もここで1になっているので問題ない*/
return new obj();
}
/*"void StoreObject(obj@)"として登録*/
obj *o = 0;
void StoreObject(obj *newO)
{
if( o ) o->Release();
/*StoreObjectが呼び出された時点で、AngelScriptによって
AddRefされているのでここでAddRefする必要はない。*/
o = newO;
}
/*"obj@ RetrieveObject()"として登録*/
obj *RetrieveObject()
{
/*@付きで返すためAddRefしておくことが期待される*/
if( o ) o->AddRef();
return o;
}
/*"void DoSomething(obj@)"として登録*/
void DoSomething(obj *o)
{
/*@付きなので、事前にAddRef()されているのでRelease()する*/
if( o ) o->Release();
}
そしてさらに@+というのがある。例えば次のような状況を考える。
/*"obj@ ChooseObj(obj@, obj@)"として登録*/
obj *ChooseObj(obj *a, obj *b)
{
return some_condition ? a : b;
}
これはaかbがリークする危険がある。しかしこのような場合も
/*"obj@+ ChooseObj(obj@+, obj@+)"として登録*/
obj *ChooseObj(obj *a, obj *b)
{
return some_condition ? a : b;
}
とすれば、AngelScriptが参照を管理してくれるらしい。ちょっと怖い上に、動作に多少のオーバーヘッドがあるようで使いたいと思えない。
7. GCについて
バインドとは少し変わってしまうが、自分で@の動作などを確認するときにGCの動作を把握していなかったためにはまったのでここにあわせて書く。GC用のインターフェイスはLuaなどと違いかなり簡素だが、利用者がガーベッジの回収も含めて全部ユーザーが指定しないといけない。GCの動作は、asEGCFlagsのasGC_ONE_STEP,asGC_FULL_CYCLE,asGC_DESTROY_GARBAGE,asGC_DETECT_GARBAGEをasIScriptEngine::GarbageCollectに指定して行う。ここで注意しないといけないのが、asGC_ONE_STEPは(単なるRCから発生したのも含む)ガーベッジの回収自体はやってくれないということだ。
/*ガーベッジの回収は行わない。*/
asEngine_->GarbageCollect( asGC_ONE_STEP );
シーン転換時、ローディング時にasGC_FULL_CYCLEで全て回収し、ちょっと暇ができた時に( asGC_FULL_CYCLE | asGC_DESTROY_GARBAGE )でガーベッジを回収し、通常は毎フレームasGC_ONE_STEPを呼ぶのがいいと思う。
8.enumについて
スクリプトのバインドでしばし話題に出るenumのバインド。残念ながらAngelScriptでは型安全に直接バインドする方法はない。intにキャストしてグローバルにconst int として配置するしかない(とても残念!)。わざわざそんなことするくらいなら、スクリプト側に最初から定義したほうが幾分か楽であるので、私はそうしている。
だいぶ長くなってしまったが、一回要領をつかめば多分大したことない。グルーコードのジェネレーターもささっと作れる人なら比較的容易に作れるだろう。ここまでのエントリでゲーム組み込み用途として必要な部分をほとんどカバーしたのではないだろうか?(Luaで同じことを達成しようとすると。。。) AngelScriptのイージーな感じが伝わっているとうれしい。
次回は、動的リロードと最後の落ち葉拾いなどをしようと思う。