当前位置: 首页 > news >正文

手动实现一个C++绑定Lua脚本的库

把C++绑定到Lua脚本的方法很多。但是在C++11之前,都没有太好的办法。比如tolua++,这个工具需要手动编写额外的pkg文件,每次需要手动执行命令生成额外的C++文件,使用比较繁琐,所以逐渐没落了。

而我自己用的是一个自己实现的绑定库,只是这个绑定库比较简单,只能绑定int (*lua_CFunction) (lua_State *L)这种特定格式的函数,和lua的pushcfunction比,也就是多了能绑定类成员函数的功能。所以复杂一点的函数,都需要写一个专门的函数包装一下,太麻烦。

虽然我经常在以C++为底层,Lua为脚本的框架上写代码,但其实很少需要把C++接口绑定到Lua。原因是框架已经很成熟,逻辑大部分都在Lua实现,该有的接口都有了,需要绑定新接口的情况少之又少,所以也就一直这么将就用着。自从前几年项目把C++的标准提高到C++11以后,可以支持parameter pack了,对一些利用parameter pack来实现的Lua绑定库实属眼馋,终于决定要更换自己的绑定库了。

开源的绑定库是很多的,但看了一圈后,发现这些库的实现太复杂。比如sol2,实在是太过复杂了一点,出现问题自己很难调试,里面的很多功能自己也用不到。看了下他们的实现,基本都是利用parameter pack把参数展开,感觉并不复杂,于是动了自己实现一个库的念头。

实现这个库的初始版本并不困难,但修修补补持续了好长时间,现在有时间就整理一下遇到的问题。

C++与Lua的基础交互机制

Lua本身就提供一套完善的C与Lua交互机制,任何一个格式为int (*lua_CFunction) (lua_State *L)的函数都可以利用lua_pushcfunction注册到Lua脚本,从而实现在Lua调用。C++与Lua的交互也是基于这个机制,但它首先要两个问题,一个是C++是有对象的,所以需要能把成员函数函数注册到Lua。比如

class Test
{
public:int test(int a, double b, const char *c);
};

这显然是一个成员函数,调用方式为this call,如何把它转换为C方式的int (*lua_CFunction) (lua_State *L)函数呢?众所周知,在Lua中是可以obj:func(a,b,c)这样写的,表示调用obj的func函数,它其实是一个语法糖,等同于obj.func(obj,a,b,c),但是恰好这个机制与C++中的thiscall是非常类似的,obj相当于this指针,它在Lua栈的第一个位置,其他是参数即可。这样问题就解决了。

另一个问题是我们希望注册到Lua的函数并不都是这个格式的,比如没有返回值 ,参数并不是lua_State*等等。比如

int test(int a, double b, const char *c);

怎么自动把参数、返回值都不匹配的函数转换为一个特定格式为int (*lua_CFunction) (lua_State *L)的函数

对于问题1,在C++11之前通常只能手写绑定函数或者用工具自动生成,顶多也只是用一些宏来辅助一下,没法做到参数的自动推导,非常繁琐。而parameter pack允许可变参数作为模板参数,奠定了“自动推导”这个基础。以上面的test函数为例,手写时绑定函数时,是这样的:

int test_binding(lua_State *L)
{// 取参数int a = lua_tonumber(L, 1);// ... 其他参数// 返回参数lua_pushnumber(L, v);return 1;
}

可见,没法自动推导的难点在于函数的参数数量、类型,还有返回值的类型都是不一样的,才没法做到自动。而现在利用parameter pack,可以把参数和返回值取出来

template <typename C, typename Ret, typename... Args> // 返回值、参数都在这里了
class ClassRegister<Ret (C::*)(Args...)>
{
};ClassRegister<test> cr; // 模板参数传入test函数,将会自动推导出返回值Ret和各个参数的类型、数量。

既然能把参数和返回值取出来,那么意味着整个过程就可以做到自动了,那具体是怎么做到的呢?

