おもちゃバコ

中身スカスカ♡

C++にluaを組み込んでみた

こんちには

C++luaを連携させた時の備忘録です。

この記事は2022年12月時点の内容なので,未来から来た人は少し事情が違うかも。
Luaのバージョンは5.4.2です。

個人的な見解が入っているので参考程度に見てください。


連携すると何が出来るの?

C++からLuaを呼び出す/呼び出せることでプログラムデータのように扱えるようになります。

よくゲーム開発で利用されているのでゲームを例として

場合を考えます。

なんとなく想像がつくと思いますが,ぐねぐね動く・追尾するなどの行動を30個ぐらいプログラムしておき,それを組み合わせて実質行動が異なる100体の敵を作成するのが主流です(意見はあると思いますが…)

このとき行動パターンの「プログラム」はプログラマが作成し,その行動パターンの「データ」を組み合わせて敵AIをプランナが作成するようにすると,プログラマは制御処理に集中でき,プランナはゲームの面白さに集中できるようになるのでお互いの得意な領域で力が発揮できるようになります(よね)

要は「同じ処理を利用して,異なるプログラムを大量生産したい」ときにLuaなどのスクリプト言語が(コンパイルなどの面から見ても)便利です。
このような性質から比較的規模が大きく,大量生産が必要な環境などで力を発揮しやすいのかなと個人的には思っています。

また,スクリプト言語Luaである必要はありませんが,歴史的な背景からC++から呼び出されるのはLuaが主流です。
気になった方はLuaWikipediaを読んでみてください。
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を適当な場所に配置します。

2. Visual Studioのプロジェクト設定

適当にプロジェクトを作成し,ソースコードを追加しておきます。
ここでは「空のプロジェクト」に「main.cpp」を追加したことにします。
また,今回は64ビット版を使用するのでx86 から x64 にビルド設定を変更しておきます。

プロジェクト -> プロパティ を選択してプロパティ設定画面を開きます。

  1. C++17 設定
    構成プロパティ -> 全般 から C++言語標準を C++17 に設定。
    C++17に設定しましたが,別に設定しなくても大丈夫です。

  2. luaインクルードファイル 設定
    構成プロパティ -> C/C++ から 追加のインクルードディレクトリ に解凍したluaのincludeを指定します。
    Cドライブ直下なら「C:\lua-5.4.2_Win64_vc16_lib\include\」となります。

  3. リンカ 設定
    構成プロパティ -> リンカー -> 入力 から 追加の依存ファイル に解凍した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";

変数の型に応じて呼び出すluaAPIが変わる点に注意。
あと,使い終わったら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を組み合わせてプログラムを作成する方法にはそれぞれ利点があるので使い分けていきたいと思いました。

おわり