欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

把Lua函数传递到C/C++中实例

程序员文章站 2022-07-05 08:50:57
问题 在lua中,因为函数也是第一类值,所以会出现将函数作为另一个函数的参数,或者函数作 为函数的返回值。这种机制在很多地方都能代码更灵活更简洁,例如: 复制代码 代码...

问题

在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++数据结 构对应。