首先是返回值Ret,这个比较容易理解。在C++中,返回值只有void和其他类型这两种,所以只需要区分void和其他就行。

        static int caller(lua_State *L, const std::index_sequence<I...> &){if constexpr (std::is_void_v<Ret>){((*ptr)->*fp)(lua_to_cpp<remove_cvref<Args>>(L, 2 + I)...);return 0;}else{cpp_to_lua(L, ((*ptr)->*fp)(lua_to_cpp<remove_cvref<Args>>(L, 2 + I)...));return 1;}}

其余是参数,参数需要数量和类型才能进一步处理。C++11提供了typename... Args这种写法,当然也提供了遍历它的方式。在这里,由于要从Lua的栈上取值,需要构建一个栈索引,所以用make_index_sequence比较合适。

    template <typename C, typename Ret, typename... Args>class ClassRegister<Ret (C::*)(Args...)>{private:static constexpr auto indices =std::make_index_sequence<sizeof...(Args)>{};template <auto fp, size_t... I>static int caller(lua_State *L, const std::index_sequence<I...> &){T **ptr = (T **)luaL_checkudata(L, 1, class_name_);if constexpr (std::is_void_v<Ret>){((*ptr)->*fp)(lua_to_cpp<remove_cvref<Args>>(L, 2 + I)...);return 0;}else{cpp_to_lua(L, ((*ptr)->*fp)(lua_to_cpp<remove_cvref<Args>>(L, 2 + I)...));return 1;}}
};

从上面的代码可以看到,当注册test函数时,用ClassRegister<test> cr来实例化一个ClassRegister,这样test函数的返回值和参数都在ClassRegister的模板参数中了,同时用make_index_sequence根据参数个数生成一个0 1 2 3这样的序列I。然后取参数时,用lua_to_cpp(Args(L, 2 + I) ...)依次从Lua的栈上取值。lua_to_cpp(Args(L, 2 + I) ...)的意思是把参数Args一个个展开,然后以(L, 2 + I)作为参数去调用模板函数lua_to_cpp,代入test函数的参数,就依次调用

template <> inline int lua_to_cpp<int>(L, 2 + 0);
template <> inline double lua_to_cpp<double>(L, 2 + 1);
template <> inline const char *lua_to_cpp<const char *>(L, 2 + 2);

只要我们实现了各个类型的lua_to_cpp函数,那Lua栈的的参数就会被一个个取出来。现在有了this指针,有了函数指针,有了参数,就可以正确调用C++的函数了。

到了这里,一个基础的C++绑定Lua的机制也就完成了。但原理归原理,实际还会遇到许多问题。

lua_CFunction、C函数、成员函数、lua_CFunction成员函数

绑定不同类型参数的函数是实现了,可有时候,我们也希望不要自动推导参数而是手动一个个从Lua栈上取出参数。比如我们要写一个sha1函数,支持传多个字符串,自动把它们拼接起来计算出sha1值。

local val = sha1("abc", "def")
local val2 = sha1("abcdef")assert(val == val2)

这样,用const char *sha1(const char *str)就不合适,没法实现支持传入任意数量字符串,而用int sha1(lua_State *L)就可以实现。所以需要对lua_CFunction格式的函数进行特殊处理,不自动推导而是直接push到Lua。同样的,成员函数、static函数也是需要做一些特殊处理。最终,写了一串if else来特殊处理不同类型的函数

    template <auto fp> void def(const char *name){lua_CFunction cfp = nullptr;if constexpr (std::is_same_v<decltype(fp), lua_CFunction>){cfp = fp;}else if constexpr (!std::is_member_function_pointer_v<decltype(fp)>){cfp = StaticRegister<decltype(fp)>::template reg<fp>;}else if constexpr (is_lua_func<decltype(fp)>){cfp = &fun_thunk<fp>;}else{cfp = ClassRegister<decltype(fp)>::template reg<fp>;}luaL_getmetatable(L_, class_name_);lua_pushcfunction(L_, cfp);lua_setfield(L_, -2, name);lua_pop(L_, 1); /* drop class metatable */}

remove_cvref

在C++中,可以把函数或者变量指定为const,参数可以是引用,比如

