1. ホーム
  2. スクリプト・コラム
  3. ルア

Luaチュートリアル(XVII)。C言語API入門

2022-02-12 11:12:38

Luaは組み込み型のスクリプト言語、つまりLuaは単体のプログラムではなく、実際には大きく2つの適用形態があります。1つ目は、C/C++がLuaのコードを呼び出すメインプログラムであり、Luaは"拡張可能な言語"として考えることができ、これを"アプリケーションコード"と呼んでいます。2つ目の形態では、Luaが制御し、C/C++コードはLuaのquot;ライブラリコード"として動作します。どちらの場合も、2つの言語間のコミュニケーションは、Luaが提供するC言語のAPIを通じて行われます。

1. 基本的なこと

C APIは、C/C++のコードがLuaと対話できるようにするための関数群です。Luaグローバル変数の読み書き、Lua関数の呼び出し、Luaコードの実行、Luaコードが呼び出すC関数の登録などが含まれます。ここでは、簡単なサンプルコードを紹介します。

コピーコード コードは以下の通りです。

#include <stdio.h>
#include <string.h>
#include <lua.hpp>
#include <luxlib.h>
#include <lualib.h>

int main(void)
{
    const char* buff = "print(\"hello\")";
    int error;
    lua_State* L = luaL_newstate();
    luaL_openlibs(L);

    error = luaL_loadbuffer(L,buff,strlen(buff),"line") || lua_pcall(L,0,0,0);
    int s = lua_gettop(L);
    if (error) {
        fprintf(stderr,"%s",lua_tostring(L,-1));
        lua_pop(L,1);
    }
    lua_close(L);
    return 0;
}


上記のコードについて、具体的に説明すると次のようになります。

    1). 上記のコードは、私のCプロジェクトではなく、私のC++プロジェクトに基づいているため、インクルードされたヘッダーファイルはlua.hppです。もし、Cプロジェクトだったら、lua.hをインクルードすればいいのです。
    2). Luaライブラリはグローバル変数を定義せず、全ての状態を動的構造体lua_Stateに保持し、後の全てのC APIはそのポインタを第1引数として必要とします。
    3). luaL_openlibs関数は、ioライブラリや文字列ライブラリなど、Luaの標準ライブラリすべてを開くために使用されます。
    4). luaL_loadbuffer buff内のLuaコードをコンパイルし、コンパイルしたブロックを仮想スタックに押し付けながら、エラーがなければ0を返します。
    5). lua_pcall関数はブロックをスタックからポップオフし、ブロックをプロテクトモードで実行します。実行に成功すると0を返し、そうでなければエラーメッセージがスタックに押されます。
    6). lua_tostring関数の-1はスタックの一番上にあるインデックス値を示し、スタックの一番下にあるインデックス値は1、...となります。この関数は、スタックの一番上にあるエラーメッセージを返しますが、スタックからポップアップすることはありません。
    7). lua_pop は、仮想スタックから指定された数の要素をポップするマクロで、1 はスタックの先頭だけをポップすることを意味します。
    8). lua_close は、状態ポインタが参照するリソースを解放するために使用されます。

    2.スタック

    LuaとC言語の間でデータをやり取りする場合、Luaは動的型付け、Cは静的型付け、Luaは自動メモリ管理、Cは手動メモリ管理など、両言語の間には大きな違いがあります。これらの問題を解決するために、Luaの設計者は両者間のデータのやりとりに仮想スタックを媒介として使用しました。C/C++プログラムでLuaから値を取得するには、LuaのC API関数を呼び出すだけで、Luaは指定された値をスタックにプレスします。Luaに値を渡すには、値をスタックに押し込んだ後、LuaのC APIを呼び出す必要があり、Luaはその値を取得しスタックから取り出します。Luaは、異なる種類の値をスタックに押したり、異なる種類の値をスタックから取り出したりできるように、それぞれの種類に対応した固有の関数を用意しています。

