- 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でオブジェクト指向風にする場合は、 this
や self
に相当するオブジェクトを 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 をうまく受け渡すことができる。
賢い。
動くサンプルも発見。
改造してみた
- 任意の
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")