luaのメソッドで ":" を避ける(lua scriptとc++両方)


  • 20190815 メモ書きを読めるように書き直し

: 避けたい

以下のような table がある。

local t = {
    name = 'Mr-table'
}

t.hello = function(self, target)
    print(self.name, 'say hello to', target)
end

t.hello(t, 'You') -- Mr-table   say hello to    You

lua ではシンタックスシュガーのコロンを使って以下のように書ける。

t:hello('You') -- Mr-table  say hello to    You

-- t.hello(t, 'You') と同じ

2回出てきた t が1回になって便利。

なのだけど、ちょっとリスクがある。

-- うっかりコロンにするのを忘れた・・・
t.hello('You') -- nil   say hello to    nil

エラーにならないのだけど、意図した挙動とは違う(エラーになるのは運が良いのだ)。
luaは、引数の型のみならず数もチェックしないので間違いが分かりにくくなる可能性があります。

解決策は在って、self を引数以外の手段で参照する。

local self = t
t.hello = function(target)
    -- クロージャーで外の変数を参照
    print(self.name, 'say hello to', target)
end
t.hello('You') -- Mr-table  say hello to    You

metatable を使う場合

-- class風味なmetatable
local HelloMetaTable = {
}
-- method的な関数
HelloMetaTable.hello = function(self, target)
    print(self.name, 'say hello to', target)
end
HelloMetaTable.__index = HelloMetaTable

-- インスタンスを作る
function createTableObject(name)
    local t = {
        name = name
    }
    setmetatable(t, HelloMetaTable)
    return t
end
local t = createTableObject('Mr-table')
t:hello('You') -- Mr-table  say hello to    You

metatable 経由のメソッド呼び出しに self を渡すには、
__index にテーブルではなく関数をセットする手法があります。

-- metatableの __index には 関数を返す関数をセットすることができる
HelloMetaTable.__index = function(self, key)
    -- __index に関数をセットすると、実行時に
    -- 第1引数には '.' の左側, 第2引数には '.' の右側が来る
    return function(target)
        -- 関数の外の self と key を抱え込む
        return HelloMetaTable[key](self, target)
    end
end

t.hello('You') -- Mr-table  say hello to    You

-- __index 関数が実行されて
local hello = t.hello
-- 中の関数を実行する
hello('You')

C-API

ToDo: 簡単な動くサンプル
ToDo: upvalueの説明

Cバインディングでも同じ概念を表現できる。

// metatable.__index にこれがセットしてあるとする
// 実行時には、
// stack#1 には self,
// stack#2 には key
// が入っていて、key に応じた function を返すことが期待されている
int IndexDispatch(lua_State* L)
{
    // stack1, 2 を upvalue にしてExecuteを返す
    lua_pushcclosure(L, &Execute, 2);
    return 1;
}

// 実行時には、
// upvalue#1 に self,
// upvalue#2 に key
// statck#1... に関数の引数が入っている
int Execute(lua_State* L)
{
  auto obj = ToUserData(L, lua_upvalueindex(1));
  auto key = lua_tostring(L, lua_upvalueindex(2));
  // keyに応じた処理をどこかから取り出して
  // objとstackの引数を使って実行する・・・
}

==================================

C++ でテクニカルなことをするのと lua で self を埋め込むのが混ざってなんだかよくわからない記事になったので、
書き直し。主題は、 lua のクロージャーで self を受け渡す方。

書き直す前の残骸

久しぶりに lua を c++ に組み込むべく試行錯誤している。

を見つけて、よいではないかと使っていたのだけど、
ひとつ困ったことがあった。

バインドしたクラスメソッドの呼び出しがコロンなのである。

-- 例
p1:shoot()

組み込んで使ってみたところ、しょっちゅう : にするのを忘れてつらい(SyntaxErrorにできればよいのだが・・・)。
luaでオブジェクト指向風にする場合は、 thisself に相当するオブジェクトを closure で関数に埋め込むことで : を回避できるので sol2 でやる方法を探してみたが見つからなかった。

C API でやる方法を探索

発見した。

local var = object()
local varFunc = var.getColor;

varFunc();     -- Calls var.getColor()

varFunc( 35 ); -- Calls var.getColor( 35 )

まさにこれ。

手法

userdata に __index をセットしておく。

lua_pushstring(L, "__index");
lua_pushcfunction(L, &Luna < T >::property_getter);
lua_settable(L, metatable);

__index が呼び出されたときに
stack は

  • 1:table(userdata)
  • 2:key

となっている。
lua_pushlightuserdataで upvalue に this に相当する object を記録しておく。
c closure を lua に返す。

lua_pushnumber(L, _index ^ ( 1 << 8 ) ); // Push the right func index
lua_pushlightuserdata(L, obj); // <- これが this への pointer
lua_pushcclosure(L, &Luna < T >::function_dispatch, 2);

なるほどー。
この方法なら、同じ metatable を共有しつつ this をうまく受け渡すことができる。
賢い。

動くサンプルも発見。

Lua 5.2 and Luna Five Example

改造してみた

  • 任意の member関数ポインタ(R (T::*f)(AS...)) を登録できる(まだ、0引数と1引数(int) しか実装していないが簡単に増やせる)
  • lua の stack から std::tuple を作って、 this と member関数ポインタと tuple からメソッドをコールする
  • headeronly にできるように static じゃない実装にした

userdata, metatable, lightuserdata, upvalue, closure を使う。

  • ToDo: constructor に引数を渡す実装が無い

実装

後でコード整理する。
とりあえずメモ

#pragma once
#include "lua.hpp" //Lua for C++ Header
#include <typeinfo>
#include <vector>
#include <string>
#include <unordered_map>
#include <functional>
#include <iostream>
#include <tuple>
#include <assert.h>

void luna_setvalue(lua_State *L, int n)
{
    lua_pushinteger(L, n);
}

int luna_getvalue(lua_State *L, int index)
{
    return (int)luaL_checkinteger(L, index);
}

std::tuple<> luna_totuple(lua_State *L, int index, std::tuple<> *)
{
    return std::tuple<>();
}

template <typename A, typename... AS>
std::tuple<A, AS...> luna_totuple(lua_State *L, int index, std::tuple<A, AS...> *)
{
    std::tuple<A> t = std::make_tuple(luna_getvalue(L, index));
    return std::tuple_cat(std::move(t),
                          luna_totuple<AS...>(L, index + 1));
}

template <typename... AS>
std::tuple<AS...> luna_totuple(lua_State *L, int index)
{
    std::tuple<AS...> *p = nullptr;
    return luna_totuple(L, index, p);
}

/*
Modified Luna Five
This version has been slightly modified
Taken from: http://lua-users.org/wiki/LunaFive
*/
template <class T>
class Luna
{
    typedef std::function<int(lua_State *, T *)> LuaCall;
    struct PtrWithFT
    {
        T *Self;
        Luna *Luna;
    };

public:
    ~Luna()
    {
        std::cout << "~Luna" << std::endl;
    }

    std::unordered_map<std::string, LuaCall> m_map;

    int Call(lua_State *L, T *self, const char *key)
    {
        auto found = m_map.find(key);
        if (found == m_map.end())
        {
            lua_pushfstring(L, "no %s method", key);
            lua_error(L);
            return 0;
        }

        return found->second(L, self);
    }

    template <typename R, typename... AS, std::size_t... IS>
    void _AddMethod(const char *name,
                    R (T::*f)(AS...),
                    std::index_sequence<IS...>)
    {
        LuaCall call = [f](lua_State *L, T *self) -> int {
            // args
            auto args = luna_totuple<AS...>(L, 1);

            // apply
            R r = (self->*f)(std::get<IS>(args)...);

            // return
            luna_setvalue(L, r);

            return 1;
        };
        m_map.insert(std::make_pair(name, call));
    }

    template <typename... AS, std::size_t... IS>
    void _AddMethod(const char *name,
                    void (T::*f)(AS...),
                    std::index_sequence<IS...>)
    {
        LuaCall call = [f](lua_State *L, T *self) -> int {
            // args
            auto args = luna_totuple<AS...>(L, 1);

            // apply
            (self->*f)(std::get<IS>(args)...);

            return 0;
        };
        m_map.insert(std::make_pair(name, call));
    }

    template <typename R, typename... AS>
    Luna &AddMethod(const char *name,
                    R (T::*f)(AS...))
    {
        _AddMethod(name, f, std::index_sequence_for<AS...>{});
        return *this;
    }


    void Push(lua_State *L)
    {
        auto name = typeid(T).name();
        assert(luaL_newmetatable(L, name) == 1);
        int metatable = lua_gettop(L);

        lua_pushstring(L, "__gc");
        lua_pushcfunction(L, &Luna<T>::gc_obj);
        lua_settable(L, metatable);

        lua_pushstring(L, "__tostring");
        lua_pushcfunction(L, &Luna<T>::to_string);
        lua_settable(L, metatable);

        lua_pushstring(L, "__index");
        lua_pushcfunction(L, &Luna<T>::property_getter);
        lua_settable(L, metatable);

        lua_pushlightuserdata(L, this);
        lua_pushcclosure(L, &Luna::constructor, 1);
    }

