如何实现可调用任意参数函数的通用包装器,以简化C++函数与对象的Lua调用对接
如何实现可调用任意参数函数的通用包装器,以简化C++函数与对象的Lua调用对接
兄弟,我太懂你这种想高效复用的心情了——你已经有给VBScript用的COM dispatch那套成熟逻辑,现在要对接Lua,肯定不想挨个给几十上百个C函数写重复的Lua栈操作包装对吧?其实完全可以实现一个通用的包装器,自动把C函数/对象成员函数和Lua的调用逻辑对接起来,不用手动写每个函数的适配代码,和你之前的COM dispatch思路异曲同工。
核心思路
本质上就是用C++的模板(尤其是变参模板)做一层中间适配:
- 自动从Lua栈上读取对应类型的参数,转换成C++函数需要的参数类型
- 调用目标C++函数/成员函数
- 自动把C++函数的返回值压回Lua栈,供Lua脚本使用
这样不管你的函数有多少参数、是什么返回类型,只要在通用模板里覆盖了对应的类型处理逻辑,就能自动适配,不用手写重复代码。
通用函数包装器实现(普通全局函数)
先从最简单的普通全局函数开始,我们先写基础的类型读取和参数展开逻辑:
#include <lua.hpp> #include <tuple> #include <type_traits> #include <utility> // 基础类型:从Lua栈读取指定类型的参数 template<typename T> T lua_get_param(lua_State* L, int idx) { if constexpr (std::is_same_v<T, int>) { return luaL_checkinteger(L, idx); } else if constexpr (std::is_same_v<T, float>) { return static_cast<float>(luaL_checknumber(L, idx)); } else if constexpr (std::is_same_v<T, double>) { return luaL_checknumber(L, idx); } else if constexpr (std::is_same_v<T, const char*>) { return luaL_checkstring(L, idx); } else if constexpr (std::is_same_v<T, std::string>) { return std::string(luaL_checkstring(L, idx)); } // 这里可以扩展更多自定义类型,比如把Lua userdata转换成C++对象指针 } // 变参模板展开:读取所有函数需要的参数 template<typename... Args, std::size_t... Is> std::tuple<Args...> lua_get_all_params(lua_State* L, std::index_sequence<Is...>) { return { lua_get_param<Args>(L, Is + 1)... }; } template<typename... Args> std::tuple<Args...> lua_get_all_params(lua_State* L) { return lua_get_all_params<Args...>(L, std::index_sequence_for<Args...>{}); } // 通用函数包装器:适配任意签名的普通函数 template<typename Ret, typename... Args> int lua_wrap_global_func(lua_State* L, Ret (*func)(Args...)) { // 检查参数数量是否匹配 if (lua_gettop(L) != sizeof...(Args)) { return luaL_error(L, "Expected %zu arguments, got %d", sizeof...(Args), lua_gettop(L)); } // 读取所有参数 auto args = lua_get_all_params<Args...>(L); // 调用目标函数 Ret result = std::apply(func, args); // 将返回值压入Lua栈(无返回值则不压栈) if constexpr (!std::is_void_v<Ret>) { if constexpr (std::is_same_v<Ret, int>) { lua_pushinteger(L, result); } else if constexpr (std::is_same_v<Ret, float> || std::is_same_v<Ret, double>) { lua_pushnumber(L, result); } else if constexpr (std::is_same_v<Ret, const char*> || std::is_same_v<Ret, std::string>) { lua_pushstring(L, result.c_str()); } // 扩展自定义类型的返回值处理 return 1; } return 0; }
成员函数的通用包装
如果要对接C++类的成员函数,需要额外处理Lua栈上的对象实例(一般存在userdata里),我们稍微修改一下包装逻辑:
// 成员函数的参数读取:跳过第一个userdata参数 template<typename... Args, std::size_t... Is> std::tuple<Args...> lua_get_member_params(lua_State* L, std::index_sequence<Is...>) { // 成员函数调用时,Lua栈第一个参数是对象实例,所以函数参数从索引2开始 return { lua_get_param<Args>(L, Is + 2)... }; } template<typename... Args> std::tuple<Args...> lua_get_member_params(lua_State* L) { return lua_get_member_params<Args...>(L, std::index_sequence_for<Args...>{}); } // 通用成员函数包装器 template<typename Class, typename Ret, typename... Args> int lua_wrap_member_func(lua_State* L, Ret (Class::*func)(Args...)) { // 检查参数数量:对象实例 + 函数参数 if (lua_gettop(L) != sizeof...(Args) + 1) { return luaL_error(L, "Expected %zu arguments, got %d", sizeof...(Args)+1, lua_gettop(L)); } // 从userdata获取C++对象实例(需要提前给类注册元表) Class* obj = static_cast<Class*>(luaL_checkudata(L, 1, "YourClassMetaTableName")); if (!obj) { return luaL_error(L, "Invalid object instance"); } // 读取成员函数需要的参数 auto args = lua_get_member_params<Args...>(L); // 调用成员函数 Ret result = std::apply([obj, func](Args... args) { return (obj->*func)(args...); }, args); // 处理返回值,逻辑和普通函数一致 if constexpr (!std::is_void_v<Ret>) { // 压栈逻辑,同上 return 1; } return 0; }
注册到Lua环境
有了通用包装器,接下来就是把C++函数注册到Lua里,我们可以写简单的注册辅助函数,甚至用宏来简化:
// 注册全局函数到Lua template<typename Ret, typename... Args> void register_global_func(lua_State* L, const char* func_name, Ret (*func)(Args...)) { // 包装成Lua可调用的C函数 lua_pushcclosure(L, [](lua_State* L) -> int { return lua_wrap_global_func(L, reinterpret_cast<Ret(*)(Args...)>(lua_topointer(L, lua_upvalueindex(1)))); }, 1); lua_pushlightuserdata(L, reinterpret_cast<void*>(func)); lua_setglobal(L, func_name); } // 注册成员函数到类的元表 template<typename Class, typename Ret, typename... Args> void register_member_func(lua_State* L, const char* meta_table_name, const char* func_name, Ret (Class::*func)(Args...)) { luaL_getmetatable(L, meta_table_name); lua_pushcclosure(L, [](lua_State* L) -> int { return lua_wrap_member_func(L, reinterpret_cast<Ret(Class::*)(Args...)>(lua_topointer(L, lua_upvalueindex(1)))); }, 1); lua_pushlightuserdata(L, reinterpret_cast<void*>(func)); lua_setfield(L, -2, func_name); lua_pop(L, 1); } // 用宏简化注册,避免手动写模板参数 #define REGISTER_GLOBAL_FUNC(L, func) \ register_global_func(L, #func, func) #define REGISTER_MEMBER_FUNC(L, meta_table, func) \ register_member_func(L, meta_table, #func, &std::remove_pointer_t<decltype(this)>::func)
注意事项
- 自定义类型处理:如果你的函数涉及自定义C++类,需要把类实例包装成Lua的userdata,并给它设置元表,这样Lua才能识别并传递给包装器。
- 错误处理:上面的代码里加了基础的参数数量检查,你还可以扩展类型检查,比如Lua栈上的参数类型和C++函数要求不匹配时,抛出更明确的Lua错误。
- 重载函数:如果有重载函数,需要手动指定模板参数,或者用不同的函数名注册到Lua。
- 性能:模板展开是编译期完成的,运行时几乎没有额外开销,不用担心性能问题。
这个方案和你之前的COM dispatch思路类似,都是做一层通用的参数/返回值映射,把重复的适配逻辑抽象出来,这样你只需要写一次通用包装器,剩下的就是注册函数/成员函数,工作量会大大减少。
内容来源于stack exchange
相关产品推荐
相关产品推荐

