Edited at

スクリプト言語AngelScriptの紹介


要約

静的型付けスクリプト言語(グルー言語)のAngelScriptの紹介です。


公式サイト(https://www.angelcode.com/angelscript/)

AngelScriptのクラスについては別記事に記載しています。

使用したコードはGitHubに置いてあります。

https://github.com/unknown-ds/angelscript_sample


Luaと比較した特徴

スクリプト言語で有名なLuaと比較した特徴を挙げてみます。

Lua
AngelScript

変数型付け
動的
静的

文法
Cライクな別物?
C++ライク

バインダ
別で用意する(toLua++など)
標準で装備

実行速度
速い
Luaより少し遅い(未確認)

ドキュメント
豊富
少ない


長所


  • 静的型付けなのでコード量がある程度以上の場合はコードの把握し易くなる。

  • 文法がC++にかなり似ているので学習コストが低い。


短所


  • ドキュメントが少ない。

  • アドオンやツール等が少ない。


プロジェクトの作成

angelscript_2.33.0\sdk\angelscript\projects にプロジェクトファイルが入っていますが、自前で作成してみます。


環境


  • Windows10 64bit


  • Visual Studio Express2017 for Windows Desktop


  • AngelScript ver.2.33.0



使用アドオン


  • scriptarray

  • scriptbuilder

  • scriptstdstring

  • scriptmath


ローカル環境とソリューション作成


  1. ローカル環境にプロジェクト用フォルダを用意。

  2. VisualStudioで新規プロジェクト作成(windowsコンソールアプリケーション)

  3. プロジェクトフォルダに[sdk]を作成、その下に[add_on][angelscript]フォルダを作成。[angelscript]フォルダにangelscrpt_2.33.0.zipから解凍した[sdk/angelscript]の[include]と[source]をコピー。[add_on]フォルダに[sdk/add_on]の[scriptarray]と[scriptbuilder]と[scriptstdstring]をコピー

  4. 上記をVisualStudioプロジェクトに追加。

  5. スクリプトエンジン作成、関数登録用のクラスを作成するための.cpp./hファイルを追加。

ローカルフォルダ構成

└─as_compiler(プロジェクトフォルダ)
  └─sdk
    ├─add_on
    │ ├─scriptarray
    │ ├─scriptbuilder
    │ ├─scriptmath
    │ └─scriptstdstring
    └─angelscript
      ├─include
      └─source

proj_tree.png


プリコンパイルヘッダの使用

プリコンパイルヘッダの使用のために各cppファイルに

#include "stdafx.h"

を追加。

「プリコンパイル済みヘッダー ファイルを開けません。」というエラーが出てしまう場合は、以下の設定

- プロジェクトのプロパティ

[構成プロパティ]→[C/C++]→[プリコンパイル済みヘッダー]→[プリコンパイル済みヘッダー]→[使用 (/Yu)]

- StdAfx.cppのプロパティ

[構成プロパティ]→[C/C++]→[プリコンパイル済みヘッダー]→[プリコンパイル済みヘッダー]→[作成 (/Yc)]


カスタムビルドツールの設定

カスタムビルドツールはファイルごとに設定します。

ファイルのプロパティを開き、「構成プロパティ」→「全般」→「項目の種類」から「カスタム ビルド ツール」を選択し、「適用」する。「構成プロパティ」に「カスタム ビルド ツール」の項目が表示されます。

[as_callfunc_x64_msvc_asm.asm]をプロジェクトに入れてプロパティ→[項目の種類]→[カスタムビルドツール]

▼コマンドライン

ml64.exe /c /nologo /Fo"$(Configuration)\as_callfunc_x64_msvc_asm.obj" /W3 /Zi /Ta "%(RootDir)%(Directory)\%(Filename)%(Extension)"
▼出力ファイル
$(Configuration)\as_callfunc_x64_msvc_asm.obj

これでビルドが通るはず。。:slight_smile:


実装

スクリプトに必要な処理を幾つか挙げます。


スクリプトエンジン生成

script_mgr.hにシングルトンクラスCScriptMgrを用意、初期化メソッドInit()内で準備を行います。

// スクリプトエンジンを生成

mpEngine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
if (mpEngine == 0) {
printf("Failed to create script engine.\n");
return;
}


アドオンの登録

アドオンの登録です、自前のアドオンやクラスがある場合も同じように初期化時で登録する必要があります。

// arrayアドオンの登録

RegisterScriptArray(mpEngine, true);
// stringアドオンの登録
RegisterStdString(mpEngine);
RegisterStdStringUtils(mpEngine);
// mathアドオンの登録
RegisterScriptMath(mpEngine);


C++側での関数実装

Native版はそのまま実装、Generic版はasIScriptGenericクラスにある引数取得メソッドから引数を取得します。intやfloatなどの値型の取得にはGetArgWord()やGetArgFloat()が使えます。

返り値がある場合は返り値設定メソッドSetReturn???()で設定します。(???は型名)

// スクリプトにバインドするテスト関数1(Native)

float testBindFunc1(
float _Lhs, // 引数1
float _Rhs // 引数2
)
{
return (_Lhs + _Rhs);
}

// Generic
void testBindFunc1Generic(
asIScriptGeneric *pGen
)
{
// 引数取得
// (この場合は _Lhs = pGen->GetArgFloat(0); でもよい)
float _Lhs = *reinterpret_cast<float*>(pGen->GetAddressOfArg(0));
float _Rhs = *reinterpret_cast<float*>(pGen->GetAddressOfArg(1));
// float型返り値設定
pGen->SetReturnFloat(testBindFunc1(_Lhs, _Rhs));
}


C++側で実装した関数をバインド

C++側で実装している関数の登録です。NativeとGenericをそれぞれ用意しています。

不要なら一方でも可、その場合はas_config.hにて #define AS_MAX_PORTABILITY を定義してGeneric版のみにしたほうがいいと思います。

// C++実装関数の登録、登録時のresultを細かくチェックすると良い。

if (!strstr(asGetLibraryOptions(), "AS_MAX_PORTABILITY")) {
// Native
result = mpEngine->RegisterGlobalFunction("void printf(const string &in)", asFUNCTION(asPrintf0), asCALL_CDECL); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("void printf(const string &in, ?&in)", asFUNCTION(asPrintf1), asCALL_CDECL); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("float testBindFunc1(float, float)", asFUNCTION(testBindFunc1), asCALL_CDECL); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("int testBindFunc2(array<int> &in, int)", asFUNCTION(testBindFunc2), asCALL_CDECL); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("string testBindFunc3(string)", asFUNCTION(testBindFunc3), asCALL_CDECL); assert(result >= 0);
}else{
// Generic
result = mpEngine->RegisterGlobalFunction("void printf(const string &in)", asFUNCTION(asPrintf0Generic), asCALL_GENERIC); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("void printf(const string &in, ?&in)", asFUNCTION(asPrintf1Generic), asCALL_GENERIC); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("float testBindFunc1Generic(float, float)", asFUNCTION(testBindFunc1Generic), asCALL_GENERIC); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("int testBindFunc2Generic(array<int> &in, int)", asFUNCTION(testBindFunc2Generic), asCALL_GENERIC); assert(result >= 0);
result = mpEngine->RegisterGlobalFunction("string testBindFunc3Generic(string)", asFUNCTION(testBindFunc3Generic), asCALL_GENERIC); assert(result >= 0);
}


実行

スクリプトをコンパイルの後、実行します。

また予めコンパイル後にバイトコードへ出力して、実行時はバイトコードを読んでコンパイルなしにすることもできます。


準備

int result;

asIScriptModule *pModule = mpEngine->GetModule(pModuleName);

// スクリプトコンテキストの作成、(スクリプトの実行単位で必要)
asIScriptContext *pCtx = mpEngine->CreateContext();
if (!pCtx) {
printf("Failed to create the context.\n");
mpEngine->Release();
return 0;
}


スクリプト側実装の関数をC++側で実行する1

返り値なし、引き数なしの関数testFunction1の実行例です。

// 関数を探す

asIScriptFunction *pFunc0 = pModule->GetFunctionByDecl("void testFunction1()");
// 準備
pCtx->Prepare(pFunc0);
// 実行
result = pCtx->Execute();


スクリプト側実装の関数をC++側で実行する2(エラー処理付)

返り値float、引き数(float, float)の関数testFunction2の実行例です。

エラー後はコンテキストの解放(pCtx.Release())を忘れずに行う。

asIScriptFunction *pFunc = pModule->GetFunctionByDecl("float testFunction2(float, float)");

if (pFunc == NULL) {
printf("Function not found.\n");
return 0;
}
result = pCtx->Prepare(pFunc);
if (result != 0) {
printf("Failed to prepare the context.\n");
return 0;
}
// 引き数をセット
result = pCtx->SetArgFloat(0, 5.0f);
if (result != 0) {
printf("Failed to set arments.[%d]\n", result);
return 0;
}
result = pCtx->SetArgFloat(1, 3.0f);
if (result != 0) {
printf("Failed to set arments.[%d]\n", result);
return 0;
}

// 実行
result = pCtx->Execute();
if (result != asEXECUTION_FINISHED) {
// 通常終了しなかった場合
if (result == asEXECUTION_ABORTED){
// 割り込み強制終了
printf("Script aborted.");
}else
if (result == asEXECUTION_EXCEPTION){
// 例外発生
printf("Script threw Exception.\n");
asIScriptFunction *func = pCtx->GetExceptionFunction();
printf("%s, %s, %s[line:%d] %s\n"
, func->GetDeclaration()
, func->GetModuleName()
, func->GetScriptSectionName()
, pCtx->GetExceptionLineNumber()
, pCtx->GetExceptionString());
}else
if (result == asEXECUTION_SUSPENDED){
// 中断終了
int line = pCtx->GetExceptionLineNumber();
printf("Script suspend.[line:%d]\n", line);
}else{
// 不明なエラー
printf("The script ended for some unforeseen reason (%d).\n", result);
}
}else{
// 通常終了
float returnValue = pCtx->GetReturnFloat(); // 返り値取得
printf("C++ testFunction2: %f\n", returnValue);
}


C++側で実装している関数をスクリプト側で呼ぶ

RegisterGlobalFunction()登録した第1引数の名前でそのまま呼べます。


test00.as

float ret1 = testBindFunc1(1.0f, 2.0f);   // -> 3.0f


呼び出しミス(引数の数や型の間違い)の場合は以下のようなエラーが出ます。

スクリプト名とエラー行数がでます。ただしエラーがあると即終了するため、複数エラーがあってもまとめては出力してくれないようです。

// スクリプトファイルtest00.asにて、testBindFunc1の引数を間違えたエラー例(登録されている引数タイプが見当たらない)

test00.as (38, 1) : INFORMATION : Compiling void testFunction1()
test00.as (50, 15) : ERROR : No matching signatures to 'testBindFunc1(float&, float, const float)'
Assertion failed: pMsg->type != asMSGTYPE_ERROR, file c:\as_compiler\script_mgr.cpp, line 186


その他

インターフェイスや列挙型も実装可能なのでかなりC++に近い。

スクリプト文法での違う点をいくつか列挙します。


キャスト

// キャスト用の関数がある感じです。

int a = 10;
float b = float(a); // -> 10.0f


配列

// C++ の std::array ではなく std::vectorに近い。

array<float> arr = {1.0f, 2.0f, 3.0f};
arr.insertLast(4.0f);  // 最後尾に追加


クラスオブジェクト

class CTest {}           // CTestクラスがあるとして(中身省略)

CTest ctest = CTest(); // オブジェクト生成
ref @hdle = CTest(); // 参照(アドオンscripthandle必須)


まとめ

C++ライクで使いやすいスクリプト言語だと思います。

使用する方が増えてくれることを願っています。:pray: