把Lua函数传递到C/C++中实例
问题
在lua中,因为函数也是第一类值,所以会出现将函数作为另一个函数的参数,或者函数作 为函数的返回值。这种机制在很多地方都能代码更灵活更简洁,例如:
table.sort(table [,comp])
这里的comp就要求传入一个函数,我们在调用时,大概会有如下形式:
table.sort(t, comp) -- 直接写函数名
table.sort(t, local_comp) -- 某个局部函数
table.sort(t, function (a, b) xxx end ) -- 临时构造一个匿名函数
其中最后一种方式最为灵活,任意时候在需要的时候构造一个匿名函数。这种在lua自身的 环境中使用,自然没有问题。但是,当我们在c/c++中注册一些函数到lua环境中,而这些 函数也需要使用函数参数的时候,问题就出来了。
lua本身是不支持将lua函数作为函数参数传入c/c++的,不管这个想要传入的函数是全局的 、局部的、或者匿名的(匿名的本质上也算局部的)。一般情况下,我们唯一的交互方式, 不是传入一个函数,而是一个全局函数名。c/c++保存这个函数名,在需要回调lua的时候, 就在lua全局表中找到这个函数(根据函数名),然后再调用之。情况大致如下:
function lua_func () xxx end
cfunc(lua_func) -- wrong
cfunc("lua_func") -- right
我们这回的脚本模块,策划会大量使用需要回调函数的c/c++函数。显然,创建大量的全局 函数,先是从写代码的角度看,就是很伤神的。
解决
我们最终需要的方式,大概如下:
cfunc(lua_func) -- ok
cfunc(function () xxx end) -- ok
local xxx = function () xxx end
cfunc(xxx) -- ok
要解决这个问题,我的思路是直接在lua层做一些包装。因为c/c++那边仅支持传入一个全局 函数名(当然不一定得全局的,根据实际情况,可能在其他自己构造的表里也行),也就是 一个字符串,所以我的思路就是将lua函数和一个唯一的字符串做映射。
function wrap (fn)
local id = generate_id()
local fn_s = "__callback_fn"..id
_g[fn_s] = fn
return fn_s
end
这个wrap函数,就是将一个函数在全局表里映射到一个字符串上,那么在使用时:
cfunc(wrap(function () xxx end))
cfunc(const char *fn_name, xxx); -- cfunc的原型
cfunc是c/c++方注册进lua的函数,它的原型很中规中矩,即:只接收一个函数名,一个字 符串,如之前所说,c/c++要调用这个回调函数时,就根据这个字符串去查找对应的函数。 脚本方在调用时,如果想传入一个匿名函数了,就调用wrap函数包装一下即可。
一个改进
上面的方法有个很严重的问题,在多次调用wrap函数后,将导致全局表也随之膨胀。我们需 要想办法在c/c++完成回调后,来清除wrap建立的数据。这个工作当然可以放到c/c++来进行 ,例如每次发生回调后,就设置下全局表。但这明显是不对的,因为违背了接口的设计原则 ,这个额外的机制是在lua里添加的,那么责任也最好由lua来负。要解决这个问题,就可以 使用lua的metamethods机制。这个机制可以在lua内部发生特定事件时,让应用层得到通知。 这里,我们需要关注__call事件。
lua中只要有__call metamethod的值,均可被当作函数调用。例如:
ab(1, 2)
这里这个函数调用形式,lua就会去找ab是否有__call metamethod,如果有,则调用它。这 个事实暗示我们,一个table也可以被调用。一个改进的wrap函数如下:
local function create_callback_table (fn, name)
local t = {}
t.callback = fn
setmetatable (t, {__call = -- 关注__call
function (func, ...) -- 在t(xx)时,将调用到这个函数
func.callback (...) -- 真正的回调
del_callback (name) -- 回调完毕,清除wrap建立的数据
end })
return t
end
function wrap (fn)
local id = generate_func_id() -- 产生唯一的id
local fn_s = "_callback_fn"..id
_g[fn_s] = create_callback_table(fn, fn_s) -- _g[fn_s]对应的是一个表
return fn_s
end
在我们的c/c++程序中,依然如往常一样,先是从_g里取出函数名对应的对象。虽然这个对 象现在已经是一个table。然后lua_call。
上面的代码是否会在原有基础上增加不可接受的性能代价?虽然我没有做过实际测试,但是 从表明看来,排除meta table在lua里的代价,也就多了几次lua函数调用。
最后,感叹一下,lua里的table及metatable机制,实在非常强大。这种强大不是功能堆砌 出来的强大,而是简单东西组合出来的强大。其背后的设计思想,着实让人佩服。
4.26.2011 update
之前的文中说“lua本身是不支持将lua函数作为函数参数传入c/c++的“,这句话严格来说不 正确(由某网友评论)。假设函数cfun由c/c++注册,我们是可以编写如下代码的:
cfunc(print) -- 传入lua函数
但是问题在于,我们无法取出这个函数并保存在c/c++方。lua提供了一些接口用于取cfunc 的参数,例如lual_checknumber(封装lua_tonumber)。但没有类似lual_checkfunction的 接口。lua中的table有同样的问题。究其原因,主要是lua中的函数没有直接的c/c++数据结 构对应。