C语言和Lua之间交互的原理

为什么Lua可以作为热更新语言

首先我们得知道什么是热更新,简单来说,就是在用户通下载安装APP之后,打开App时遇到的即时更新。本质是代码更新而不是资源更新,大型手游都是将补丁资源放在专门的WEB服务器上,游戏启动时动态下载并放入到游戏的持久化目录中。

由于不同类型的语言有不同的运行机制,编译型语言如C#,是先编译成一整块中间码然后在不同平台上被.NET运行时解释执行,这就是说使用C#编写的APK或IPA安装到手机上后是没有任何C#文件的。这样就算运行时将作为补丁的C#文件从WEB服务器上下载到持久化目录也运行不了。而lua解释型语言,并不需要事先编译成块,而是运行时动态解释执行的。这样LUA就和普通的游戏资源如图片,文本没有区别,因此可以在运行时直接从WEB服务器上下载到持久化目录并被其它LUA文件调用。

lua是个嵌入式脚本语言,本身就是C写的,所以Lua脚本可以很容易的被C/C++代码调用,也可以反过来调用C/C++的函数。lua语法、解释器、执行原理都与python相似唯一差距就是lua没有强大的类库作为支撑,Lua只是具备了一些比如数学运算和字符串处理等简单的基本功能。所以lua不适合作为开发独立应用程序的语言。轻量级 LUA语言的官方版本只包括一个精简的核心和最基本的库。这使得LUA体积小、启动速度快,从而适合嵌入在别的程序里。  可扩展 LUA并不象其它许多"大而全"的语言那样,包括很多功能,比如网络通讯、图形界面等。但是LUA可以很容易地被扩展:由宿主语言(通常是C或C++)提供这些功能,LUA可以使用它们,就像是本来就内置的功能一样。

Lua和C语言之间的通信原理:虚拟栈

Lua库中没有定义任何全局变量,他将所有的状态都保存在动态结构lua_State中

为什么要用栈来储存数据而不是定义类似于lua_Value的类型呢

  1. lua_Value很难将复杂的类型映射到其他语言中
  2. Lua引擎无法搜索出一个保存在C变量中的lua table, 会认为这个table是垃圾并回收它

作用:可以解决C语言与lua语言之间的差异:

  1. Lua要求垃圾回收而C语言要求显式释放内存
  2. Lua使用动态类型而C语言使用静态类型

栈的基本操作:

  • 传值给lua: 先将值压入栈,再调用Lua API, 将其值从栈中弹出
  • 从lua中拿值: 调用Lua API,将指定值压入栈中

栈的操作规则:

  • 严格按照LIFO(last in first out,先进后出)
  • 调用Lua时,Lua只会改变栈的顶部;C代码对此栈可以查询删除插入中间元素

栈的操作函数

  • 基础操作函数:

luaL_newstate:用于创建一个新环境或状态;// lua_State *L = luaL_newstate(); 

luaL_openlibs: 辅助库函数可以打开所有的标准库;

luaL_loadbuff: 编译用户输入的每行内容并将编译后的程序块压入栈中

lua_pcall: 将程序块从栈中弹出,并在保护模式下进行,成功返回0,若执行发生错误则会向栈中压入一条错误消息

lua_tostring: 可获取栈中的错误消息

lua_pop: 将程序块从栈中弹出删除

  • 压入元素

每个C类型,lua都有对应的压入函数: lua_pushnumber, lua_pushboolean, lua_pushinteger, lua_pushstring...

需要确保栈中有足够的空间,一般是20个空闲的槽(由MINSTACK定义)

  • 查询元素

API使用索引来引用栈中的元素,第一个压入栈的是1,最后一个压入栈的是-1,以此类推

一般跟着类型检测函数: lua_isnumber, lua_istable etc.

或者是类型转换函数: lua_tonumber, lua_tostring etc.

  • 对栈的增加查询删除操作函数

lua_gettop (lua_State * L): 返回栈中元素的个数