1). 要素の押し込み

    Luaは、各C型に対応するC API関数を持っており、例えば、以下のようになります。

コピーコード コードは以下の通りです。

    void lua_pushnil(lua_State* L); --nil value
    void lua_pushboolean(lua_State* L, int b); --boolean
    void lua_pushnumber(lua_State* L, lua_Number n); -- Floating point number
    void lua_pushinteger(lua_State* L, lua_Integer n); -- integer
    void lua_pushlstring(lua_State* L, const char* s, size_t len); -- memory data of specified length
    void lua_pushstring(lua_State* L, const char* s); -- A zero-terminated string whose length can be derived from strlen.

    文字列データについては、Luaはポインターを保持せず、API時に内部コピーの生成を呼び出すため、これらの関数が戻った直後でも、文字列ポインターの解放や変更は問題なく行えます。
    スタックにデータを押し込む際、以下の関数を呼び出すことで、スタックに十分な空き容量があるかどうかを判断することができます。一般的にLuaは20個のスロットを確保しており、引数の多い関数を除けば、通常のアプリケーションではこれで十分です。
    int lua_checkstack(lua_State* L, int extra) -- 余分な空きスロット数の取得を期待し、拡張して取得できない場合はfalseを返します。 
    2). クエリ要素。

    APIでは、スタック上の要素を参照するためにquot;index"を使用しており、スタックに最初に押されたものが1、2、...といった具合になります。また、負の数をインデックスとして使用することも可能で、-1がスタックの先頭、-2がスタックの先頭より下の要素、といった具合になります。

    Luaは、返された要素の型をチェックするために、次のような特定の関数群を提供しています。

コピーコード コードは以下の通りです。

    int lua_isboolean (lua_State *L, int index);
    int lua_iscfunction (lua_State *L, int index);
    int lua_isfunction (lua_State *L, int index);
    int lua_isnil (lua_State *L, int index);
    int lua_islightuserdata (lua_State *L, int index);
    int lua_isnumber (lua_State *L, int index);
    int lua_isstring (lua_State *L, int index);
    int lua_istable (lua_State *L, int index);
    int lua_isuserdata (lua_State *L, int index);

    上記の関数は成功すれば1を、そうでなければ0を返します。注意すべきは、lua_isnumberでは、値が数値型かどうかではなく、値が数値型に変換できるかどうかをチェックしている点です。
    また、Luaには要素の型を取得するための関数lua_typeが用意されています。この関数のプロトタイプは以下の通りです。
コピーコード コードは以下の通りです。

    int lua_type (lua_State *L, int index);

    この関数の戻り値は定数値のセットで、LUA_TNIL, LUA_TNUMBER, LUA_TBOOLEAN, LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD, LUA_ これらの定数は通常 switch 文で使用されるものです。
    上記の関数に加え、Luaは以下のような変換関数を提供しています。
コピーコード コードは以下の通りです。

    int lua_toboolean (lua_State *L, int index);
    lua_CFunction lua_tocfunction (lua_State *L, int index);
    lua_Integer lua_tointeger (lua_State *L, int index);   
    const char *lua_tolstring (lua_State *L, int index, size_t *len);
    lua_Number lua_tonumber (lua_State *L, int index);
    const void *lua_topointer (lua_State *L, int index);
    const char *lua_tostring (lua_State *L, int index);
    void *lua_touserdata (lua_State *L, int index);
    --string type returns the length of the string, table type returns the equivalent of the operator '#', and userdata type returns the length of the allocated memory block.
    size_t lua_objlen (lua_State *L, int index); 

    上記の関数について、lua_toboolean、lua_tonumber、lua_tointeger、lua_objlenは呼び出しに失敗すると0を返し、その他の関数はNULLを返す。多くの場合0はエラー判定にあまり有効ではないが、ANSI Cはエラーを示す他の値を用意していない。そのため、これらの関数については、場合によっては lua_is* 系の関数を使用して型が正しいかどうかを判断する必要がありますが、その他の関数については、単に戻り値が NULL かどうかを判断すればよいのです。
    lua_tolstring関数が返す内部文字列へのポインタについては、そのインデックスが指す要素が排出された後も有効である保証はありません。この関数が返す文字列の末尾には、すべて「0」が付きます。
    上記の機能の一部を実演するために、以下のようなツール関数が提供されています。
