こんちには
この記事は2022年12月時点の内容なので,未来から来た人は少し事情が違うかも。
Luaのバージョンは5.4.2です。
個人的な見解が入っているので参考程度に見てください。
連携すると何が出来るの?
C++からLuaを呼び出す/呼び出せることでプログラムをデータのように扱えるようになります。
よくゲーム開発で利用されているのでゲームを例として
- シューティングゲームで行動パターンが異なる敵を100体作りたい
場合を考えます。
なんとなく想像がつくと思いますが,ぐねぐね動く・追尾するなどの行動を30個ぐらいプログラムしておき,それを組み合わせて実質行動が異なる100体の敵を作成するのが主流です(意見はあると思いますが…)
このとき行動パターンの「プログラム」はプログラマが作成し,その行動パターンの「データ」を組み合わせて敵AIをプランナが作成するようにすると,プログラマは制御処理に集中でき,プランナはゲームの面白さに集中できるようになるのでお互いの得意な領域で力が発揮できるようになります(よね)
要は「同じ処理を利用して,異なるプログラムを大量生産したい」ときにLuaなどのスクリプト言語が(コンパイルなどの面から見ても)便利です。
このような性質から比較的規模が大きく,大量生産が必要な環境などで力を発揮しやすいのかなと個人的には思っています。
また,スクリプト言語はLuaである必要はありませんが,歴史的な背景からC++から呼び出されるのはLuaが主流です。
気になった方はLuaのWikipediaを読んでみてください。
ja.wikipedia.org
参考
サイト
APIの利用方法を参考にさせていただきました。
inzkyk.xyz
書籍
少し古いですが考え方は参考になると思います。
読みやすかったです(お値段的にも)
関連
よかったらこっちも見てね。
lambda00.hatenablog.com
開発環境
- OS: Windows 11 22H2
- IDE: Microsoft Visual Studio Community 2019 / Version 16.9.0
- 言語: C++17
- Lua: lua 5.4.2
セットアップ
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を適当な場所に配置します。
2. Visual Studioのプロジェクト設定
適当にプロジェクトを作成し,ソースコードを追加しておきます。
ここでは「空のプロジェクト」に「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; }
C++ と Lua の連携
個人的によく使うだろうと思われる部分のみ記述しました。
他にも色々あるのでよかったら探してみてね。
また,ソースコードが冗長になりますが,途中から見てもわかるようにプログラムは差分ではなく,全文載せるようにしています。
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'
2. C++からLuaのグローバル変数を呼び出す
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"); // test3だよ lua_getglobal(pL, "test2"); // 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; { // 0: フィボナッチ数列を計算させる 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); // 1: 複数の値を処理させる 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) { // Luaからの引数を取得する const lua_Number ret = lua_tonumber(pL, 1); lua_pop(pL, lua_gettop(pL)); // Luaに渡す戻り値を詰める const int val = static_cast<int>(ret) * 2; lua_pushnumber(pL, val); const int returnLuaNum = 1; // Luaに渡す戻り値の数 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側で呼び出したい関数名 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; // Luaに渡す引数の数 int nret = 0; // Luaからの戻り値の数 int cnt = 0; // ループカウンタ while (true) { const int ret = lua_resume(pCoroutine, from, nargs, &nret); // コルーチン開始/再開 if (ret == LUA_OK) { break; } // コルーチン終了 // エラーハンドリング // - LUA_OK or LUA_YIELDでない時点でおかしい if (ret != LUA_YIELD) { std::cerr << lua_tostring(pCoroutine, lua_gettop(pCoroutine)) << std::endl; lua_close(pL); return 1; } // コルーチン処理 // - retに戻り値の数が入っている 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を組み合わせてプログラムを作成する方法にはそれぞれ利点があるので使い分けていきたいと思いました。
おわり