##どうしようもない僕に天使が降りてきた
アプリケーションにスクリプト言語を組み込みたくなった時、いろいろな選択肢があります。
最近自分がちょうどやりたいこととバッチリ噛み合った、AngelScriptを紹介しようと思います。
この記事の対象のバージョンは、AngelScript 2.29.2 (October 21st, 2014)です。
まずはスクリプト言語の選択に迷っている人のために、
自分が使ってみた経験を含めて
まずは長所と短所を整理してみようと思います。
##特徴と短所
特徴
・zlibライセンスで使い易い
・多くのプラットフォームで動作する(こちらは公式ページをご確認ください)
・アプリケーション側はC++で制御
・スクリプトの構文がC++とても似ていて、学習コストが低い
・標準でスクリプトとC++のバインディング機能がある
・スクリプトを実行前にコンパイルするプロセスがあり、構文チェックが楽
・しっかりとしたデバッグ機能がある
短所
・標準のコンテナクラスが微妙、特にDictionary
・どうもスクリプトをコンパイルしたバイナリの書き出し読み出しが不安定(自分の主観です。自分のテスト環境はosx yosemite, xcode6.1.1です。この環境だけかもしれません)
##導入
こちらは各環境ごと違っているので、自分の環境(osx yosemite, xcode6.1.1)の場合を軽く紹介する程度にします。
1、ソースコードを直接プロジェクトに追加
ちゃんとビルドする方法もありますが、おそらく楽でしょう。
ソースは、angelscript/source以下にアドオン以外はすべて揃っています。
MSVC周りのソース
as_callfunc_arm_msvc.asm,
as_callfunc_x64_mingw.cpp,
as_callfunc_x64_msvc_asm.asm,
as_callfunc_x64_msvc.cpp
は省いておくと良いでしょう。
また、angelscript/angelscript.hもheader search pathで見えるようにするか、同様にプロジェクトに追加します。
2、コンパイルオプションの設定
公式リファレンスの、
AngelScript - Manual - Getting started - Compile the library - GNUC based compilers
の所に、-fno-strict-aliasingを指定する、と記述があるので、一応フラグをソースに設定しておきましょう。
これですぐに使い始める事ができます。
##Hello World!
大抵のブログではこの辺りでハローワールド的なプログラムの解説が入るのですが、
ぶっちゃけ公式のチュートリアルサンプルをなぞるだけだと、面白くないので、紹介するだけにとどめます。
samples/tutorial/source/main.cpp
こちらを見てみれば、スクリプトの実行の手順は把握することができます。
##AngelScriptのツボ
自分の使ってみた感触をもとに、いいかんじにAngelScriptと仲良くなるためのTipsを幾つか紹介したいと思います。
###クラスの役割分担
####asIScriptEngine
これはまず最初にasCreateScriptEngine(ANGELSCRIPT_VERSION)で生成すべきクラスです。
スクリプトにC, C++の関数やクラスをエクスポート(Register〜系)する、
スクリプトのコンパイルエラーメッセージを受け取るためのコールバック(SetMessageCallback)の設定、などを担当します。
また、スクリプトは、asIScriptModuleとして扱われ、常にこのasIScriptEngineが内部の管理されています。asIScriptModuleの管理はあくまでasIScriptEngineに任せるのが普通のようです。
####asIScriptModule
スクリプトを管理するクラスです。
コンパイルもasIScriptModuleで行います。
スクリプトを実行するには以下の手順をとります。
新しくモジュールを作成
// _engine は asIScriptEngine
asIScriptModule *newModule = _engine->GetModule("モジュール名" asGM_ALWAYS_CREATE);
asIScriptEngineは、
内部で モジュール名-asIScriptModule と結びつけてオブジェクトを管理しています。
なのでasIScriptModuleは、
モジュール名でasIScriptEngineから取り出すか、
もしくはモジュールのポインタを保持しておくと良いでしょう。
自分はライブラリの作法を尊重して、モジュール名で必要になった時に取り出すような実装で運用しています。
スクリプトを読み込ませる
ひとまずセクション名は固定で良いと思います。
newModule->AddScriptSection("セクション名", "スクリプトソースコード");
スクリプトのコンパイル
int build_result = newModule->Build();
if(build_result == asSUCCESS) {
// 成功!
}
この時、コンパイルエラーがあった場合、
SetMessageCallbackで登録した関数に報告されます。
####asIScriptContext
スクリプト実行時のタイムアウトなどの割り込み処理コールバック(SetLineCallback)の設定、
スクリプトに渡す引数や、返ってくる戻り値の制御、
またスクリプトのデバッグなど、
スクリプトの実行に関するあれこれを担当します。
作成には、asIScriptEngineのCreateContext関数を使います。
※タイムアウトはひとまず実装しておいて損はないと思います。
実装しておかないと、スクリプトの中に無限ループがあると、アプリケーション側からどうすることもできなくなってしまうからです。
####asIScriptFunction
コンパイルが成功したモジュールから、関数をasIScriptFunctionとして取り出し、
実行させることができます。
// スクリプト内のmain関数を指定
asIScriptFunction *functionMain = module->GetFunctionByDecl("void main()");
// _context は asIScriptContext
_context->Prepare(functionMain);
int execute_result = _context->Execute();
execute_resultではスクリプト内で例外が投げられれたことの検知もここでできます。
正常終了の場合はasEXECUTION_FINISHEDが返ってきます。
###関数の戻り値をしつこくチェックしよう
angelscriptの多くの関数は、
エラーがあった時に1以上の数字をエラーコードとして報告できるようになっています。
なので、
int r = 0;
r = some_operation1(); assert( r >= 0 );
r = some_operation2(); assert( r >= 0 );
r = some_operation3(); assert( r >= 0 );
のようにしつこくエラーチェックを書いておくと、
開発が非常にはかどります。
このことは公式のリファレンスでも推奨されています。
###boost::intrusive_ptr
主要なAngelScriptのクラスは、それ自身が参照カウントを持ち、それで寿命を管理するようになっています。なので、boost::intrusive_ptrを使うと、メモリ管理が楽だと思います。
static inline void intrusive_ptr_add_ref(asIScriptEngine *ptr)
{
ptr->AddRef();
}
static inline void intrusive_ptr_release(asIScriptEngine *ptr)
{
ptr->Release();
}
などとやっておくと、
boost::intrusive_ptr<asIScriptEngine>
とかけて便利です。
###アドオン
AngelScriptには幾つか標準にアドオンがあります。
アドオンといっても、文字列や配列はアドオンという形で提供されているので、
しっかり目を通して、必要なものを入れておいたほうが良いです。
抜粋して紹介します。
####add_on/scriptarray
std::vectorのようなジェネリックな配列クラスです。
ただし、使い勝手をうまくC++から輸入できていません。
#define AS_USE_STLNAMES 1
とすることで、多少std::vector<>っぽいネーミングで使うこともできます。多少ですが・・・。
一応ソートなど便利関数もあります。
####scriptstdstring
std::stringを文字列クラスとして登録します。これはまず導入したほうが良いでしょう。
####scriptmath
数値計算などの関数のエクスポートがまとめられています。
自分で登録処理を書かなくて良いので使うときは便利です。
####scriptdictionary
辞書クラス・・・なのですが、std::mapの代替にはならず、とても残念な状態です。
あまりお勧めはできません。
####asIScriptContext::SetUserDataを活用しよう
例えば、スクリプトから呼び出す関数として、以下のような実装があったとします。
void print(std::string text)
{
std::cout << text;
}
通常このような関数を作るとき、std::coutに出す実装だけで済む場合は稀でしょう。
別な場所に出力したい場合、グローバル変数を使うのも一つですが、
asIScriptContext::SetUserDataを使うと便利です。
というのも、
void print(std::string text)
{
asIScriptContext *scriptContext = asGetActiveContext();
void *userData = scriptContext->GetUserData();
std::cout << text;
}
のようにスクリプトのコンテキストを通じて自由に使えるポインタが手に入るからです。
std::stringstreamなどを用意しておけば、特定の文字列バッファにデータを突っ込んで行くことなどがスマートに実装できます。
####まとめ
angel scriptは他のスクリプトには無い面白い特徴があり、
弱点もありますが、使い方によっては十分活躍できる場面もあるのではないでしょうか。自分はあります。
是非みなさんも触ってみてください。