コピーコード コードは以下の通りです。

static void stackDump(lua_State* L)
{
    int top = lua_gettop(L);
    for (int i = 1; i <= top; ++i) {
        int t = lua_type(L,i);
        switch(t) {
        case LUA_TSTRING:
            printf("'%s'",lua_tostring(L,i));
            break;
        case LUA_TBOOLEAN:
            printf(lua_toboolean(L,i) ? "true" : "false");
            break;
        case LUA_TNUMBER:
            printf("%g",lua_tonumber(L,i));
            break;
        default:
            printf("%s",lua_typename(L,t));
            break;
        }
        printf("");
    }
    printf("\n");
}

  3). その他のスタック操作関数。

    LuaのC APIには、上記のデータ交換関数に加え、以下のような仮想スタックを操作するための一般的な関数が用意されています。

コピーコード コードは以下の通りです。

    int lua_gettop(lua_State* L); -- Return the number of elements in the stack.
    void lua_settop(lua_State* L, int index); -- Set the top of the stack to the specified index value.
    void lua_pushvalue(lua_State* L, int index); -- Presses a copy of the element with the specified index onto the stack.
    void lua_remove(lua_State* L, int index); -- Removes the element at the specified index, and the element above it is automatically moved down.
    void lua_insert(lua_State* L, int index); -- Inserts the element at the top of the stack to the location pointed to by this index value.
    void lua_replace(lua_State* L, int index); -- Pops the top-of-stack element and sets the value to the specified index.

    Luaには、指定した数の要素をポップアウトさせるマクロも用意されています。
コピーコード コードは以下の通りです。
#define lua_pop(L,n) lua_settop(L, -(n) - 1)   

    次のコード例をご覧ください。
コピーコード コードは以下の通りです。

int main()
{
    lua_State* L = luaL_newstate();
    lua_pushboolean(L,1);
    lua_pushnumber(L,10);
    lua_pushnil(L);
    lua_pushstring(L,"hello");
    stackDump(L); //true 10 nil 'hello'

    lua_pushvalue(L,-4);
    stackDump(L); //true 10 nil 'hello' true

    lua_replace(L,3);
    stackDump(L); //true 10 true 'hello'

    lua_settop(L,6);
    stackDump(L); //true 10 true 'hello' nil nil

    lua_remove(L,-3);
    stackDump(L); //true 10 true nil nil

    lua_settop(L,-5);
    stackDump(L); //true

    lua_close(L);
    return 0;
}

3. C API のエラー処理。

    1). CプログラムがLuaコードを呼び出す際のエラー処理。

    通常、アプリケーションコードは "unprotected" モードで実行されます。そのため、Luaはquot;out of memory"のようなエラーを発見した場合、quot;emergency"関数を呼び出してCプログラムに通知し、アプリケーションを終了させるしかありません。lua_atpanicを使用して、独自の"emergency"関数を設定することができます。Luaのエラーが発生してもアプリケーションを終了させないようにするには、lua_pcall関数を呼び出してLuaのコードをプロテクトモードで実行します。こうすることで、lua_pcallはメモリエラーが発生した際にエラーコードを返し、インタープリタを一貫性のある状態にリセットします。LuaでC言語のコードを保護するには、lua_cpall関数を使用します。この関数は、C関数を引数として受け取り、そのC関数を呼び出すことになります。
    2). LuaはCのプログラムを呼び出します。

    一般的に、Luaから呼び出されたC関数がエラーを検出した場合、lua_errorを呼び出し、Lua内のクリーンアップが必要なリソースをすべてクリーンアップしてから、エラーメッセージとともに実行を開始したlua_pcallにジャンプバックする必要があります。