    static PtrWithFT *ToUserData(lua_State *L, int index)
    {
        auto obj = static_cast<PtrWithFT **>(lua_touserdata(L, index));
        if (!obj)
        {
            return nullptr;
        }
        return *obj;
    }

    static int constructor(lua_State *L)
    {
        auto luna = (Luna *)lua_topointer(L, lua_upvalueindex(1));

        // new instance
        auto p = static_cast<PtrWithFT **>(lua_newuserdata(L, sizeof(PtrWithFT *))); // Push value = userdata
        *p = new PtrWithFT{
            .Self = new T(),
            .Luna = luna,
        };

        luaL_getmetatable(L, typeid(T).name()); // Fetch global metatable T::classname
        lua_setmetatable(L, -2);
        return 1;
    }

    // stack 1:table(userdata), 2:key
    static int property_getter(lua_State *L)
    {
        lua_pushcclosure(L, &Luna<T>::function_dispatch, 2);
        return 1;
    }

    // upvalue 1:table(userdata), 2:key
    static int function_dispatch(lua_State *L)
    {
        auto obj = ToUserData(L, lua_upvalueindex(1));
        auto key = lua_tostring(L, lua_upvalueindex(2));

        return obj->Luna->Call(L, obj->Self, key);
    }

    static int gc_obj(lua_State *L)
    {
        auto obj = ToUserData(L, -1);
        delete obj->Self;
        return 0;
    }

    static int to_string(lua_State *L)
    {
        auto obj = ToUserData(L, -1);
        if (obj)
            lua_pushfstring(L, "%s (%p)", typeid(T).name(), obj);
        else
            lua_pushstring(L, "Empty object");
        return 1;
    }
};
#include <iostream>

/*
Account to class to be made available in Lua using the Luna Five C++ Header.
*/
class Account
{
    int m_balance = 0;

public:
    Account()
    {
    }

    ~Account()
    {
        std::cout << "C++: Deleted Account " << this << std::endl;
    }

    void Deposit(int n)
    {
        m_balance += n;
    }

    void Withdraw(int n)
    {
        m_balance -= n;
    }

    int Balance()
    {
        return m_balance;
    }
};

//////////////////////////////////////////////////////////////////////////////
#include "luna.h"

int main(int argc, char *argv[])
{
    //Check if a Lua Script was specified
    if (argc != 2)
    {
        printf("Error! No Lua script or too many scripts were specified.\n");
        printf("Usage: %s <Script>.lua\n", argv[0]);
        return -1;
    }

    //Create a new Lua state
    lua_State *L = luaL_newstate();

    //Load Lua base library
    luaopen_base(L);

    //Register "Account" Class with Lua
    Luna<Account> account;
    account
        .AddMethod("Deposit", &Account::Deposit)
        .AddMethod("Withdraw", &Account::Withdraw)
        .AddMethod("Balance", &Account::Balance)
        .Push(L);
    lua_setglobal(L, "Account");

    //Execute the Lua script
    printf("C++: Executing Lua Script: %s\n", argv[1]);
    if (luaL_dofile(L, argv[1]) != 0)
    {
        printf("Lua Error: %s\n", lua_tostring(L, -1));
    }

    //Close Lua state
    lua_close(L);
    return 0;
}

参考サイトと同じスクリプト。

--
-- Lua 5.2 & Luna Five: Account Example
-- Date: 16 May 2013
-- Website: http://blog.p86.dk
--

-- Override Lua Print()
local print = function (x)
  print("Lua: "..x)
end

local a1 = Account(100)
print("Created Account 1 with $"..a1:Balance().." balance")

cash = 50
a1.Deposit(cash)
print("Account 1: Depositing $"..cash)

print("Account 1: New Balance is $"..a1:Balance())

cash = 25
a1.Withdraw(cash)
print("Account 1: Withdrawing $"..cash)

print("Account 1: New Balance is $"..a1:Balance())


-- Open a 2nd Account
local a2 = Account(1000)
print("Created Account 2 with $"..a2:Balance().." balance")

cash = 500
a2.Deposit(cash)
print("Account 2: Depositing $"..cash)

print("Account 2: New Balance is $"..a2:Balance())

cash = 250
a2.Withdraw(cash)
print("Account 2: Withdrawing $"..cash)

print("Account 2: New Balance is $"..a2:Balance())


-- Check the 1st Account Again
print("Account 1: New Balance is $"..a1:Balance())
cash = 25
a1.Withdraw(cash)
print("Account 1: Withdrawing $"..cash)

print("Account 1: New Balance is $"..a1:Balance())

print("End of Script")