类型安全的 C++/Lua 任意参数互调用

在 C++ 和 Lua 协作时,双方的互调用是一个绕不开的话题。通常情况下,我们直接使用 Lua/C API 就可以完成普通的参数传递过程。但在代码中直接操作 lua stack,容易写出繁冗和重复的代码。这时我们往往会借助 tolua++ 之类的库,把参数传递的工作自动化,降低负担。

进一步讲,由于 Lua 的参数传递在个数和类型上非常灵活(任何一个函数可以传递任意个数和类型的参数),有时我们会希望在与 C++ 的互操作时保留这种灵活性,比如 C++ 向 Lua 发一个消息时,如果可以是一个消息 ID 带上任意数量和类型的参数,就会很方便(反过来也一样)。由于 C++ 能通过可变参的模板函数实现类型安全的参数传递,与 Lua 的动态参数列表相结合后,我们就能在一个接口上实现更大的跨语言自由度。

有不少第三方库能够简化 C++ 和 Lua 之间的互调用,这次我们使用 LuaBridge 来完成工作。开始前我们先介绍一下普通的互调用怎么做。

首先,从 C++ 调 Lua 的函数:

-- lua side
function foo(str, i, f)
    return string.format("%s, %d, %f", str, i, f)
end
// C side
luabridge::LuaRef foo = luabridge::getGlobal(L, "foo");
auto retString = foo("bar", 12, 0.25f);   // 这里先忽略错误处理
接着是 Lua 调 C++:

// C side
int CallMe(const std::string& arg1, const std::string& arg2)
{
    return std::stoi(arg1) + std::stoi(arg2); // 同样先不管错误处理
}

luabridge::getGlobalNamespace(L)
    .beginNamespace("native")
    .addFunction("call_me", CallMe)
    .endNamespace();
-- lua side
sum = native.call_me("15", "20")    -- sum = 35
嗯,可以看到,在 LuaBridge 的帮助下,双方互调用的参数和返回值符合各自的习惯,不用写任何额外的代码。

好了,热身完毕。现在我们看一下 C++ 调用 Lua 的可变参接口。

-- lua side
function g_post(msgID, ...)
    _queue:appendMsg({id=msgID, args={...}})
end
我们在可作为 functor 使用的 luabridge::LuaRef 上做一个简单的封装,如下:

template<class TRet, class... U>
TRet PostMessage(U&&... u)
{
    // 获取对应的函数
    auto refFunc = GetGlobal("g_post");
    if (!refFunc.isFunction())
        return luabridge::LuaRef(L);

    // 生成携带所有参数的 functor
    auto func = std::bind(refFunc, std::forward<U>(u)...);

    // implCallGlobal() 实现略, 使用 try/catch 处理错误,并把返回值转回需要的类型
    return implCallGlobal(name, func);
}
有了这样的接口,就可以在 C++ 这边用下面的方式去调:

// C side
PostMessage(MsgType_A, "foo", "bar");
PostMessage(MsgType_B, 100, 0.25f, std::string("std::string goes as well.");
// 任意的参数组合...
而在 Lua 端的队列里,就可以得到

-- lua side
{ id=MsgType_A, args={"foo", "bar" } }
{ id=MsgType_B, args={100, 0.25, "std::string goes as well." } }
-- args 表内可以容纳传过来的任意参数
对于特定的消息类型,Lua 只需检测自己关心的参数是否匹配即可。
这样从某种程度上把动态语言的灵活性延伸到了宿主语言。

而反过来 Lua 以任意参数化的方式调 C++ 就稍麻烦一点,因为 C++ 本质上是静态的,函数的参数类型需要在编译时完全确定。

我们可以这么做:

-- 在 Lua 端简单封装一下
function g_post_native(msgID, ...)
    native.post(msgID, {...})
end
// C side
int Post(int msgID, luabridge::LuaRef args)
{
    // 这里的 switch 可以用 template <int N> 来避免分支处理,并消除每一个 case 内重复的代码。具体实现暂略,这里为了清晰直接手写
    switch (msgID)
    {
        case MsgA:
        {
            auto t = tuple_cast<std::string, std::string>(args);
            return ProcessA(std::get<0>(t), std::get<1>(t));
        }
        case MsgB:
        {
            auto t = tuple_cast<int, float, float>(args);
            return ProcessB(std::get<0>(t), std::get<1>(t), std::get<2>(t));
        }
    }

    return FAILED_BAD_ID;
}
这里使用 tuple_cast 的好处是把所有的类型转换重复代码收拢到一处,对自定义类型的扩展也很容易。 tuple_cast() 函数本质上是把一个 LuaRef 根据期望类型(由模板参数指定)展开成一个 std::tuple,对于任何一组给定的类型,递归地在编译期完成展开。具体的技术在之前的 blog 中有提到,这里不再赘述。

好了,现在可以在 Lua 端这样调了:

-- lua side
g_post_native(MsgType_A, "foo", "bar");
g_post_native(MsgType_B, 100, 0.1, 12.5);
然后在 C++ 端直接定义接受明确参数列表的函数

// C side
int ProcessA(const std::string& s1, const std::string& s2);
int ProcessB(int arg1, float arg2, float arg3);
这样的最大好处是,不管是写脚本的脚本程序员,还是写宿主语言的工程师,都可以以各自语言习惯的方式去写,尤其是 C++ 端程序员,总是可以用 tuple_cast 转成自己期望的参数列表,让所有的接口函数做到 self-documenting。

时间: 2024-09-08 19:11:29

类型安全的 C++/Lua 任意参数互调用的相关文章

Lua 脚本怎么样调用外部脚本

在游戏脚本开发中,我们往往会发现脚本量非常大,而且我们经常会在一些核心脚本文件中定义常用的功能函数,但是Lua脚本没有提供include关键词,那又是怎样调用外部函数的呢?如何实现脚本的Include功能? test.lua脚本定义main函数如下: function main(szName, num1, num2) print("main()", szName, num1, num2); local nRandMax = 10000; local nRand = math.rando

求关于lua尾调用概念疑问

问题描述 求关于lua尾调用概念疑问 lua初学者,看programming in lua一书中写道:"Lua中类似return g(...)这种格式的调用是尾调用.但是g和g的参数都可以是复杂表达式,因为Lua会在调用之前计算表达式的值.例如下面的调用是尾调用:return x[i].foo(x[j] + a*b, i + j)" 但是,另外几个例子中却不是尾调用,如下都不是尾调用 return g(x) + 1 -- must do the addition return x or

Lua教程(十九):C调用Lua_Lua

1. 基础:     Lua的一项重要用途就是作为一种配置语言.现在从一个简单的示例开始吧.   复制代码 代码如下:     --这里是用Lua代码定义的窗口大小的配置信息     width = 200     height = 300       下面是读取配置信息的C/C++代码:   复制代码 代码如下: #include <stdio.h> #include <string.h> #include <lua.hpp> #include <lauxlib

C++中调用Lua配置文件和响应函数示例_Lua

Lua是脚本语言,最大的优势就是轻巧灵便,不用编译.当C的框架写好,只要更改lua的相应处理即可以更改功能,并且不用重新编译.以下是在C中调用Lua资源方法的示例程序:   C++端: // Lua1.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<stdio.h> extern "C" { //如不用extern会出现连接错误,编译成了C++文件 #include <lua.h> #

Lua中调用C语言函数实例_Lua

在上一篇文章(C调用lua函数)中,讲述了如何用c语言调用lua函数,通常,A语言能调用B语言,反过来也是成立的.正如Java与c语言之间使用JNI来互调,Lua与C也可以互调. 当lua调用c函数时,使用了和c调用lua中的同一种栈,c函数从栈中得到函数,然后将结果压入栈中.为了区分返回结果和栈中的其他值,每一个函数返回结果的个数. 这里有个重要的概念:这个栈不是全局的结构,每个函数都有自己的私有局部栈.哪怕c函数调用了lua代码,lua代码再次调用该c函数,他们有各自独立的局部栈.第一个参数

Lua脚本调用外部脚本_Lua

test.lua脚本定义main函数如下: function main(szName, num1, num2) print("main()", szName, num1, num2); local nRandMax = 10000; local nRand = math.random(nRandMax); print("nRand =", nRand) return 1; end 现在我想在test.lua脚本中调用另外一个test1.lua脚本文件中的GetRan

Lua教程(二十):Lua调用C函数_Lua

Lua可以调用C函数的能力将极大的提高Lua的可扩展性和可用性.对于有些和操作系统相关的功能,或者是对效率要求较高的模块,我们完全可以通过C函数来实现,之后再通过Lua调用指定的C函数.对于那些可被Lua调用的C函数而言,其接口必须遵循Lua要求的形式,即typedef int (*lua_CFunction)(lua_State* L).简单说明一下,该函数类型仅仅包含一个表示Lua环境的指针作为其唯一的参数,实现者可以通过该指针进一步获取Lua代码中实际传入的参数.返回值是整型,表示该C函数

Lua调用自定义C模块_Lua

这是<Lua程序设计>中提到的,但是想成功执行,对于初学Lua的确没那么简单.这里涉及如何如何生成一个动态链接库so文件:Lua5.2中导出函数从LuaL_register变成了LuaL_newlib.对于具体的细节有待深入.这里的模块名是hello_lib, Lua解释器会根据名字找到对应的模块,而后执行其中的 luaopen_XXX方法. 代码: #include <math.h> #include <lua5.2/lua.h> #include <lua5.

Unity3D 预备知识:C#与Lua相互调用

在使用Unity开发游戏以支持热更新的方案中,使用ULua是比较成熟的一种方案.那么,在使用ULua之前,我们必须先搞清楚,C#与Lua是怎样交互的了? 一.基本原理 简单地说,c#调用lua, 是c# 通过Pinvoke方式调用了lua的dll(一个C库),然后这个dll执行了lua脚本.        ULua = Lua + LuaJit(解析器.解释器) +LuaInterface.        其中,LuaInterface中的核心就是C#通过Pinvoke对Lua C库调用的封装,