userdataとmetatableを用いたLuaの機能拡張

いちごパック > Luaの解説 > userdataとmetatableを用いたLuaの機能拡張
このページは、C言語からLuaを制御する方法を理解している方向けの説明です。 Luaの制御方法をご存じでない方は、 まずLuaのC言語インターフェースをお読みください。

Luaの機能拡張とmetatable

C/C++言語で関数を実装し、それをLua関数の引数として呼び出すか、 予めLuaのグローバル変数などとして保存したうえでLuaの関数を呼び出せば、 Luaの機能拡張は可能です。
しかしながら、C/C++言語による拡張機能が状態を持つこともあります。 このような場合の実装方法の1つとして、ここでは、 userdataとmetatableを用いてC言語側とLuaスクリプト側でやりとりする方法を説明します。

userdata(full)

Luaでは、C言語専用のメモリをuserdataという形で扱えます。 userdataにはメモリブロック(full)とポインタ(light)があります。 これらのうち、fullのuserdataに対しては、 各userdataに対して個別のmetatableを割り当てることができます。 個別のmetatableを利用するために、ここではfullのuserdataを利用します。 metatableについては後ほど説明します。
userdata(full)を利用するための関数は3つあります。
/* 新しいuserdata(full)の確保 */
void* lua_newuserdata(lua_State* L, size_t datasize);
/* userdata(full)のポインタ取得 */
void* lua_touserdata(lua_State* L, int stackindex);
/* userdata(full/light)であれば1、それ以外なら0を返す */
int lua_isuserdata(lua_State* L, int stackindex);
fullのuserdataはlua_newuserdata(L,datasize)で確保できます。 この関数は確保されたuserdataをLuaスタックにpushし、 メモリの実体をポインタ(void*)として返します。 また、Luaスタックにおかれたuserdataに対してlua_touserdata(L,stackindex)を呼び出すと、 userdataへのポインタ(void*)を取得できます。
userdataとして確保されたメモリは、numberやstringと同様、変数として扱われます。 また、Luaの変数としては、userdata(full)は参照として扱われます。 変数の代入によってコピーが作られることはありません。 userdata(full)は参照されなくなった後で、自動的に解放されます。 例えば、lua_newuserdata()でpushされた変数を、どのLua変数としても保存せずにpopすれば、 (void*として返されたポインタをC言語側から利用したくても)そのuserdataは解放されます。

metatable

Luaでは、metatableという機能を使って、未定義の演算を定義できます。 metatableはtable型変数です。 演算を定義するには、要素として__addや__subなどのkeyを持つ関数を持たせます。
metatableは、keyとして演算の名前、valueとして演算の関数を持つtableにすぎませんので、 通常のtable操作関数を使って用意できます。 また、すべてC言語の関数であり、予めkeyとvalueの配列が用意できる場合は、 luaL_setfuncs()やluaL_newlib()も使えます。
luaL_setfuncs()は、pushされたtableにkeyとvalueを設定します。 tableは、Luaスタックにpushされたままの状態になります。 引数として与えるkey_valueはポインタでも問題ありません。 lua_pushcclosure()と同じように、 tableより後にnum_upvalues個の変数をpushしておき、num_upvaluesを1以上とすると、 関数呼び出し時にはそれらの変数にアクセスできるようになります。 num_upvaluesを1以上とした場合、tableはpopされませんが、pushされたnum_upvalues個の変数はpopされます。
luaL_newlib()は、新しいtableを確保し、そのtableにkeyとvalueを設定します。 確保されたtableは、Luaスタックにpushされた状態になります。 luaL_newlib()は関数ではなく、関数型マクロです。マクロの制約により、 引数として与えるkey_valuesは本物の配列である必要があります。
luaL_newlib()とluaL_setfuncs()のいずれでも、 配列の最後のエントリはname、funcともNULLとする必要があります。
typedef struct {
    const char*   name; /* key (関数名) */
    lua_CFunction func; /* value (関数) */
} luaL_Reg;
/* key_values設定済みのtableを作成してpushする */
void luaL_newlib(lua_State* L, const luaL_Reg key_values[]);
/* pushされたtableにkey_valuesを設定する。num_upvalues>0なら、key_valuesに与えられたすべての関数をcclosureとして扱う */
void luaL_setfuncs(lua_State* L, const luaL_Reg* key_values, int num_upvalues);
userdataに対するmetatableは、registryと呼ばれるグローバルtableで管理されています。 Luaのregistryを直接操作することも可能ですが、 registryを操作する次の3つの関数が補助ライブラリに用意されていますので、 ここではこれらを使った方法を説明します。
/* まだmetatableが存在しないなら、新しいmetatableを確保し、keyがtable_nameのtableとしてregistryに登録する。metatableをpushする */
int luaL_newmetatable(lua_State* L, const char* table_name);
/* registryに登録された、keyがtable_nameのtableをpushする */
int luaL_getmetatable(lua_State* L, const char* table_name);
/* 最後にpushされた変数のmetatableとして、keyがtable_nameのtableを設定する */
void luaL_setmetatable(lua_State* L, const char* table_name);
したがって、例えば次のようにmetatableをregistryに登録します。
int ichigo_add(lua_State* L);
int ichigo_tostring(lua_State* L);
const luaL_Reg key_values[] =
{
    { "__add", ichigo_add },
    { "__tostring", ichigo_tostring },
    { NULL, NULL }
};

