目录
一 基础知识准备
1) C++嵌入Lua脚本
二 具体实现
lua和c交互很直接,直接调用lua提供的c接口即可,和c++的类交互是需要利用lua本身的一些特性,主要利用lua table的__index,_newindex,_gc元方法。
__index
:
索引 table[key]获取key的对应的值
。 当 table
不是表或是表 table
中不存在 key
这个键时,这个事件被触发。 此时,会读出 table
相应的元方法。尽管名字取成这样, 这个事件的元方法其实可以是一个函数也可以是一张表。 如果它是一个函数,则以 table
和 key
作为参数调用它。 如果它是一张表,最终的结果就是以 key
取索引这张表的结果。 (这个索引过程是走常规的流程,而不是直接索引, 所以这次索引有可能引发另一次元方法。
local t = {
name = "hehe",
}
local mt = {
__index = function(table, key)
print("call __index :" .. key);
end
}
setmetatable(t,mt);
print(t.money);
---------------------------------------------------------------------------
-- 运行结果:
-- call __index :money
-- nil
----------------------------------------------------------------------------
__newindex
:
索引赋值 table[key] = value
。 和索引事件类似,它发生在 table
不是表或是表 table
中不存在 key
这个键的时候。 此时,会读出 table
相应的元方法。同索引过程那样, 这个事件的元方法即可以是函数,也可以是一张表。 如果是一个函数, 则以 table
、 key
、以及 value
为参数传入。 如果是一张表, Lua 对这张表做索引赋值操作。 (这个索引过程是走常规的流程,而不是直接索引赋值, 所以这次索引赋值有可能引发另一次元方法。)一旦有了 "newindex" 元方法, Lua 就不再做最初的赋值操作。
local smartMan = {
name = "none",
}
local other = {
name = "大家好,我是很无辜的table"
}
local t1 = {};
local mt = {
__index = smartMan,
__newindex = other
}
setmetatable(t1, mt);
print("other的名字,赋值前:" .. other.name);
t1.name = "小偷";
print("other的名字,赋值后:" .. other.name);
print("t1的名字:" .. t1.name);
-----------------------------------------------------------------------
-- 运行结果:
-- other的名字,赋值前:大家好,我是很无辜的table
-- other的名字,赋值后:小偷
-- t1的名字:none
------------------------------------------------------------------------
当给t1的name字段赋值后,other的name字段反而被赋值了,而t1的name字段仍然没有发生变化。(实际上t1的name字段还是不存在的,它只是通过__index找到了smartMan的name字段)于是,我们给t1的name赋值的时候,实际上是给other的name赋值了。
调用规则
1 如果__newindex是一个函数,则在给table不存在的字段赋值时,会调用这个函数。
2 如果__newindex是一个table,则在给table不存在的字段赋值时,会直接给__newindex的table赋值。
对于__index同样也是一样的可以是一个函数也可以是一个表,因此我们可以把c++中的一个类(在lua里其实就是一个表table),我们把c++的成员函数注册到该表的__index元表中,属性注册到__newindex原表中,当我们在lua中调用对应的函数或者属性时lua就会到对用的原表中找到对应的方法去调用。
__call:
函数调用操作 func(args)
。 当 Lua 尝试调用一个非函数的值的时候会触发这个函数(当table名字做为函数名字的形式被调用的时候)。 查找 func
的元方法, 如果找得到,就调用这个元方法, func
作为第一个参数传入,原来调用的参数(args
)后依次排在后面。
-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end
-- 定义元方法__call
mytable = setmetatable({10}, {
__call = function(mytable, newtable)
sum = 0
for i = 1, table_maxn(mytable) do
sum = sum + mytable[i]
end
for i = 1, table_maxn(newtable) do
sum = sum + newtable[i]
end
return sum
end
})
newtable = {10,20,30}
print("res = "..mytable(newtable))
------------------------------------------------------
res = 70
------------------------------------------------------
当table名字做为函数名字的形式被调用的时候,我们可以看到mytable(newtable)这种形式跟c++中的构造函数的调用形式是一样的,所以我们可以把对应类的构造函数注册到__call中就可以在lua中创建c++对象了。
__gc:
在对象被GC的时候,会先调用元表里面的“gc”域,相当于c++里的析构函数
因为我们注册在lua中的c++类对象内存是在c++中创建的,lua回收对象时不会自己释放这块内存,但是会调用对应的__gc元方法,所有我们需要把对象的析构函数或者对应的回收内存的逻辑注册到这里,这样当lua gc回收该对象时就可以把对应的内存回收。
更详细具体的实现方案: https://github.com/DGuco/luabridge
三 注意事项
关于lua的异常
Lua 在内部发生异常时,VM 会在 C 的 stack frame 上直接跳至之前设置的恢复点,然后 unwind lua vm 层次上的 lua stack 。lua stack (CallInfo 结构)在捕获异常后是正确的,但 C 的 stack frame 的处理未必如你的宿主程序所愿。也就是 RAII 机制很可能没有被触发。也就是说lua里面使用的异常并不是c++的异常,只是使用了c的setjump和longjump来实现到恢复点的跳转,所以并不会有C++所期望的栈的展开操作,所以在C++里面看来是异常安全的代码,此时也是“不安全”的,也不能保证异常安全。例如
void Function1(lua_state state)
{
TestClass tmp();
luaL_checkstring(state,1);
}
当上面的luaL_checkstring出现异常时候,TestClass的析构函数并不会被调用,假如你需要在析构函数里面释放一些资源,可能会导致资源泄露、锁忘记释放等问题。所以在使luaL_checkxxx时候,需要很小心,在luaL_checkxxx之前尽量不要申请一些需要之后释放的资源,尤其是加锁函数 (通常我们会把锁的释放封装到一个类的析构函数中,来保证出现异常或者提前return的时候自动调用析构函数,从而确保锁的释放),如果遇到上面的情况就不能保证它正常工作了,咋lua的源码ldo.c中可以得到验证,gcc和g++编译lua时lua确实给出了不同的实现:
#if !defined(LUAI_THROW) /* { */
#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP) /* { */
/* C++ exceptions */
#define LUAI_THROW(L,c) throw(c)
#define LUAI_TRY(L,c,a) \
try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; }
#define luai_jmpbuf int /* dummy variable */
#elif defined(LUA_USE_POSIX) /* }{ */
/* in POSIX, try _longjmp/_setjmp (more efficient) */
#define LUAI_THROW(L,c) _longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a) if (_setjmp((c)->b) == 0) { a }
#define luai_jmpbuf jmp_buf
#else /* }{ */
/* ISO C handling with long jumps */
#define LUAI_THROW(L,c) longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a }
#define luai_jmpbuf jmp_buf
#endif /* } */
#endif /* } */
因此对于这种问题,我们可以通过用c++重新构建lua虚拟机,而不是用gcc编译的c库。
//lua lib源码库是否用g++编译
#ifdef COMPILE_LUA_WITH_CXX
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
#else
//lua lib源码库用gcc编译
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#endif
如果你用 extern "C"{}这种方式引用lua库则说明你调用的是用gcc编译出来的lua库,当我们用c++编译lua库时直接include即可(即上面展示代码中的前4行),比如我用cmake重新构建c++版本lua库。
cmake_minimum_required(VERSION 3.6)
project(lua)
set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
add_compile_options(-O2 -Wall -g -pipe -Wextra -std=c++11)
set(LUA_HEADER_FILES
lapi.h
.
.
.
lvm.h
lzio.h)
set(LUA_SOURCE_FILES
lapi.c
.
.
.
lzio.c
)
set_source_files_properties(${LUA_HEADER_FILES} PROPERTIES LANGUAGE CXX)
set_source_files_properties(${LUA_SOURCE_FILES} PROPERTIES LANGUAGE CXX)
include_directories(
/usr/include/
/usr/local/include/
)
link_directories(
/usr/lib/
/usr/local/lib/
)
add_library(lua
${LUA_HEADER_FILES}
${LUA_SOURCE_FILES})
target_link_libraries(lua dl)
set(LIBRARY_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/lib)
在CMakeList.txt中添加下面两行
set_source_files_properties(${LUA_HEADER_FILES} PROPERTIES LANGUAGE CXX)
set_source_files_properties(${LUA_SOURCE_FILES} PROPERTIES LANGUAGE CXX)
设置lua的源码类型从而使用g++重新编译lua源码,从而达到在c++中正确使用lua的目的。
相关源代码: https://github.com/DGuco/luabridge
参考文章