)
前言虽说Lua热更已经有很多年了但还是有老登项目在一直用C#如果你想从C#转到Lua那么这篇文章绝对是你的不二之选。本文从lua底层虚拟机实现不极度扣细节意在让读者能从编程语言的最底层语义、语法分析跳过来理解lua的基础和特性做到知根知底方能百战不殆。Lua的跨平台lua为什么能够跨平台因为使用的是纯c语言虚拟机解释器Lua虚拟机编译、运行规则luac命令主要有两个用途第一作为编译器把Lua源文件编译成二进制chunk文件第二作为反编译器分析二进制chunk将信息输出到控制台。整套流程详细流程Lua 源码 / 二进制代码│ Lua 解析器▼chunk│ luaL_loadxxx()▼被编译成 Lua 函数││ 放到 lua_State 的栈顶▼┌─────────────────┐│ lua_State *L ││ ││ Lua 栈 │◄──── Lua C API 操作│ 调用帧 │ lua_pushxxx()│ 执行状态 │ lua_gettop()│ PC 等 │ lua_pcall()└────────┬────────┘│▼Lua VM执行 Lua 字节码Chunks书中的定义是Chunk 是一系列语句Lua 执行的每一块语句比如一个文件或者交互模式下的一行都是一个 Chunk。一段可以被Lua解释器解释执行的代码就叫作chunkLua编译器以函数为单位进行编译每一个函数都会被Lua编译器编译为一个内部结构这个结构叫作“原型”Prototype。原型主要包含6部分内容分别是函数基本信息包括参数数量、局部变量数量等、字节码、常量表、Upvalue表、调式信息、子函数原型列表。由此可知函数原型是一种递归结构并且Lua源码中函数的嵌套关系会直接反映在编译后的原型里。把主函数编译成函数原型后Lua编译器会给它再添加一个头部然后一起dump成luac.out文件。头部总共占用约30个字节因平台而异其中包含签名、版本号、格式号、各种整数类型占用的字节数以及大小端和浮点数格式识别信息等lua虚拟机就是解析chunk的解析器LuaAPI控制lua栈接口让其他语言能操作lua栈与lua代码交互Lua State、Lua栈解析原型把原型信息、操作码放到栈里进行操作取指令、解码、执行...数据类型8种boolnilfunctionnumberstringuserData、thread、table。nil空booleans只有nil是false其余都是truenumber小于100,000,000,000,000就没问题万亿string会根据内容和使用的运算符在number与string之间自动转换。..在Lua中是字符串连接符当在一个数字后面写..时必须加上空格以防止被解释错。 尽管字符串和数字可以自动转换但两者是不同的像 10 10这样的比较永远都是错的。如果需要显式将string转成数字可以使用函数tonumber()如果string不是正确的数字该函数将返回nil。string 在虚拟机种内存管理Functionlua种函数也是变量userDataLua 自己不理解的一块“外部数据”但 Lua 可以持有它、传递它、给它挂 metatable并在 GC 时通知 C。用途是把 C/C 对象包装成 Lua 对象。使用luaAPIc调用luaC Player*↓放进 userdata↓userdata 压入 Lua 栈↓lua_setglobal设成 Lua 全局变量↓Lua 获得 player在哪里找到函数lua_register(L, add, lua_add); int lua_add(lua_State *L) { int a lua_tointeger(L, 1); int b lua_tointeger(L, 2); lua_pushinteger(L, a b); return 1; }使用注册的方式把add注册到lua里。然后调用luaAPI拿去参数。(在xlua中wrapper 做的就是这个)Thread(暂时忽略)值和类型相比c语言没有和-and、or、notand和or的运算结果不是true和false而是和它的两个操作数相关。可以理解为返回最后判断的哪个操作数从左到右链接运算符..表使用{}来表示注意如果没用指定key则自动为从1开始的索引局部变量local i 0;if conditions then then-part elseif conditions then elseif-part .. ---多个 elseif else else-part end;for 将用 exp3 作为 step 从 exp1初始值到 exp2终止值执行 loop-part。其中exp3可以省略默认step1for varexp1,exp2,exp3 do loop-part endipairs、pairs和for in后续补充函数基本语法function func_name (arguments-list) statements-list; end;函数默认使用abc.func_name(abc,x)调用。但是第一个参数传o会比较麻烦然后有abcfunc_name(abc,x); 这个语法糖。多返回值function maximum (a) local mi 1 -- maximum index local m a[mi] -- maximum value for i,val in ipairs(a) do if val m then mi i m val end end return m, mi end print(maximum({8,10,23,12,5})) -- 23 3也可以用多个参数接收多个返回值多退少补补nil可变参数lua的可变参数需要再函数列表中使用三点...表示在函数内部可以用arg[n]拿取其实根据前面所说函数也是一个数据类型所以如下两句是一样的function foo (x) return 2*x end foo function (x) return 2*x end闭包和其他语言的闭包类似但函数也可以作为返回值所以function newCounter() local i 0 return function() -- anonymous function i i 1 return i end end c1 newCounter() print(c1()) -- 1 print(c1()) -- 2lua能正确处理尾递归并且不会出现栈溢出function f(x) return g(x) end迭代器Pairs / ipairs无状态迭代器状态作为迭代函数返回值for循环给你传递给下一次迭代多状态迭代器把原本需要作为返回值传递的状态改为存储在内部table 或闭包中包了一层用闭包真正的迭代器内部循环外部只处理数据理解有问题ipairs和pairs都是无状态迭代器坑ipairs中途遇到nil会停止Table、Metatable元表Lua表本质上是关联数组。Lua 5.0开始改用混合数据结构来实现表元表就是一系列元方法的合集。__index实现面向对象的基础。写一个元表然后把它作为“模板”替换掉一个table的元表这就是new的过程__newindex给不存在的值写入__calltable 被当成函数调用__add、__sub、__mul、__eq 、__lt、__le重写运算符__tostring转换成字符串__len长度__concat重载链接预算夫__pairs自定义迭代器__gcgc清理之前调用Module模块require(Player)↓查找 Player.lua↓加载文件↓执行 Player.lua↓得到 return Player↓缓存起来↓返回 Playerrequire是单次加载加载后会缓存哪怕多次调用也只加载一次dofile(Player.lua)则是重新加载协程本质给每个协程保存一套独立的 Lua 执行上下文栈、调用帧、程序计数器等yield时保存当前执行状态resume时恢复这套状态继续执行。对比unityunity相当于每帧自动帮你执行了resume而lua需要手动。垃圾回收GCRoot Lua VM 自己确定还在使用的“起点引用”。GC Root├── 全局环境├── 当前 Lua 执行栈├── 活跃协程├── 注册表 / VM 内部引用└── 其他 Lua VM 持有的对象如何识别垃圾从GCRoot出发能不能找到这个对象。解决了环引用但整个环都没被引用的问题。如何回收标记活对象三色标记白未扫描灰当前扫描位置的子节点黑扫描位置并且有变动后重处理颜色、清扫白色。清扫不是一次性完成是增量gc分帧对比c#同lua的机制但是增加了代际GC对比cpp智能指针:有几个strong owner引用计数循环引用会导致内存无法被回收。但是weakptr能解决观察对象但是不增加strong count。unique_ptr则是仅在生命周期内生效对比unity对于destory只是把native object实际看到的销毁了。但是未来这个类还是要等C#的垃圾回收机制去回收1、Lua GC、C# GC、Unity Native Object 生命周期是三个不同的问题。2、何时可能会泄露lua 、c# 的比如staic匿名里的 等 长时间挂在root上的。cpp的就是尽可能不要出现循环引用Lua 与 C# 的互操作lua操作c#读取lua栈然后把结果塞回栈的过程Lua代码↓访问 CS 类型 / C# 对象↓Lua metatable__index / __newindex↓找到 xLua 注册好的 Wrapper↓调用 XXXWrap._m_XXX(lua_State*)↓从 Lua Stack 读取参数↓ObjectTranslator 把 userdata 转成真正的 C# 对象↓Wrapper 直接调用 C# 方法↓C# 返回值↓ObjectTranslator / LuaAPI Push 到 Lua Stack↓返回 Luac#操作lua找到lua函数然后通过栈执行函数最后从栈中取出结果的过程C#add.Call(10, 20)↓找到对应 Lua function↓把 10、20 Push 到 Lua Stack↓lua_pcall()↓Lua VM 执行Add(10, 20)↓30 放到 Lua Stack↓ObjectTranslator.Get↓C# result 30