void init_metatable(lua_State* L) { /* registryにmetatable用のグローバルtableを確保 */ luaL_newmetatable(L, "ichigometa"); /* metatable用のグローバルtableに関数を設定 */ luaL_setfuncs(L, key_values, 0); /* 不要になったmetatableをpop */ lua_pop(L,1); }
このコードでは、ichigometaというkeyを持つmetatableをregistryに登録し、 関数を設定しています。
metatableを作れば、userdataにmetatableを関連付けることでそのmetatableが使えます。 例えば次のように、userdataを作り、そのメンバを初期化するとともにmetatableを関連付ける関数を用意しておくと良いでしょう。
ichigo_data* ichigo_data_new(lua_State* L)
{
    ichigo_data* vret;

/* userdata(full)を確保 */ vret = (ichigo_data*)lua_newuserdata(L, sizeof(ichigo_data)); /* vretのメンバを初期化 */ vret->timestamp = 0; /* vretのmetatableとして、registryに登録されたmetatableを設定 */ luaL_setmetatable(L, TABLE_NAME); return vret; }
また、Luaから今回のuserdataを確保するための関数init_utc()を用意し、 予めLuaのグローバル変数として保存しておきます。
int ichigo_utc(lua_State* L)
{
    ichigo_data* vret;

vret = ichigo_data_new(L); vret->timestamp = time(NULL); return 1; } /* Luaのグローバル関数ichigoutcを登録する関数。main関数から呼び出す */ void init_function(lua_State* L) { /* C言語の関数をpush */ lua_pushcfunction(L,ichigo_utc); /* Luaのglobal変数として保存し、pop */ lua_setglobal(L,"ichigoutc"); }

__indexを使ったメソッドの実装

Luaでは、metatableのkeyとして__indexを持つtableが存在し、 そのtableがkeyとしてfunc、valueとして関数を持つ場合は、 obj.func(args)という呼び出しを__index[name](args)という呼び出しとして扱われます。 また、Luaスクリプトでは、obj:func(args)という呼び出しは、 obj.func(obj,args)という呼び出しとして扱われます。
したがって、metatableに__indexという名前のtableを登録しておけば、 objの操作をobj:func(args)として呼び出すことができるようになり、 Luaコードをオブジェクト指向の形で書けるようになります。
__indexに登録するtableはkeyとしてfunc、valueとして関数を持っていれば何でもかまいません。luaL_newmetatable()やluaL_getmetatable()でまずmetatableをpushし、 次にkeyとして__indexをpush、その次にvalueとして登録したいtableをpushして、 lua_settable(L, -3)を呼び出せば登録できます。 lua_settable()の呼び出し後、keyとvalueはpopされますがmetatableはそのままです。 Luaスタックから削除する場合はlua_pop()やlua_remove()を呼び出します。
例として、先のinit_metatable()に__indexを追加するコードを示します。 この例では関数を1つだけ追加していますが、複数の関数をまとめて追加してもかまいません。
int ichigo_japanese_era(lua_State* L);
const luaL_Reg key_values_methods[] =
{
    { "japanese_era", ichigo_japanese_era },
    { NULL, NULL }
};