int get() const
{
}void set(Val &v)
{
}

在模板推导中,加不加const和引用,推导出来的类型是不一样的,但对于C++和Lua交互来说,这个类型就是一样的,比如从Lua传进来的this指针,不存在是否为const这个说法。理想情况下,我们可以规定push到Lua的函数,不能加const这种修饰词,参数不作引用。但我在实际使用过程中,偶尔遇到一个函数,既需要在C++中调用,又需要在lua中调用,这时候又不想再多写一个专门push到lua的函数,所以在类型推导过程中往往多加了remove_cvref<Args>这个来去掉修饰词。

这提供了便利,但也增加了风险,假如有些人就直接修改了lua的一些引用呢?程序是真可能会出问题。

构造函数

C++的构造函数是个麻烦事,因为它和其他函数不一样,是没有返回值的,而且构建函数可以有多个的。那这就意味着上面的推导是解决不了这个问题。我最初的想法是每个类push到C++时,简单地调用默认构造函数,参数通过其他函数传入即可。但后来发现不行,比如说有些类是单例,可不希望有人在Lua另外创建一个实例。

最终我觉得比较稳妥的方案是:如果提供了默认构造函数,则使用默认构造函数,否则需要手动指定构造函数。若没有默认构造函数,也没有指定构造函数,则无法在Lua创建一个C++对象。

重载

重载意味着同一个函数名有多个函数,上面通过函数指针直接推导出函数参数和返回值的机制就会失效。目前没有太好的解决方案,可以像Sol2那样提供一个模板物化机制,或者用lambda来包一层。但我的意见是,push到Lua的函数不要有重载,换个函数名。

重载实现起来太过于麻烦,我没有兴趣去做这个。

C++调用Lua函数

