手动实现一个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.mzph.cn/news/930642.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录算法训练营第十天 | leetcode 232 225 20 1047

用栈实现队列 整体思路:使用两个栈一个当作入栈,一个当作出栈,当队列需要出队列的时候,将入栈的元素,倒序放入出栈里面,对出栈进行pop操作即可,若需要获得队头元素,同理,执行出栈peek操作即可,判断队列是否为…

重庆永川网站建设公司广东个人备案网站内容

SQL RDBMS 概念 RDBMS是关系数据库管理系统(Relational Database Management System)的缩写。 RDBMS是SQL的基础&#xff0c;也是所有现代数据库系统(如MS SQL Server、IBMDB2、Oracle、MySQL和MicrosoftAccess)的基础。 关系数据库管理系统(Relational Database Management Sy…

深入解析:黑马k8s(十七)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

怎么做下载类的网站课程平台网站建设报价

Gateway整合Sentinel ​ 前面使用过Sentinel组件对服务提供者、服务消费者进行流控、限流等操作。除此之外&#xff0c;Sentinel还支持对Gateway、Zuul等主流网关进行限流。 ​ 自sentinel1.6.0版开始&#xff0c;Sentinel提供了Gateway的适配模块&#xff0c;能针对路由(rou…

openJDK历史版本

https://jdk.java.net/archive/

2025冲压件厂家权威推荐榜:冲压件/新能源冲压件/光伏冲压件/精密冲压件/异形冲压件/五金冲压件/铝冲压件/汽配冲压件/不锈钢冲压件/家具冲压件厂家公司精密制造与品质保障实力之选

行业背景分析随着制造业向高端化、智能化方向转型升级,冲压件作为基础零部件在汽车制造、电子设备、家电产品等领域的应用日益广泛。2025年,中国冲压件市场规模预计将突破5000亿元,行业竞争格局呈现专业化、精细化发…

做a免费视频在线观看网站茂名网站制作公司

作者&#xff1a;billy 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 前言 目前能下载到的最新版本是 PyCharm 2021.1。 请注意对应 Python 的版本&#xff1a; Python 2: 2.7Python 3: >3.6, <3.11…

图解C++智能指针的循环引用

欢迎大家访问我的个人主页guts的小屋循环引用是学习智能指针过程中的一个小难点,笔者愚钝,明明知道是两个指针互相引用导致了内存泄漏,但看各种文字资料时,脑子里总是一团浆糊,感觉似懂非懂,于是自己绘制了几张图…

国庆收心指南:用AI提示词工程解决节后综合征

作为程序员,我们习惯用技术思维解决工作中的复杂问题。但面对国庆假期后的"不想上班"、"睡眠紊乱"、"注意力涣散",你有没有想过也可以用结构化的方法来应对?今天分享一个实用的AI工具…

做海鱼的网站网站后期维护怎么做

多维时序 | MATLAB实现CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测预测效果基本介…

CF1895F Fancy Arrays

题目大意: 设一个长度为 \(n\) 的数组是 “Fancy” 的,当且仅当它满足下面条件。\(|a_{i} - a_{i - 1}| \le k\) 存在 \(i\) 满足 \(x \le a_{i} \le x + k - 1\) \(a_{i} \ge 0\)给定 \(n,k,x\),求 "Fancy&qu…

高档手表网站自己弄一个网站要多少钱

拦截器 1 回顾过滤器&#xff1a; Servlet规范中的三大接口&#xff1a;Servlet接口&#xff0c;Filter接口、Listener接口。 过滤器接口&#xff0c;是Servlet2.3版本以来&#xff0c;定义的一种小型的&#xff0c;可插拔的Web组件&#xff0c;可以用来拦截和处理Servlet容…

文件系统的全局结构

物理格式化后 低级格式化,划分扇区,检测坏扇区,并用备用扇区替换坏掉的扇区 逻辑格式化后 磁盘分区(分卷)完成各部分的文件系统初始化 文件系统在外存🀄️的结构 分为用户区和内核区 近期访问过的目录文件会缓存…

完整教程:记一次idea中lombok无法使用的解决方案

完整教程:记一次idea中lombok无法使用的解决方案2025-10-07 17:02 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; displa…

网站建设培训的心得食品核酸第三方检测机构

杨淼信息与计算机科学 跆拳道国家一级教练员 曾经tricking爱好者3 人赞同了该回答一个不算建议的建议 直接看&#xff0c;看到不懂的就查相应的知识&#xff08;据说牛人都靠wiki活着&#xff09;。 知识当有需求的时候&#xff0c;才会学的更快。 觉得这样做的好处是&#xff…

详细介绍:在机器视觉测量和机器视觉定位中,棋盘格标定如何影响精度

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025.10.7

bot选前两问容易解决,得 \(E:\dfrac{x^2}{6}+\dfrac{y^2}{3}=1\),\(y_p\in(1,3]\) bot指出:第三问是可以直接联立爆算的! 设 \(P(x_0,y_0)\),则 \(MN:x_0x+2y_0y-6=0\),\(P\) 到 \(MN\) 的距离为: \[\dfrac{x_0…

自由型象棋分析程序

可以键盘操作。比如鼠标移到某个位置,按r出来个黑车。空格删掉棋子。还可以自己吃自己,空白吃自己等,我感觉摆残局最方便。 程序很乱。鄙人之前所发贴的拼凑版。再贴部分代码: httpd.py# !/usr/bin/python3 from e…

网站建设数据石家庄建站公司

文章目录 概念介绍使用方法示例代码 我们在上一章回中介绍了Icon这种Widget&#xff0c;本章回中将介绍 如何使用字体图标。闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 本章回中介绍的字体图标是指Icon中使用特殊的字体来充当图标&#xff0c;字体图标可以看作…

luogu P1648 看守

题目大意 给定 \(d\) 维坐标的 \(n\) 个点,每个点表示为 \(d\) 个数值,求两点间最大距离 \(d\) 维两点间距离为 \(|x_1-y_1|+|x_2-y_2|+...+|x_d-y_d|\) Sol 我们随便找两个点 \(A\),\(B\) 作为演示: \[dis_{A,B}=|…