void init_metatable(lua_State* L) { /* registryにmetatable用のグローバルtableを確保 */ luaL_newmetatable(L, TABLE_NAME); printf("newmetatable: stack = %d\n",(int)lua_gettop(L));
/* metatable用のグローバルtableに関数を設定 */ luaL_setfuncs(L, key_values_meta, 0); printf("setfuncs: stack = %d\n",(int)lua_gettop(L));
/* metatableの__indexに、keyとしてメソッド名を持つtableを設定 */ lua_pushliteral(L, "__index"); /* key */ luaL_newlib(L, key_values_methods); /* value */ lua_settable(L, -3); /* -3: pushされたmetatable */ printf("setfuncs: stack = %d\n",(int)lua_gettop(L));
/* 不要になったmetatableをpop */ lua_pop(L,1); }
__indexを介して追加したコードは、 Luaスクリプトからobj:japanese_era()の形で呼び出せます。

オブジェクト指向コードの1例

日付を管理するC言語のオブジェクトichigo_dataと、 時間を進める演算__add、文字列に変換する演算__tostring、 日付を和歴(昭和・平成)の文字列に変換するメソッドjapanese_era()を実装し、 Luaからオブジェクト指向の形で呼び出す1例を示します。
この例はC言語で実装していますが、 C++言語で実装する場合は構造体ではなくクラスとして実装し、 クラスをnewする際にplacement newを使うと良いでしょう。
#include <lua.h> /* lua_ */
#include <lauxlib.h> /* luaL_ */
#include <lualib.h> /* luaL_openlibs() */
#include <stdio.h>
#include <string.h>
#include <time.h>

#define SCRIPT_NAME "ichigometa1.lua"
#define TABLE_NAME "ichigometa"
#define FUNCTION_NAME "ichigoutc"

typedef struct
{
    time_t timestamp; /* 時刻(秒単位) */
} ichigo_data;

/* metatable設定済みの新しいichigo_dataをpushする関数 */
ichigo_data* ichigo_data_new(lua_State* L);

int ichigo_add(lua_State* L)
{
    ichigo_data* v1;
    lua_Number v2;
    ichigo_data* vret;

    puts("ichigo_add()");
    if (lua_gettop(L) < 2 || !lua_isuserdata(L,1))
        return luaL_error(L, "ichigo_add() error");
    v1 = (ichigo_data*)lua_touserdata(L,1);
    v2 = lua_tonumber(L,2);
    vret = ichigo_data_new(L);
    vret->timestamp = v1->timestamp + (time_t)v2;
    lua_remove(L,2);
    lua_remove(L,1);
    return 1;
}

int ichigo_tostring(lua_State* L)
{
    ichigo_data* v1;
    const char* p;

    puts("ichigo_tostring()");
    if (lua_gettop(L) < 1 || !lua_isuserdata(L,1))
        return luaL_error(L, "ichigo_tostring() error");
    v1 = (ichigo_data*)lua_touserdata(L,1);
    p = ctime(&v1->timestamp);
    if (p == NULL)
        return luaL_error(L, "ctime() error");
    lua_pop(L,1);
    lua_pushlstring(L, p, strlen(p)-1);
    return 1;
}

