C语言和Lua之间交互的原理
建议去看《Lua程序设计》24-28章,里面详细介绍了Lua和C语言之间的通信原理,多看函数是怎么调用的,就会理解了虚拟栈是怎么操作的,以下是我看完后的总结。
为什么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的类型呢
- lua_Value很难将复杂的类型映射到其他语言中
- Lua引擎无法搜索出一个保存在C变量中的lua table, 会认为这个table是垃圾并回收它
作用:可以解决C语言与lua语言之间的差异:
- Lua要求垃圾回收而C语言要求显式释放内存
- 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
1 2 3 4 5 6 | <code-pre class = "code-pre" id= "pre-DEMt66" ><code-line class = "line-numbers-rows" ></code-line>设置table, 将字段名和字段值压入栈中,并调用lua_settable创建table <code-line class = "line-numbers-rows" ></code-line> void setfield(lua_State* L, const char * key, int value) <code-line class = "line-numbers-rows" ></code-line> lua_pushstring(L, key) <code-line class = "line-numbers-rows" ></code-line> lua_pushnumber(L, value/MAX_COLOR); <code-line class = "line-numbers-rows" ></code-line> lua_settable(L, -3) <code-line class = "line-numbers-rows" ></code-line>}</code-pre> |
ColorTable("GREEN", 1, 0, 0) -> background = GREEN = {r = 1, g = 0, b = 0}
1 2 3 4 5 6 | <code-pre class = "code-pre" id= "pre-mm7JDy" ><code-line class = "line-numbers-rows" ></code-line> struct ColorTable{<br> char * name;<br> int red;<br> int green;<br>}<br><br> void setColor(lua_State* L, struct ColorTable *c){ <code-line class = "line-numbers-rows" ></code-line> lua_newTable(L) <code-line class = "line-numbers-rows" ></code-line> setfield(L, 'r' , c->red); <code-line class = "line-numbers-rows" ></code-line> setfield(L, 'g' , c->green); <code-line class = "line-numbers-rows" ></code-line> lua_setglobal(L, c->name); <code-line class = "line-numbers-rows" ></code-line>}</code-pre> |
拿Lua table里的值: background = {r = 1, g = 0, b = 0}
1 2 3 | <code-pre class = "code-pre" id= "pre-sAWcZ8" ><code-line class = "line-numbers-rows" ></code-line>lua_setglobal(L, "background" ) <code-line class = "line-numbers-rows" ></code-line> if lua_istable(L, -1){ <code-line class = "line-numbers-rows" ></code-line> red = lua_getfield(L, 'r' );<br> green = lua_getfield(L, 'g' );<br> blue = lua_getfield(L, 'b' );<br>} </code-pre> |
从C调用Lua函数
流程:
- 函数压入栈
- 参数压入栈
- lua_pcall进行函数调用
- 弹出返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <code-pre class = "code-pre" id= "pre-fMm6cm" ><code-line class = "line-numbers-rows" ></code-line> // lua file <code-line class = "line-numbers-rows" ></code-line>function add(x, y) <code-line class = "line-numbers-rows" ></code-line> return x+y <code-line class = "line-numbers-rows" ></code-line>end <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> // C++ <code-line class = "line-numbers-rows" ></code-line>lua_getglobalname(L, 'add' ) 1. 把lua函数压入栈 <code-line class = "line-numbers-rows" ></code-line>lua_pushnumber(L, x) 2. 把参数压入栈 <code-line class = "line-numbers-rows" ></code-line>lua_pushnumber(L, y) <code-line class = "line-numbers-rows" ></code-line> if (lua_pcall(L, 2, 1, 0) != 0) 3. 用lua_pcall进行函数调用 <code-line class = "line-numbers-rows" ></code-line> error(L, "wrong function: %s" , lua_tostring(L, -1)); <code-line class = "line-numbers-rows" ></code-line> if (!lua_isnumber(L, -1) // 验证返回值 <code-line class = "line-numbers-rows" ></code-line> error(L, "must return number" ); <code-line class = "line-numbers-rows" ></code-line>z = lua_tonumber(L, -1); <code-line class = "line-numbers-rows" ></code-line>lua_pop(L, -1); 4. 将返回值从栈中弹出 <code-line class = "line-numbers-rows" ></code-line> return z; </code-pre> |
从Lua调用C函数
所有注册到Lua中的函数都具有相同的原型:typedef int (*lua_Cfunction) (lua_State* L); 仅有一个参数且为Lua的状态
1 2 3 4 5 6 7 8 9 10 11 12 | <code-pre class = "code-pre" id= "pre-H8S3ck" ><code-line class = "line-numbers-rows" ></code-line> // 将要在Lua中调用的C函数add() <code-line class = "line-numbers-rows" ></code-line> static int add(luaState* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> int x = lua_tonumber(L, 1); <code-line class = "line-numbers-rows" ></code-line> int y = lua_tonumber(L, 2); <code-line class = "line-numbers-rows" ></code-line> lua_pushnumber(L, x+y) <code-line class = "line-numbers-rows" ></code-line> return L; <code-line class = "line-numbers-rows" ></code-line>} <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line>lua使用C函数前必须先注册这个函数 <code-line class = "line-numbers-rows" ></code-line>lua_pushcfunction(L, add); // 压入C函数类型的值 <code-line class = "line-numbers-rows" ></code-line>lua_setglobal(L, "myAdd" ); // 将该值赋予全局变量myAdd<br><br>之后就可以在lua中直接调用myAdd(x, y)</code-pre> |
数组操作
数组操作函数:
- lua_rawgeti(lua_State* L, int index, int key),相当于:
- lua_pushnumber(L, key);
- lua_rawget(L, index);
- lua_rawseti(lua_State* L, int index, int key),相当于:
- lua_pushnumber(L, key);
- lua_insert(L, -2,); // key放在前一个元素下面,因为前一个元素为赋值value
- lua_rawset(L, t);
- index表示table在栈中的位置,key表示元素在table的位置
例子:一个变换函数在C中,对lua table里的值应用了一个给定函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <code-pre class = "code-pre" id= "pre-hTx62R" ><code-line class = "line-numbers-rows" ></code-line> int turnMap(lua_State* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> luaL_checkType(L, 1, LUA_TABLE); <code-line class = "line-numbers-rows" ></code-line> luaL_checkType(L, 2, LUA_TFUNCTION); <code-line class = "line-numbers-rows" ></code-line> int n = lua_objlen(L, 1) <code-line class = "line-numbers-rows" ></code-line> for ( int i = 1; i <=n; i++) <code-line class = "line-numbers-rows" ></code-line> { <code-line class = "line-numbers-rows" ></code-line> lua_pushvalue(L, 2); // 将索引位置的拷贝值压入栈顶,也就是f <code-line class = "line-numbers-rows" ></code-line> lua_rawgetti(L, 1, i); // 压入t[i] <code-line class = "line-numbers-rows" ></code-line> lua_call(L, 1, 1); // 调用 f(t[i]),结果压入栈顶 <code-line class = "line-numbers-rows" ></code-line> lua_rawseti(L, 1, i); // t[i] = 结果 <code-line class = "line-numbers-rows" ></code-line> } <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> return 0; <code-line class = "line-numbers-rows" ></code-line>} </code-pre> |
upvalue
类似于C语言中的静态变量机制,只在一个特定的函数里可见
关键函数:
- lua_pushclosure(lua_State* L, lua_function* function, int number): 第二个参数是基础函数,第三个参数是upvalue的数量
- upvalue的初始化必须是在创建closure之前
- 每个closure可以有不同的upvalue
- 一个函数可以创建多个closure
- lua_upvalueindex(int index): 生成upvalue的伪索引,可以像其他栈索引一样使用,不同的是它不在栈上
- index不能为负数
示例1:不断增加的counter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <code-pre class = "code-pre" id= "pre-DKpKBz" ><code-line class = "line-numbers-rows" ></code-line> static int count(lua_State* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> int val = lua_tointeger(L, lua_upvalueindex(1)); //拿到第一个也是唯一一个upvalue <code-line class = "line-numbers-rows" ></code-line> lua_pushinteger(L, ++val); <code-line class = "line-numbers-rows" ></code-line> lua_pushvalue(L, -1); // 复制结果压入栈中 <code-line class = "line-numbers-rows" ></code-line> lua_replace(L, lua_upvalueindex(1)); // 更新upvalue <code-line class = "line-numbers-rows" ></code-line> return 1 <code-line class = "line-numbers-rows" ></code-line>} <code-line class = "line-numbers-rows" ></code-line> int newCount(lua_State* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> lua_pushnumber(L, 0); // 设置upvalue的初始值为0 <code-line class = "line-numbers-rows" ></code-line> lua_pushclosure(L, &count, 1) // 创建closure,建立该函数与upvalue之间的联系 <code-line class = "line-numbers-rows" ></code-line> } </code-pre> |
示例2:upvalue实现tuple
tuple_new是用于创建tuple的函数,由于参数已经在栈中,所以只需要将这些参数作为upvalue,并调用lua_pushcclosure来创建基于t_tuple的closure即可。数组tuplelib和luaopen_tuple是创建库的标准代码,tuple库中只有new函数
关键函数:
- luaL_Reg: 数组元素结构,有两个字段,一个字符串和一个函数指针
- 必须以NULL, NULL结尾代表结束
- luaL_register(lua_State* L, string functionName, const struct luaL_Reg* lib):根据给定的名字“functionName”创建一个table,并用数组lib中的信息填充这个table
- 函数返回时会把这个table留在栈中
- luaL_opint(lua_State* L, int index, int num): 类似于luaL_checkint, 但它允许参数为null,若不存在则返回默认值(0)
- lua_isnone(lua_State* L, lua_upvalueindex(int i)): 用于测试upvalue是否存在
- lua_argcheck(lua_State* L, bool condition, int index, string errMsg): 用于检测condition是否满足,若不满足则返回error message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <code-pre class = "code-pre" id= "pre-pjn2cY" ><code-line class = "line-numbers-rows" ></code-line>tuple的使用 <code-line class = "line-numbers-rows" ></code-line>x = tuple. new (10, 'a' , {}, 3) <code-line class = "line-numbers-rows" ></code-line>print(x(1)) // 10 <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> int luaopen_tuple(lua_State* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> luaL_register(L, "tuple" , tuplelib); // 给tuple注册函数列表 <code-line class = "line-numbers-rows" ></code-line> return 1; <code-line class = "line-numbers-rows" ></code-line>} <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> static const struct luaL_Reg tuplelib[] = <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> { "new" , tuple_new}, <code-line class = "line-numbers-rows" ></code-line> {NULL, NULL}, //必须以null结尾 <code-line class = "line-numbers-rows" ></code-line>}; <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> int tuple_new(lua_State* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> lua_pushcclosure(L, t_tuple, lua_gettop(L); // 创建closure,参数的个数为upvalue的数量 <code-line class = "line-numbers-rows" ></code-line> return 1; <code-line class = "line-numbers-rows" ></code-line>} <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> int t_tuple(lua_State* L) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> int op = luaL_opint(L, 1, 0);<br> if (op == 0)<br> {<br> // 遍历所有的upvalue并压入栈中<br> for(int i = 1; !lua_isnone(L, lua_upvalueindex(i)); i++)<br> {<br> lua_pushvalue(L, lua_upvalueindex(i);<br> }<br> return i - 1; // return upvalue的数量<br> }<br> else<br> {<br> luaL_argcheck(L, op > 0, 1, "out of range");<br> if (lua_isnone(L, lua_upvalueindex(op)) // 无此字段<br> return 0;<br> lua_pushvalue(L, lua_upvalueindex(op);<br> return 1;<br> }<br>}</code-pre> |
元表
一种辨别不同类型的userdata的方法是为每种类型创建一个唯一的元表,每当得到一个userdata,就检查它是否拥有正确的元表
- luaL_setmetatable: 创建一个新的table作为元表,并将其压入栈顶,然后将这个table与注册表中的指定名称关联起来
- luaL_getmetatable:在注册表中搜索与tname关联的元表
面对对象的操作
1 2 3 4 5 6 7 8 9 | <code-pre class = "code-pre" id= "pre-YXcBxN" ><code-line class = "line-numbers-rows" ></code-line>local metaarray = getmetatable(array. new (1)) <code-line class = "line-numbers-rows" ></code-line>metaarray.--index = metaarray // 当a时userdata时是没有size key的,因此lua会通过a元表的--index,找到自己本身metaarray,然后再找到size <code-line class = "line-numbers-rows" ></code-line>metaarray.size = array.size <code-line class = "line-numbers-rows" ></code-line> static const struct luaL_Reg arraylib_m [] = { <code-line class = "line-numbers-rows" ></code-line>{ "--newindex" , setarray}, //元方法 将a:setarray(i, 0) 变成 a[i] = 0 <code-line class = "line-numbers-rows" ></code-line>{ "--index" , getarray}, //将a:get(i) 变成 a[i] <code-line class = "line-numbers-rows" ></code-line>{ "--len" , getsize}, <code-line class = "line-numbers-rows" ></code-line>{NULL, NULL} <code-line class = "line-numbers-rows" ></code-line>}</code-pre> |
luaL_register(L, NULL, arraylib_m): 以NULL作为库名,是不会创建任何用于储存函数的table,而是以栈顶的table作为存储函数的table
这样就可以直接调用metaarray.getarray(10)
C#与Lua的交互很容易产生GC,该怎么优化呢?
首先看个例子,下面func1的性能比func2的性能好几十倍,造成这个差异的“元凶”就是装箱和拆箱
1 2 3 4 5 6 7 8 9 | <code-pre class = "code-pre" id= "pre-6ZMDQx" ><code-line class = "line-numbers-rows" ></code-line> int func1( int i) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> return i + 1; <code-line class = "line-numbers-rows" ></code-line>} <code-line class = "line-numbers-rows" ></code-line> <code-line class = "line-numbers-rows" ></code-line> object func2( object o) <code-line class = "line-numbers-rows" ></code-line>{ <code-line class = "line-numbers-rows" ></code-line> return ( int )o + 1; <code-line class = "line-numbers-rows" ></code-line>}</code-pre> |
交互的优化的方向主要是减少装箱和拆箱的次数
SLua的优化思路:把lua的栈操作api暴露出来,一个个参数的压栈,调用完一个个返回值的取。这些压栈和取返回值的接口都是确定类型的,也就是说都是类似于func1的接口
- 用lua重新实现了Vector3的所有方法
- 不要直接传Vector3/Quaternion等unity值类型, 改为传三个float x, floaty, float z
- 频繁调用的函数,参数的数量要控制
- 优先使用static函数导出,减少使用成员方法导出。一个object要访问成员方法或者成员变量,都需要查找lua userdata和c#对象的引用,或者查找metatable,耗时甚多。直接导出static函数,可以减少这样的消耗。比如 LuaUtil.SetPos(obj, pos.x, pos.y,pos.z)
Reference:
- 《Lua程序设计》第24-28章
- https://gameinstitute.qq.com/community/detail/125117
- https://blog.csdn.net/sm9sun/article/details/68946343
__EOF__

本文链接:https://www.cnblogs.com/cancantrbl/p/15987888.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理