基于method实现lua访问C++对象成员
一、问题
对于一个在C++创建的类对象,lua中如何调用这个对象的C++接口?进一步,如果我们想在lua中实现对这个C++类的接口扩展,该如何实现?
二、lua对于类似于C++中meta类型的支持
在lua中,为了模拟对于C++中面向对象中一个类接口的支持,提供一个专门的"NameSpace:function"类型的语法结构。
1、如何声明
从解析的地方看,这里是自动在函数的参数列表的最开始位置添加了一个self变量。
lua-5.3.4\src\lparser.c
static void body (LexState *ls, expdesc *e, int ismethod, int line) {
/* body -> '(' parlist ')' block END */
FuncState new_fs;
BlockCnt bl;
new_fs.f = addprototype(ls);
new_fs.f->linedefined = line;
open_func(ls, &new_fs, &bl);
checknext(ls, '(');
if (ismethod) {
new_localvarliteral(ls, "self"); /* create 'self' parameter */
adjustlocalvars(ls, 1);
}
……
}
需要注意的是,这个地方是将":"作为函数名的一部分处理的。从这个解析可以看到,函数名的命名规则决定了它是否是一个“method”,但同时这个变量名也会被拆分,同样作为一个"fieldsel"语法,这意味着和table的field一样,这个地方会在table中添加一个名字为":"后面字符串的field,并且它指向的是这个函数。
static int funcname (LexState *ls, expdesc *v) {
/* funcname -> NAME {fieldsel} [':' NAME] */
int ismethod = 0;
singlevar(ls, v);
while (ls->t.token == '.')
fieldsel(ls, v);
if (ls->t.token == ':') {
ismethod = 1;
fieldsel(ls, v);
}
return ismethod;
}
2、如何调用
这里可以看到,在suffixedexp函数中传入的变量v表示的是之前解析出来的数值(例如some:little这种语法中,这个v表示的就是some这个表达式)
static void suffixedexp (LexState *ls, expdesc *v) {
/* suffixedexp ->
primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */
FuncState *fs = ls->fs;
int line = ls->linenumber;
primaryexp(ls, v);
for (;;) {
……
case ':': { /* ':' NAME funcargs */
expdesc key;
luaX_next(ls);
checkname(ls, &key);
luaK_self(fs, v, &key);
funcargs(ls, v, line);
break;
……
}
在这个地方生成了一个OP_SELF指令,并且预分配了两个寄存器,分别用来存储self和查询获得的function地址。
lua-5.3.4\src\lcode.c
/*
** Emit SELF instruction (convert expression 'e' into 'e:key(e,').
*/
void luaK_self (FuncState *fs, expdesc *e, expdesc *key) {
int ereg;
luaK_exp2anyreg(fs, e);
ereg = e->u.info; /* register where 'e' was placed */
freeexp(fs, e);
e->u.info = fs->freereg; /* base register for op_self */
e->k = VNONRELOC; /* self expression has a fixed register */
luaK_reserveregs(fs, 2); /* function and 'self' produced by op_self */
luaK_codeABC(fs, OP_SELF, e->u.info, ereg, luaK_exp2RK(fs, key));
freeexp(fs, key);
}
3、如何执行
这里看到一个特殊的处理:除了将self(也就是前面suffixedexp压入的v值)通过setobjs2s(L, ra + 1, rb);原封不动的压入堆栈之外,还进行了一个额外的查找动作,就是从v中查找函数的内容,这个地方是一个“普通”的table中查找指定成员的操作,所以这个查找也可以通过它的__index成员完成。
void luaV_execute (lua_State *L) {
……
vmcase(OP_SELF) {
const TValue *aux;
StkId rb = RB(i);
TValue *rc = RKC(i);
TString *key = tsvalue(rc); /* key must be a string */
setobjs2s(L, ra + 1, rb);
if (luaV_fastget(L, rb, key, aux, luaH_getstr)) {
setobj2s(L, ra, aux);
}
else Protect(luaV_finishget(L, rb, rc, ra, aux));
vmbreak;
}
……
}
4、当直接使用类名调用函数时
这种情况下,self指向的就是metatable本身,大致略等于C++中调用静态成员函数(self指向metatable本身)
tsecer@harry: cat lua.static.call.method.lua
meta = {
x = 1111,
}
function meta:func1()
print("xxxx")
print(self)
for k,v in pairs(self) do
print(k, v)
end
end
meta:func1()
tsecer@harry: /home/tsecer/study/lua-5.3.4/src/lua lua.static.call.method.lua
xxxx
table: 0x64b4b0
x 1111
func1 function: 0x64b0e0
tsecer@harry:
5、总结
总起来说,定义的时候是metatable:funcname,调用的时候是obj:funcname。这一点的确是和C++的语法是神似的。
三、如何使用
0、限制
这里的限制依然在于lua中只有table和userdata支持(lightuserdata也不支持)metatable,所以想使用lua的metatable方法一定需要使用userdata结构。
1、在lua中调用C++中对象方法
tsecer@harry: cat lua.oo.cpp
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
struct AA
{
AA(int x):
m_x(x)
{}
int func1(int x)
{
printf("in %s x %d m_x %d\n", __func__, x, m_x);
return 0;
}
int func2(int x, int y)
{
printf("in %s x %d y %d m_x %d\n", __func__, x, m_x);
return 0;
}
int m_x;
};
int CallAAFunc1(lua_State *L)
{
//第一个参数保存self指针
AA *pobj = *(AA**)lua_touserdata(L, 1);
//接下来保存参数列表
int parm1 = lua_tonumber(L, 2);
//调用函数
pobj->func1(parm1);
return 0;
}
int CallAAFunc2(lua_State *L)
{
//由于是通过 AAObj::Func2调用,所以状态机中会有三个参数
//从1到3一次为:self也就是AAObj对象,调用时的两个参数
AA *pobj = *(AA**)lua_touserdata(L, 1);
int parm1 = lua_tonumber(L, 2), parm2 = lua_tonumber(L, 3);
pobj->func2(parm1, parm2);
return 0;
}
//注册个lua的__index方法,当lua需要查找一个结构当前不存在的filed时调用
int Index(lua_State *L)
{
//第一个参数为变量本身,它的metatable中存储有函数到Cfunction的映射
int imetatable = lua_getmetatable(L, 1);
if (imetatable != 1)
{
printf("get metatable failed \n");
return -1;
}
//第二个参数为函数名
const char *funcname = lua_tostring(L, 2);
if (funcname == nullptr)
{
printf("get funcname failed \n");
return -1;
}
//从metatable中以函数名为键值查找cfunction
lua_getfield(L, -1, funcname);
return 1;
}
int CreateMetaTable(lua_State *L)
{
//由于lua中的metatable也是一个简单的table,所以这个地方首先直接创建一个普通的table
lua_createtable(L, 0, 0);
//创建metatable中的__index方法
lua_pushstring(L, "__index");
//创建该方法对应的C函数
lua_pushcfunction(L, Index);
//将新生成的metatable的__index设置为新创建的cfunction
lua_rawset(L, -3);
//在metatable中添加转发函数
//函数名
lua_pushstring(L, "Func1");
//C函数地址
lua_pushcfunction(L, CallAAFunc1);
//设置hashtable
lua_rawset(L, -3);
//在metatable中设置Func2为自定义的转发函数
lua_pushstring(L, "Func2");
lua_pushcfunction(L, CallAAFunc2);
lua_rawset(L, -3);
//将AA作为全局变量添加到lua虚拟机中
lua_setglobal(L, "AA");
return 1;
}
int BindObj(lua_State *L, AA *Obj)
{
//创建一个只包含一个指针类型的userdata,其中保存C++中创建对象的地址
*(AA**)lua_newuserdata(L, sizeof(Obj)) = Obj;
//将新创建变量赋值给lua中全局变量AAObj
lua_setglobal(L, "AAObj");
//从lua中查找到AAObj变量(结果在栈顶)
lua_getglobal(L, "AAObj");
//查找到之前创建的metatable:AA变量
lua_getglobal(L, "AA");
//将堆栈中-2位置变量(AAObj)的metatable设置为栈顶变量(AA变量)
lua_setmetatable(L, -2);
//在lua虚拟机中执行AAObj(userdata存储了对象指针的lua变量)
//由于AAObj是一个userdata,所以会调用它metatable中的__index方法,也就是前面设置的Index函数
//传入的参数是AAObj和需要查找的成员Func1/Func2
const char *luascript = "AAObj:Func1(1)"
"AAObj:Func2(2, 3)";
if (luaL_loadstring(L, luascript) == LUA_OK) {
if (lua_pcall(L, 0, 0, 0) == LUA_OK) {
lua_pop(L, lua_gettop(L));
}
}
else
{
printf("load failed\n");
return -1;
}
}
int main(int argc, char ** argv) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
//lua_register(L, "CreateObject", CreateObject);
CreateMetaTable(L);
AA a1(1111), a2(2222);
BindObj(L, &a1);
BindObj(L, &a2);
lua_close(L);
return 0;
}
tsecer@harry: ./a.out
in func1 x 1 m_x 1111
in func2 x 2 y 1111 m_x 1
in func1 x 1 m_x 2222
2、在C++中调用lua method
下面的例子比较简单,但是里面展示通过userdata的自定义结构配合metatable完成对C++不同类型对象的访问
tsecer@harry: cat lua.oo.cpp
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#include "string.h" //strcmp
struct AA
{
AA(int x):
m_x(x)
{}
int func1(int x)
{
printf("in %s x %d m_x %d\n", __func__, x, m_x);
return 0;
}
int func2(int x, int y)
{
printf("in %s x %d y %d m_x %d\n", __func__, x, m_x);
return 0;
}
int m_x;
};
int CallAAFunc1(lua_State *L)
{
//第一个参数保存self指针
AA *pobj = *(AA**)lua_touserdata(L, 1);
//接下来保存参数列表
int parm1 = lua_tonumber(L, 2);
//调用函数
pobj->func1(parm1);
return 0;
}
int CallAAFunc2(lua_State *L)
{
//由于是通过 AAObj::Func2调用,所以状态机中会有三个参数
//从1到3一次为:self也就是AAObj对象,调用时的两个参数
AA *pobj = *(AA**)lua_touserdata(L, 1);
int parm1 = lua_tonumber(L, 2), parm2 = lua_tonumber(L, 3);
pobj->func2(parm1, parm2);
return 0;
}
//注册个lua的__index方法,当lua需要查找一个结构当前不存在的filed时调用
int Index(lua_State *L)
{
//第一个参数为变量本身,它的metatable中存储有函数到Cfunction的映射
int imetatable = lua_getmetatable(L, 1);
if (imetatable != 1)
{
printf("get metatable failed \n");
return -1;
}
//第二个参数为函数名
const char *funcname = lua_tostring(L, 2);
if (funcname == nullptr)
{
printf("get funcname failed \n");
return -1;
}
AA *pobj = *(AA**)lua_touserdata(L, 1);
if (strcmp(funcname, "m_x") == 0)
{//对于成员做一个特殊处理
lua_pushinteger(L, pobj->m_x);
}
else
{
//从metatable中以函数名为键值查找cfunction
lua_getfield(L, -1, funcname);
}
return 1;
}
int CreateMetaTable(lua_State *L)
{
//由于lua中的metatable也是一个简单的table,所以这个地方首先直接创建一个普通的table
lua_createtable(L, 0, 0);
//创建metatable中的__index方法
lua_pushstring(L, "__index");
//创建该方法对应的C函数
lua_pushcfunction(L, Index);
//将新生成的metatable的__index设置为新创建的cfunction
lua_rawset(L, -3);
//在metatable中添加转发函数
//函数名
lua_pushstring(L, "Func1");
//C函数地址
lua_pushcfunction(L, CallAAFunc1);
//设置hashtable
lua_rawset(L, -3);
//在metatable中设置Func2为自定义的转发函数
lua_pushstring(L, "Func2");
lua_pushcfunction(L, CallAAFunc2);
lua_rawset(L, -3);
//将AA作为全局变量添加到lua虚拟机中
lua_setglobal(L, "AA");
return 1;
}
int BindObj(lua_State *L, AA *Obj)
{
//现在lua中定义一个方法(method),这样在后面才能通过函数名(Func3)调用
const char *luascript = "function AA:Func3(x) print(self.m_x * 100000 + x); end";
if (luaL_loadstring(L, luascript) == LUA_OK) {
if (lua_pcall(L, 0, 0, 0) == LUA_OK) {
lua_pop(L, lua_gettop(L));
}
}
else
{
printf("load failed\n");
return -1;
}
//从AA表中找到Func3地址
lua_getglobal(L, "AA");
//lua_pushvalue(L, -1);
//查找其中的Func3方法
lua_getfield(L, -1, "Func3");
//创建一个userdata并把它保留在栈中,作为method调用的self参数
//创建一个只包含一个指针类型的userdata,其中保存C++中创建对象的地址
*(AA**)lua_newuserdata(L, sizeof(Obj)) = Obj;
//从lua中读取AA table,该值作为新创建对象的metatable
lua_getglobal(L, "AA");
//将新创建对象的metatable(-2中保存的下标)设置为AA(table,栈中下标为-1)
//该操作执行之后堆栈中的AA table会被自动从堆栈中弹出
lua_setmetatable(L, -2);
//函数调用参数入栈
lua_pushinteger(L, 1234);
//执行函数调用
lua_call(L, 2, 1);
return 0;
}
int main(int argc, char ** argv) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
//lua_register(L, "CreateObject", CreateObject);
CreateMetaTable(L);
AA a1(1111);
BindObj(L, &a1);
lua_close(L);
return 0;
}
tsecer@harry:
四、unlua如何实现
可以看到,UnLua也是使用userdata结构,并在其中存储C++对象地址,并为对象设置专有的metatable实现。
UnLua-master\Plugins\UnLua\Source\UnLua\Private\LuaCore.cpp
/**
* Push a UObject to Lua stack
*/
void PushObjectCore(lua_State *L, UObjectBaseUtility *Object)
{
if (!Object)
{
lua_pushnil(L);
return;
}
void **Dest = (void**)lua_newuserdata(L, sizeof(void*)); // create a userdata
*Dest = Object; // store the UObject address
……
// the UObject is object instance
TStringConversion<TStringConvert<TCHAR, ANSICHAR>> ClassName(*FString::Printf(TEXT("%s%s"), Class->GetPrefixCPP(), *Class->GetName()));
bSuccess = TryToSetMetatable(L, ClassName.Get());
……
}