lua_settop (lua_State * L, int index): 修改栈中元素的数量;如果比以前的多则从nil补充

lua_pushvalue (lua_State * L, int index): 将指定索引的值副本压入栈中

lua_remove (lua_State * L, int index): 将指定索引的值移除

lua_insert (lua_State * L, int index): 将指定索引的位置上开辟新元素,再将栈顶元素移到该位置

lua_replace (lua_State * L, int index): 弹出栈顶值,并将该值设置到指定的索引上

API中的错误处理:

用lua_pcall来调用Lua代码,在保护模式下运行。如果发生了内存分配错误,lua_pcall会返回错误代码

当C函数检测出一个错误时,应该调用lua_error。此函数会清理Lua中所有需要清理的东西,然后跳转回发起执行的那个lua_pcall, 并附上一条错误消息

C转Lua Table操作

将C语言的struct转变成Lua的table

设置table, 将字段名和字段值压入栈中,并调用lua_settable创建table
void setfield(lua_State* L, const char* key, int value)
    lua_pushstring(L, key)
    lua_pushnumber(L, value/MAX_COLOR);
    lua_settable(L, -3)
}

从C调用Lua函数

流程

  1. 函数压入栈
  2. 参数压入栈
  3. lua_pcall进行函数调用
  4. 弹出返回值
// lua file
function add(x, y)
  return x+y
end
// C++
lua_getglobalname(L, 'add') 1. 把lua函数压入栈
lua_pushnumber(L, x) 2. 把参数压入栈
lua_pushnumber(L, y)
if (lua_pcall(L, 2, 1, 0) != 0) 3. 用lua_pcall进行函数调用
  error(L, "wrong function: %s", lua_tostring(L, -1));
if(!lua_isnumber(L, -1) // 验证返回值
  error(L, "must return number");
z = lua_tonumber(L, -1);
lua_pop(L, -1); 4. 将返回值从栈中弹出
return z; 

从Lua调用C函数

所有注册到Lua中的函数都具有相同的原型:typedef int (*lua_Cfunction) (lua_State* L);

仅有一个参数且为Lua的状态

// 将要在Lua中调用的C函数add()
static int add(luaState* L)
{
    int x = lua_tonumber(L, 1);
  int y = lua_tonumber(L, 2);
  lua_pushnumber(L, x+y)
  return L;
}
 
lua使用C函数前必须先注册这个函数
lua_pushcfunction(L, add); // 压入C函数类型的值
lua_setglobal(L, "myAdd"); // 将该值赋予全局变量myAdd<br><br>之后就可以在lua中直接调用myAdd(x, y)

数组操作

数组操作函数:

  • lua_rawgeti(lua_State* L, int index, int key),相当于:
  1. lua_pushnumber(L, key);
  2. lua_rawget(L, index);
  • lua_rawseti(lua_State* L, int index, int key),相当于:
  1. lua_pushnumber(L, key);
  2. lua_insert(L, -2,); // key放在前一个元素下面,因为前一个元素为赋值value
  3. lua_rawset(L, t);
  • index表示table在栈中的位置,key表示元素在table的位置

例子:一个变换函数在C中,对lua table里的值应用了一个给定函数

int turnMap(lua_State* L)
{
    luaL_checkType(L, 1, LUA_TABLE);
    luaL_checkType(L, 2, LUA_TFUNCTION);
    int n = lua_objlen(L, 1)
    for(int i = 1; i <=n; i++)
    {
        lua_pushvalue(L, 2);// 将索引位置的拷贝值压入栈顶,也就是f
        lua_rawgetti(L, 1, i); // 压入t[i]
        lua_call(L, 1, 1); // 调用 f(t[i]),结果压入栈顶
        lua_rawseti(L, 1, i); // t[i] = 结果
    }
 
    return 0;
}

 

posted @ 2023-11-20 17:19  ImreW  阅读(75)  评论(0编辑  收藏  举报