int ichigo_japanese_era(lua_State* L)
{
    ichigo_data* v1;
    struct tm* p;
    char buf[64];

    puts("ichigo_japanese_era()");
    if (lua_gettop(L) < 1 || !lua_isuserdata(L,1))
        return luaL_error(L, "ichigo_japanese_era() error");
    v1 = (ichigo_data*)lua_touserdata(L,1);
    p = localtime(&v1->timestamp);
    if (p == NULL)
        return luaL_error(L, "localtime() error");
    if (p->tm_year < 89) {
        sprintf(buf,"S.%02d/%02d/%02d %02d:%02d:%02d",
          p->tm_year-25,p->tm_mon+1,p->tm_mday,
          p->tm_hour,p->tm_min,p->tm_sec);
    } else {
        sprintf(buf,"H.%02d/%02d/%02d %02d:%02d:%02d",
          p->tm_year-88,p->tm_mon+1,p->tm_mday,
          p->tm_hour,p->tm_min,p->tm_sec);
    }
    lua_pop(L,1);
    lua_pushstring(L, buf);
    return 1;
}

/* metatableに登録する関数 */
const luaL_Reg key_values_meta[] =
{
    { "__add", ichigo_add },
    { "__tostring", ichigo_tostring },
    { NULL, NULL }
};

const luaL_Reg key_values_methods[] =
{
    { "japanese_era", ichigo_japanese_era },
    { NULL, NULL }
};

void init_metatable(lua_State* L)
{
    /* registryにmetatable用のグローバルtableを確保 */
    luaL_newmetatable(L, TABLE_NAME);
    printf("newmetatable: stack = %d\n",(int)lua_gettop(L));

    /* metatable用のグローバルtableに関数を設定 */
    luaL_setfuncs(L, key_values_meta, 0);
    printf("setfuncs: stack = %d\n",(int)lua_gettop(L));

    /* metatableの__indexに、keyとしてメソッド名を持つtableを設定 */
    lua_pushliteral(L, "__index"); /* key */
    luaL_newlib(L, key_values_methods); /* value */
    lua_settable(L, -3); /* -3: pushされたmetatable */
    printf("setfuncs: stack = %d\n",(int)lua_gettop(L));

    /* 不要になったmetatableをpop */
    lua_pop(L,1);
}

ichigo_data* ichigo_data_new(lua_State* L)
{
    ichigo_data* vret;

    /* userdata(full)を確保 */
    vret = (ichigo_data*)lua_newuserdata(L, sizeof(ichigo_data));
    /* vretのメンバを初期化 */
    vret->timestamp = 0;
    /* vretのmetatableとして、registryに登録されたmetatableを設定 */
    luaL_setmetatable(L, TABLE_NAME);
    return vret;
}

int ichigo_utc(lua_State* L)
{
    ichigo_data* vret;

    vret = ichigo_data_new(L);
    vret->timestamp = time(NULL);
    return 1;
}

void init_function(lua_State* L)
{
    /* C言語の関数をpush */
    lua_pushcfunction(L,ichigo_utc);
    /* Luaのglobal変数として保存し、pop */
    lua_setglobal(L,FUNCTION_NAME);
}

int main()
{
    lua_State* L;
    int result;

    L = luaL_newstate();
    if (L == NULL) {
        puts("luaL_newstate() error");
        return 1;
    }
    luaL_openlibs(L);

    init_metatable(L);
    init_function(L);

    result = luaL_loadfile(L, SCRIPT_NAME);
    if (result != LUA_OK) {
        puts("luaL_loadfile() error");
        return 1;
    }
    result = lua_pcall(L, 0, LUA_MULTRET, 0);
    if (result != LUA_OK) {
        puts("lua_pcall() error");
        return 1;
    }
    lua_close(L);

    return 0;
}
このコードから読み込むLuaスクリプトの1例を示します。 ファイル名はichigometa1.luaとします。
today = ichigoutc()
print(today)
tomorrow = today + 24*60*60
print(tomorrow)
jtoday = today:japanese_era()
print(jtoday)
実行結果は次のようになります。
Sun Nov 26 11:23:17 2017
Mon Nov 27 11:23:17 2017
H.29/11/26 11:23:17
newmetatable: stack = 1
setfuncs: stack = 1
setfuncs: stack = 1
ichigo_tostring()
ichigo_add()
ichigo_tostring()
ichigo_japanese_era()