こんちには
C++とluaを連携させた時の備忘録です。
この記事は2022年12月時点の内容なので,未来から来た人は少し事情が違うかも。
Luaのバージョンは5.4.2です。
個人的な見解が入っているので参考程度に見てください。
連携すると何が出来るの?
C++からLuaを呼び出す/呼び出せることでプログラムをデータのように扱えるようになります。
よくゲーム開発で利用されているのでゲームを例として
場合を考えます。
なんとなく想像がつくと思いますが,ぐねぐね動く・追尾するなどの行動を30個ぐらいプログラムしておき,それを組み合わせて実質行動が異なる100体の敵を作成するのが主流です(意見はあると思いますが…)
このとき行動パターンの「プログラム」はプログラマが作成し,その行動パターンの「データ」を組み合わせて敵AIをプランナが作成するようにすると,プログラマは制御処理に集中でき,プランナはゲームの面白さに集中できるようになるのでお互いの得意な領域で力が発揮できるようになります(よね)
要は「同じ処理を利用して,異なるプログラムを大量生産したい」ときにLuaなどのスクリプト言語が(コンパイルなどの面から見ても)便利です。
このような性質から比較的規模が大きく,大量生産が必要な環境などで力を発揮しやすいのかなと個人的には思っています。
また,スクリプト言語はLuaである必要はありませんが,歴史的な背景からC++から呼び出されるのはLuaが主流です。
気になった方はLuaのWikipediaを読んでみてください。
ja.wikipedia.org
参考
サイト
APIの利用方法を参考にさせていただきました。
inzkyk.xyz
書籍
少し古いですが考え方は参考になると思います。
読みやすかったです(お値段的にも)
関連
よかったらこっちも見てね。
lambda00.hatenablog.com
開発環境
セットアップ
Windows + Visual Studio の条件下での初期設定についてです。
LuaはバージョンによってAPIの引数が多々異なるので注意してください。
1. Luaのダウンロード
公式サイトからソースとバイナリのどちらかを落とします。
www.lua.org
Linux向けにはMakefileが提供されていますが,
Windowsはmakeするのが面倒なのでバイナリで落とした方が楽だと思います。
download -> binaries -> History から 「Lua 5.4.2 - Release 1」 を選択してsourceforgeへ飛びます。
Windows Libraries -> Static の順に辿り 「lua-5.4.2_Win64_vc16_lib.zip」をダウンロード。
ダウンロードしたzipを適当な場所に配置します。
適当にプロジェクトを作成し,ソースコードを追加しておきます。
ここでは「空のプロジェクト」に「main.cpp」を追加したことにします。
また,今回は64ビット版を使用するのでx86 から x64 にビルド設定を変更しておきます。
プロジェクト -> プロパティ を選択してプロパティ設定画面を開きます。
C++17 設定
構成プロパティ -> 全般 から C++言語標準を C++17 に設定。
C++17に設定しましたが,別に設定しなくても大丈夫です。
luaインクルードファイル 設定
構成プロパティ -> C/C++ から 追加のインクルードディレクトリ に解凍したluaのincludeを指定します。
Cドライブ直下なら「C:\lua-5.4.2_Win64_vc16_lib\include\」となります。
リンカ 設定
構成プロパティ -> リンカー -> 入力 から 追加の依存ファイル に解凍したluaのライブラリを指定します。
Cドライブ直下なら「C:\lua-5.4.2_Win64_vc16_lib\lua54.lib」となります。
3. ビルド
ここまでで #include <lua.hpp> がコンパイル出来ることを確認しておくと良いです。
こける場合はプロパティの構成やプラットフォームが適切か確認してみてください。
Debug/Release の x64 で実行してみよう。
#include <lua.hpp>
int main()
{
lua_State* pL = luaL_newstate();
lua_close(pL);
return 0;
}
個人的によく使うだろうと思われる部分のみ記述しました。
他にも色々あるのでよかったら探してみてね。
また,ソースコードが冗長になりますが,途中から見てもわかるようにプログラムは差分ではなく,全文載せるようにしています。
APIの詳細についてはリファレンスを見てね。
1. Luaファイル読み込み
C++からLuaファイルを読み込みます。
Visual Studioからデバッガにアタッチして実行する場合はソリューションファイルの場所に配置するのが良いと思います。
#include <lua.hpp>
#include <iostream>
int main()
{
lua_State* pL = luaL_newstate();
if (luaL_dofile(pL, "./sample.lua") != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
lua_close(pL);
return 0;
}
test1 = 100;
test2 = 200;
test3 = test1 + test2;
test4 = "AIUEO";
test5 = test 1 + test 2
luaL_dofile(pL, "./sample.lua")
でluaファイルを読み込んでいますが,luaファイルに構文エラーなどがあるとエラーコードをスタックトップに配置してくれます。
C++自体は終了しないのでエラーハンドリングは忘れずに。
出力結果
./sample.lua:5: unexpected symbol near '1'
C++からLuaのグローバル変数を呼び出します(luaファイルは上と同じ)
Luaはスタックマシンなので,pushする順番に注意。
#include <lua.hpp>
#include <iostream>
int main()
{
lua_State* pL = luaL_newstate();
if (luaL_dofile(pL, "./sample.lua") != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "Sample: variable" << std::endl;
{
lua_getglobal(pL, "test1");
lua_getglobal(pL, "test3");
lua_getglobal(pL, "test2");
lua_getglobal(pL, "test4");
const int num = lua_gettop(pL);
for (int i = 1; i <= num; ++i)
{
const int type = lua_type(pL, i);
std::cout << "type=" << type << ", " << "name=" << lua_typename(pL, type) << ", " << "val=" << lua_tonumber(pL, i) << ", " << "str=" << lua_tostring(pL, i) << std::endl;
}
lua_pop(pL, num);
}
lua_close(pL);
return 0;
}
test1 = 100;
test2 = 200;
test3 = test1 + test2;
test4 = "AIUEO";
変数の型に応じて呼び出すluaのAPIが変わる点に注意。
あと,使い終わったらlua_popでスタックをお掃除しようね。
出力結果
Sample: variable
type=3, name=number, val=100, str=100
type=3, name=number, val=300, str=300
type=3, name=number, val=200, str=200
type=4, name=string, val=0, str=AIUEO
3. C++からLuaの関数を呼び出す
Luaの特徴として複数の戻り値を返せます。
いずれにせよ操作はスタックなのでそんなに難しくないね。
関数はグローバルな点に注意。
#include <lua.hpp>
#include <iostream>
int main()
{
lua_State* pL = luaL_newstate();
if (luaL_dofile(pL, "./sample.lua") != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "Sample: function C++ call Lua" << std::endl;
{
const int nargs0 = 1;
const int nresults0 = 1;
const int msgh0 = 0;
lua_getglobal(pL, "fibonacci");
lua_pushnumber(pL, 10);
if (lua_pcall(pL, nargs0, nresults0, msgh0) != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "\t" << "fibonacci=" << lua_tointeger(pL, 1) << std::endl;
const int num1 = lua_gettop(pL);
lua_pop(pL, num1);
const int nargs1 = 2;
const int nresults1 = 3;
const int msgh1 = 0;
lua_getglobal(pL, "multi");
lua_pushnumber(pL, 3);
lua_pushnumber(pL, 7);
if (lua_pcall(pL, nargs1, nresults1, msgh1) != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "\t" << "return=" << lua_tointeger(pL, 1) << ", " << lua_tointeger(pL, 2) << ", " << lua_tostring(pL, 3) << std::endl;
const int num2 = lua_gettop(pL);
lua_pop(pL, num2);
}
lua_close(pL);
return 0;
function fibonacci(val)
if val <= 0 then
return 0;
elseif val == 1 then
return 1;
end
return fibonacci(val-1) + fibonacci(val-2)
end
function multi(val1, val2)
return val1 + val2, val1 * val2, "aiueo"
end
出力結果
Sample: function C++ call Lua
fibonacci=55
return=10, 21, aiueo
4. LuaからC++の関数を呼び出す
おそらく一番よく使う用途だと思う。
基本的にはC++側からLuaで使用したい関数のコールバックを登録するだけ。
#include <lua.hpp>
#include <iostream>
int UltimateFunction(lua_State* pL)
{
const lua_Number ret = lua_tonumber(pL, 1);
lua_pop(pL, lua_gettop(pL));
const int val = static_cast<int>(ret) * 2;
lua_pushnumber(pL, val);
const int returnLuaNum = 1;
return returnLuaNum;
}
int main()
{
lua_State* pL = luaL_newstate();
if (luaL_dofile(pL, "./sample.lua") != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "Sample: function Lua call C++" << std::endl;
{
const char luaFuncName[] = "Ultimate";
lua_register(pL, luaFuncName, &UltimateFunction);
const int nargs = 0;
const int nresults = 1;
const int msgh = 0;
lua_getglobal(pL, "master");
if (lua_pcall(pL, nargs, nresults, msgh) != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "\t" << "master=" << lua_tointeger(pL, 1) << std::endl;
}
lua_close(pL);
return 0;
}
function master()
return Ultimate(1567)
end
C++ から Lua を呼び出し,Lua から C++を呼び出しているけど,そんなにややこしくないはず。
出力結果
Sample: function Lua call C++
master=3134
5. コルーチンの利用
Luaの特徴であるコルーチンをC++から利用してみます。
他の言語と同じくyieldとresumeが鍵です。
#include <lua.hpp>
#include <iostream>
int main()
{
lua_State* pL = luaL_newstate();
if (luaL_dofile(pL, "./sample.lua") != LUA_OK)
{
std::cerr << lua_tostring(pL, lua_gettop(pL)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "Sample: Coroutine" << std::endl;
{
luaL_openlibs(pL);
lua_State* pCoroutine = lua_newthread(pL);
lua_getglobal(pCoroutine, "co_routine");
lua_State* from = nullptr;
const int nargs = 0;
int nret = 0;
int cnt = 0;
while (true)
{
const int ret = lua_resume(pCoroutine, from, nargs, &nret);
if (ret == LUA_OK) { break; }
if (ret != LUA_YIELD)
{
std::cerr << lua_tostring(pCoroutine, lua_gettop(pCoroutine)) << std::endl;
lua_close(pL);
return 1;
}
std::cout << "\t" << "cnt=" << cnt++ << ", " << "ret=" << nret;
for (int i = 0; i < nret; ++i)
{
std::cout << ", " << lua_tostring(pCoroutine, i + 1);
}
std::cout << std::endl;
}
const int num = lua_gettop(pL);
lua_pop(pL, num);
}
lua_close(pL);
return 0;
}
function co_routine()
coroutine.yield("Blue", "RED");
coroutine.yield("Eyes", "EYES", 1);
coroutine.yield("White", "BLACK", 1, 2);
coroutine.yield("Dragon", "DRAGON", 1, 2, 3);
end
lua_resume()で再開させない限りLuaのスレッドは停止していますね。
これはいろいろと便利。
出力結果
Sample: Coroutine
cnt=0, ret=2, Blue, RED
cnt=1, ret=3, Eyes, EYES, 1
cnt=2, ret=4, White, BLACK, 1, 2
cnt=3, ret=5, Dragon, DRAGON, 1, 2, 3
感想
ざっくりと個人用にC++とLuaのよく利用する連携をまとめてみました。
C++とLuaの連携は良い点もありますが,Luaは仮想マシン(LuaVM)上で動作する都合上,解析されやすい点がデメリットに挙げられますね。
luaをバイナリ化する方法もありますが,C++で作成されたバイナリよりは読みやすいので解析防止に有効とは言えませんがしないよりは良いかもしれません...
また,速度面もかなり違うので重い処理はC++で行うのが良いと思われます。
C++だけでプログラムを作成するのと,C++とLuaを組み合わせてプログラムを作成する方法にはそれぞれ利点があるので使い分けていきたいと思いました。
おわり