zifa2003293 发表于 2023-2-3 10:38

Lua与C交互原理

前言
游戏开发中必不可少就是热更新了,目前的热更新方案主要有两种:基于Lua的热更新解决方案,比如xLua、toLua、sLua、uLua 和 基于C#的热更新方案 HybridCLR,两种方案各有优缺点,本文目前重点不是介绍这些。作为铺垫,其实要想更好的理解基于Lua的热更新方案,明白Lua与C(宿主语言)交互的原理是至关重要的,下面将简单的介绍一下这个过程。
lua和C交互的核心就是lua栈,lua和C的所有数据交互都是通过lua栈来完成的。
1、 C调用lua
C调用lua很简单,通常lua都是作为宿主语言(本例是C)的扩展脚本,在运行时,宿主语言需要读取lua脚本数据,其主要步骤:
1)加载lua脚本 luaL_loadfile
2)运行lua脚本 lua_pcall
3)获取相关数据 lua_getglobal ...
4)使用相关数据 lua_tostring lua_pcall ...
在宿主语言中运行lua脚本的时候,lua也需要能够访问到C(宿主语言)中定义的一些变量或者函数,这个过程大致为:
1)将C变量或函数push到lua栈中
2)通过lua_setglobal为当前lua栈顶的函数或变量命名,这样在lua中可通过该名字完成对变量或函数的使用
3)之后可在加载的lua脚本中使用C变量或函数
2、在lua脚本中调用C
lua中使用C的方式大多数都是通过库文件的形式来实现的,将C函数编译为动态库文件,这样可以在lua主程序中,加载这个库文件,并使用其中的C函数。下面举个简单的例子来说明一下整体的大概流程。
假设要给lua使用的C代码定义在mylib.c中
1)在mylib.c中定义给lua调用的C函数,函数原型一般都是 int (lua_State*) 这样的等等,如:
static int c_addsub(lua_State* L)
{
    double a = luaL_checknumber(L,1); // 获取参数1
    double b = luaL_checknumber(L,2); // 获取参数2
    lua_pushnumber(L, a+b); // 压入返回值1
    lua_pushnumber(L, a-b); // 压入返回值2
    return 2; // 两个返回值
}2)在mylib.c中定义一个注册函数,用于lua在加载C动态库时,调用该函数完成对库中所导出的C函数的注册,如:
// 将C模块中要导出的C函数放入到luaL_Reg结构体数组内
static const struct luaL_Reg l[] = {
    {"addsub", c_addsub},
    {NULL, NULL} // 以NULL标识结束
};
// 该函数在导入C库时调用 完成对库中导出的函数的注册 必须是non-static
int luaopen_mylib(lua_State* L)
{
    // 完成实际的注册工作
    // 注册方式一: luaL_openlib(lua_State* L, const char* name, const luaL_Reg* l, int nup)
    //   L : lua_State
    // name: 表明该C库被加载后,所导出的函数位于哪一个全局table之下
    //       这里是"clib" 那么之后lua中通过clib.addsub完成对C函数的调用
    //   l : 要导出的函数的lua_Reg结构体数组
    //         luaL_openlib自动将该数组内的name-function对注册并填充到第二参数指定的table下
    // nup : upvalue的个数,如果不为0,则注册的所有函数都共享这些upvalues
    luaL_openlib(L, "clib", l, 0);

    // 注册方式二: luaL_newlibtable + luaL_setfuncs (等价于lua_newlib)
    // luaL_newlibtable(L, l);
    // luaL_setfuncs(L, l, 0);
    // 前两句等价于:
    // luaL_newlib(L, l);

    // 将包含name-cfunction键值对的table返回给lua
    return 1;
}注意上面方式一和方式二的主要区别:前者(luaL_openlib)为name-cfunction对在lua中注册了一个名字(“clib”),而后者(luaL_newlib)没有,它只是将这个table返回给了lua。更多的关于luaL_openlib和luaL_newlib注册方式的差异以及他们各自的注册原理,有深入理解需求的可以参阅官方文档或者其他资料。本质上来讲,其交互核心都是类似的,只是将C作为动态库时,我们需要提供一个”入口函数”,用以在加载该动态库后执行,完成对库中所有导出函数的注册。
3)将相关C文件编译成动态链接库
把一个源码文件编译成动态库很简单,但不同平台编译指令可能不一样,这个可以自行查阅,比如在Linux下,命令大概就是这样的:gcc -c mylib.c
4)在lua中加载C动态库
方式一 : 使用 loadlib
--加载C动态库 并将luaopen_mylib函数 导出到mylib变量中
mylib = loadlib("./mylib.so", "luaopen_mylib")

--调用mylib() 将执行lua_openmylib函数(在上述第二步中提到的函数) 完成对C动态库中所有导出函数的注册
--将C中返回的name-cfunction table赋给clualib变量
clualib = mylib()

--通过clualib完成C函数的调用
sum, diff = clualib.addsub(5.6, 2.4);

--针对于注册方式一,还可以通过luaL_openlib中传入的clib来使用C函数
sum, diff = clib.addsub(5.6, 2.4)方式二 : 使用 require
clualib = require("mylib")

sum,diff = clualib.addsub(5.6, 2.4)

--对于luaL_openlib完成的注册,仍然可以通过clib来访问C函数
sum, diff = clib.addsub(5.6, 2.4)关于loadlib与require工作原理差异,不是本文的重点,有需要的可以查看其他资料或者<<Programing in Lua>>这本经典的书籍。
页: [1]
查看完整版本: Lua与C交互原理