需要在C++中调用Lua函数时,我原来一直是手动push参数,再直接调用lua_pcall的,毕竟C++调用Lua的地方总共加起来也没有几处。但是一想到C++绑定Lua的库都实现了,这个不包装一下实在说不过去。C++调用Lua,意味着参数是C++的,那它的类型就是确定了的,这个通过模板就能解决。具体的方案在[Howling at the Moon - Lua for C++ Programmers - Andreas Weis - CppCon 2017](https://github.com/CppCon/CppCon2017/blob/master/Presentations/Howling at the Moon - Lua for C%2B%2B Programmers/Howling at the Moon - Lua for C%2B%2B Programmers - Andreas Weis - CppCon 2017.pdf)上也有说过,我这里就不再说了。


/*** 调用lua全局函数,需要指定返回类型,如call<int>("func", 1, 2, 3)。错误会抛异常* @param name 函数名* @param Args 参数*/
template <typename Ret, typename... Args>
Ret call(lua_State *L, const char *name, Args... args)
{
#ifndef NDEBUGStackChecker sc(L);
#endiflua_getglobal(L, "__G_C_TRACKBACK"); // 需要自己在Lua实现trace函数assert(lua_isfunction(L, 1));lua_getglobal(L, name);(lcpp::cpp_to_lua(L, args), ...);const size_t nargs = sizeof...(Args);if (LUA_OK != lua_pcall(L, (int32_t)nargs, 0, 1)){std::string message("call ");message = message + name + " :" + lua_tostring(L, -1);lua_pop(L, 2); // pop error message and tracebackthrow std::runtime_error(message);}Ret v = lua_to_cpp<Ret>(L, -1);lua_pop(L, 2); // pop retturn v and traceback functionreturn v;
}

其他问题

  1. 为什么不直接用fp(lua_to_c<Args>(L, ++i), lua_to_c<Args>(L, i), ...)而用make_index_sequence
    上面的代码中,从Lua堆栈取参数时,是依次从栈位置1 2 3...取参数,那为什么不直接使用一个简单的++i呢?

嗯,一开始我确实是这样写的,而且跑起来确实没出问题。但后来在Linux下编译出现'multiple unsequenced modifications to 'i' [-Wunsequenced]'这个警告,我才意识到,lua_to_c是把参数从lua取出,放到C++的栈上作为fp的参数去调用。但在不同平台,参数入栈的顺序是由调用约定决定的,顺序是不一样的,这++i的值就会不一样,程序就要出bug了。

  1. 为什么用函数指针而不用upvalue
    许多C++绑定Lua的库,原始的函数指针是存在push到lua函数的upvalue中,而我写的是放在模板函数的参数auto fp中。我的本意是,通过模板参数调用肯定会比取upvalue更快,在编译时就已确定好,无需要管理。而其他库会放upvalue,是因为他们允许动态绑定,有一套生命周期管理,可以动态创建和释放这些函数。

  2. 异常安全问题
    C++与Lua交互一直有一个问题,C++中的对象是依赖C++本身的异常机制来构造和销毁的,即有错误发生,应该要抛一个异常才行。但是Lua使用的是C的异常机制,调用long jump,这可能会导出一些对象的析构函数没有调用。

当然可以以C++的方式编译Lua,但这没法保证。而我也没找到好的处理方式,也从未见过一了百了,完美的处理方式。但根据我的经验,只要你不是C++调用Lua再调用C++再调用Lua这样穿插着调用,并且在调用的过程中手动创建了对象,而又不愿意用pcall,一般是没有问题的。我可以保证一次库的调用安全,但没法保证多次。

例如,在Lua中调用一个C++函数,其中有一个参数是std::string类型,那它就会创建一个std::string对象。接着发现后面的参数不匹配,这时候会抛一个runtime_error,保证std::string对象,然后在最外层的函数catch这个runtime_error,再调用luaL_error,这样可以保证库接口的安全性。但这个luaL_error的影响,如果回到lua层没有xpcall而导致越过了一些C++代码,那就得由写代码的人负责了。

还有许多的细节,比如如果把一个类注册到Lua,如何把一个已有的对象指针push到Lua而不gc掉等等,这里就不再细说了。原本只想简单地实现,但修修补补了几回,也有一千行代码了,变得比预想中复杂了。整个代码我放在了lcpp.hpp中,有兴趣的可以去看代码。

http://www.hskmm.com/?act=detail&tid=26191

相关文章:

  • 代码随想录算法训练营第十天 | leetcode 232 225 20 1047
  • openJDK历史版本
  • 2025冲压件厂家权威推荐榜:冲压件/新能源冲压件/光伏冲压件/精密冲压件/异形冲压件/五金冲压件/铝冲压件/汽配冲压件/不锈钢冲压件/家具冲压件厂家公司精密制造与品质保障实力之选
  • 图解C++智能指针的循环引用
  • 国庆收心指南:用AI提示词工程解决节后综合征
  • CF1895F Fancy Arrays
  • 文件系统的全局结构
  • 2025.10.7
  • 自由型象棋分析程序
  • 前端HTML contenteditable 属性使用指南 - 教程
  • luogu P1648 看守
  • 题解:P11219 【MX-S4-T3】「yyOI R2」youyou 的序列 II
  • Seismic Unix 基础使用
  • 2025实验室净化厂家/实验室装修厂家/实验室建设厂家权威推荐榜:专业设计与洁净技术实力之选
  • 修改注册表,实现电脑小键盘开机自启(NumLock灯常亮)
  • 完整教程:nav2笔记-250603
  • Bartender打印乱序条码教程
  • 多Agent协作入门:基于A2A协议的Agent通信
  • 时尚产品需求预测与库存优化模型解析
  • 自制带得分和推荐走法的象棋视频
  • DP分析黑科技——闫氏DP分析法
  • MUGEN游戏引擎等一系列相关杂谈
  • # 20232313 2025-2026-1 《网络与系统攻防技术》实验一实验报告 - 20232313
  • 一生一芯学习:PA2:输入输出
  • vector使用中的一个小问题
  • OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering() - 指南
  • 2025.10.7——2绿
  • 完整教程:无人机避障——感知部分(Ubuntu 20.04 复现Vins Fusion跑数据集)胎教级教程
  • 我真的博了
  • 2025.10.6——1绿1蓝