1.Lua GC机制分析与理解-上
2.Lua5.4 源码剖析——虚拟机2 之 闭包与UpValue
3.Lua的源验证编译和反编译
4.LuaJIT源码分析(一)搭建调试环境
5.lua文件怎么打开?
6.如何在Window系统安装Luarocks(踩坑篇)
Lua GC机制分析与理解-上
lua的垃圾回收(Garbage Collect)在lua编程中占据关键地位,尤其在5.3.4版本的码去源码中,本文将基于此版本进行深入探讨。掉已lua采用的源验证是标记清除式GC算法,其流程包括标记和清除两步骤:标记阶段从若干根节点开始,码去逐层追踪相关对象;清除阶段遍历标记过的掉已来电全屏显示源码对象链表,删除不再需要的源验证内存。
在lua的码去垃圾回收中,使用白、掉已灰、源验证黑三种颜色标识对象的码去状态。白色代表可回收状态,掉已初始对象为白色,源验证表示未被访问;灰色代表待标记状态,码去已访问但引用的掉已其他对象未标记;黑色则表示对象已完全标记,不可回收。为了区分新建对象的特殊情况,lua引入了白1和白2,确保在标记阶段结束后的清除阶段,新创建的对象不会被错误地清除。
GC过程从新建对象开始,通过luaC_newobj函数创建可回收对象,并将其标记为白色。触发GC的条件包括手动调用或内存使用超过设定阈值。在lua5.1之后,引入了分步执行机制,提高了系统的实时性,核心函数singlestep负责管理整个过程。
标记阶段从根对象开始,将白色变为灰色,并加入灰色链表。清除阶段则根据对象类型分步进行,如字符串直接回收,其他类型逐个检查颜色并释放空间。整个过程非搬迁式,不涉及内存整理。
总结起来,lua的GC机制就是通过灰色链表进行标记,然后遍历内存链表进行清除。虽然本文主要基于5.3.4版本,但原理适用于不同版本的lua。任何理解或改进的建议,都欢迎读者批评指正,期待您的反馈,感谢阅读。
Lua5.4 源码剖析——虚拟机2 之 闭包与UpValue
故事将由我们拥有了一段 Lua 代码开始,我们先用 Lua 语言写一段简单的打印一加一计算结果的 Lua 代码,并把代码保存在 luatest.lua 文件中:
可执行的一个 Lua 文件或者一份单独的文本形式 Lua 代码,在 Lua 源码中叫做 "Chunk"。无论我们通过什么形式去执行,智能匹配源码或者用什么编辑器去执行,最终为了先载入这段 Lua 的 Chunk 到内存中,无外乎会归结到以下两种方式:1)Lua 文件的载入:require 函数 或 loadfile 函数;2)Lua 文本代码块的载入:load 函数;这两种方式最终都会来到下面源码《lparse.c》luaY_parser 函数。该函数是解析器的入口函数,负责完成代码解析工作,最终会创建并返回一个 Lua 闭包(LClosure),见下图的红框部分:
另外,上图中间有一行代码最终会调用到 statement 函数,statement 函数是 Chunk 解析的核心函数,它会一个一个字符地处理我们编写的 Lua 代码,完成词法分析和语法分析工作,想要了解字符处理整个状态流程的可以自行研读该部分源码,见源码《lparse.c》statement 函数部分代码:
完成了解析工作之后,luaY_parser 函数会把解析的所有成果放到 Lua 闭包(LClosure)对象之中,这些存储的内容能保证后续执行器能正常执行 Lua 闭包对应的代码。
Lua 闭包由 Proto(也叫函数原型)与 UpValue(也叫上值)构成,见源码《lobject.h》LClosure 定义,我们下面将进行详细的讲解:
UpValue 是 Lua 闭包数据相关的,在 Lua 的函数调用中,根据数据的作用范围可以把数据分为两种类型:1)内部数据:函数内部自己定义的数据,或者通过函数参数的形式传入的数据(在 Lua 中通过参数传入的数据本质上也是先赋值给一个局部变量);2)外部数据:在函数的更外层进行定义,脱离了该函数后仍然有效的数据;外部数据在我们的 Lua 闭包中就是 UpValue,也叫上值。
既然 Lua 支持函数嵌套,也知道了 UpValue 本质就是上层函数的内部数据。那么 UpValue 有必要存储于 Lua 闭包(LClosure)结构体当中吗?是为了性能考虑而做的一层指针引用缓存吗?回答:并不是基于性能的考虑,因为在实际的 Lua 运用场景中,函数嵌套的层数通常来说不会太多,个别函数多一层的查询访问判断不会带来过多的性能开销。需要在闭包当中存储 UpValue 主要原因是因为内存。Lua 作为一门精致小巧的脚本语言,设计初衷不希望占用过多的系统内存,它会尽量及时地清理内存中用不到的对象。在嵌套函数中,内层函数如果仍然有被引用处于有效状态,而外层函数已经没有被引用了已经无效了,此时 Lua 支持在保留内层函数的情况下,对外层函数进行清除,从而可以清理掉外层函数引用的非当前函数 UpValue 用途以外的大量数据内存。
尽管外层函数被清除了,Lua 仍然可以保持内层函数用到的 UpValue 值的有效性。UpValue 如何能继续保持有效,我们在之前的基础教程《基本数据类型 之 Function》里面学习过,主要是因为 UpValue 有 open 与 close 两种状态,当外层函数被清除的时候,UpValue 会有一个由 open 状态切换到 close 状态的过程,会对数据进行一定的处理,感兴趣的同学可以回到前面复习一下。
UpValue 有效性例子
接下来我们举一个代码例子与一个图例,庄股轨迹源码表现一下 UpValue 在退出外层函数后仍然生效的情况,看一下可以做什么样的功能需求,加深一下印象,请看代码与注释:
上述代码在执行 OutFunc 函数后,外层的 globalFunc 函数变量完成了赋值,每次对它进行调用,都将可以对它引用的 UpValue 值即 outUpValue 变量进行正常加 1。
函数的内部数据属于函数自身的内容,外部其它函数无法通过直接的方式访问其它函数的内部数据。函数自身的东西会存在于 LClosure 结构体的 Proto*p 字段中。Proto 全称 "Function Prototypes",通常也可以叫做 "函数原型",我们来看一下它的定义,见源码《lobject.h》Proto 结构体:
结构体字段比较多,我们先不细看,后面用到哪个字段会再进行补充说明。函数的内部数据分为常量与变量(即函数局部变量),分别对应上图的如下字段:
1)常量:TValue* k 为指针指向常量数组;int sizek 为函数内部定义的常量个数,也即常量数组 k 的元素个数。
2)局部变量:LocVar* locvars 为指针指向局部变量数组;int sizelocvars 为函数定义的局部变量个数,也即局部变量数组 locvars 的元素个数。
UpValue 的描述信息会存储在 Proto 结构体中的 Upvaldesc* upvalues 字段,解析器解析 Lua 代码的时候会生成这个 UpValue 描述信息,并用于生成指令,而执行器运行的时候可以通过该描述信息方便快速地构建出真正的 UpValue 数组。
至此,我们知道了函数拥有 UpValue,有常量,有局部变量。外部数据 UpValue 也讲完,内部数据也讲完。接下来,我们开始学习函数运行的逻辑指令相关内容。
函数逻辑指令存储于函数原型 Proto 结构体中,这些函数逻辑是由一行行的 Lua 代码构成的,代码会被解析器翻译成 Lua 虚拟机能识别的指令,我们把这些指令称为 "OpCode",也叫 "操作码"。Proto 结构体存储 OpCode 使用的是下图中红框部分字段,见源码《lobject.h》Proto 结构体:
至此,我们可以简单提前说一下 Lua 虚拟机的功能了,本质上来看,Lua 虚拟机的工作,就是为当前函数(或者当前一段 OpCode 数组)准备好数据,然后有序执行 OpCode 指令。
对 OpCode 有了一定的认识了,接下来我们要补充一个 OpCode 相关的 Lua 闭包相关的内容,就是 Lua 闭包的运行环境。
一个 Lua 文件在载入的佛山源码群时候会先创建出一个最顶层(Top level)的 Lua 闭包,该闭包默认带有一个 UpValue,这个 UpValue 的变量名为 "_ENV",它指向 Lua 虚拟机的全局变量表,即_G 表,可以理解为_G 表即为当前 Lua 文件中代码的运行环境 (env)。事实上,每一个 Lua 闭包它们第一个 UpValue 值都是_ENV。
ENV 的定义在我们之前提到的解析器相关函数 mainfunc 中,见源码《lparser.c》:
如果想要设置这个载入后的初始运行环境不使用默认的 _G 表,除了直接在该文件代码中重新赋值_ENV 变量这种粗暴且不推荐的方式以外,通常是通过我们前面提到的加载 Lua 文件函数或加载 Lua 字符串代码函数传入 env 参数(Table 类型),就可以用自定义的 Table 作为当前 Lua 闭包的全局变量环境了,env 参数为上面两个函数的最末尾一个参数,'[' 与 ']' 字符中的内容表示参数可选,函数的定义摘自 Lua5.4 官网文档:
所以我们可以在 Lua 代码通过 _ENV 访问当前环境:
在 Lua 的旧版本中,变量的查询最多会分为 3 步:1)先从函数局部变量中进行查找;2)找不到的话就从 UpValue 中查找;3)还找不到就从全局环境默认 _G 表查找。而在 Lua5.4 中,把 UpValue 与全局 _G 表的查询统一为 UpValue 查询,并把一些操作判断提前到了解析器解析阶段进行,例如函数内部使用的某个 UpVaue 变量在代码解析的时候就可以通过 UpValue 描述信息知道存储于 Lua 闭包 upvals 数组的哪个下标位置,在执行器运行的时候只需要直接在数组拿取对应下标的这个 UpValue 数据即可。
从 OpCode 的层面来看,Lua 除了支持通过一个 UpValue 数组下标访问一个 UpValue 变量,在把 _G 表合并到 UpValue 之后,Lua 为此实现了通过一个字符串 key 值从某个 Table 类型的 UpValue 中查询变量的操作。
至此,我们了解了 Lua 闭包的结构与运行环境,以及 OpCode 的基本概念。接下来,我们将深入学习 OpCode,掌握 OpCode 就掌握了整个 Lua 虚拟机数据与逻辑的流向。
Lua的编译和反编译
无论是Unity项目还是Unreal的项目,我通常会使用Lua进行编程。在项目打包阶段,Lua的编译和反编译是不可或缺的步骤。在本文中,我们将探讨如何对Lua代码进行编译与反编译,以及如何利用不同的工具进行操作。
对于Lua代码的编译,我们通常有两种方法。一种是使用lua脚本直接运行代码,另一种是使用Lua的编译器(如Luac)将源代码转换为Lua字节码。通过使用指令`lua ./TestLua.lua`,我们可以测试代码的正确性。Luac是将Lua源代码编译为Lua字节码的工具,编译成功后,我们可以通过运行编译后的字节码来验证结果,一切顺利。
另一种流行的外买源码Lua编译器是Luajit,它在Unity项目中被广泛使用。使用Luajit可以提升执行速度。如果遇到编译错误,只需确保将`luajit\src\src\jit`文件放在`luajit.exe`的同一目录下的`lua`文件夹中即可。通过直接运行包含测试代码的Lua文件,我们可以确认编译和运行的流程是正确的。
在对比了两种编译方法后,我们发现它们都有各自的特点和适用场景。Luac适用于简单的脚本或对代码优化要求不高的情况,而Luajit则更适合需要高性能的项目,特别是那些对运行速度有较高要求的场景。
对于Lua的反编译,最常用的工具是`luadec`。通过将`luadec`工具与Visual Studio项目进行集成,我们能够对编译后的字节码进行反编译,恢复源代码。在尝试反编译后,我们得到了清晰可读的代码,即使在不使用调试信息的情况下,反编译结果也具有一定的可读性。
对于更复杂的反编译需求,如支持位字节码的反编译,我们遇到了一些挑战。目前,有一个名为`ljd`的工具支持位字节码的反编译,但仅限于位平台。对于位平台的字节码,我们可能需要自行修改`ljd`的Python代码来支持,这是一个需要时间和专业知识的额外工作。尽管如此,对于大部分应用场景,上述工具已经足够满足我们的需求。
总之,Lua的编译和反编译是Lua项目开发过程中的重要环节。通过选择合适的编译工具和反编译方法,可以有效提升代码的执行效率和调试效率。同时,对于反编译过程,我们应根据实际需求选择合适的工具,并注意其适用的平台和特性。
LuaJIT源码分析(一)搭建调试环境
LuaJIT,这个以高效著称的lua即时编译器(JIT),因其源码资料稀缺,促使我们不得不自建环境进行深入学习。分析源码的第一步,就是搭建一个可用于调试的环境,但即使是这个初始步骤,能找到的指导也相当有限,反映出LuaJIT的编译过程复杂性。
首先,从官方git仓库开始,通过命令`git clone https://luajit.org/git/luajit.git`获取源代码。GitHub上也有相应的镜像地址。对于调试,LuaJIT提供msvcbuild.bat脚本,位于src目录下,它将编译过程分为三个阶段:构建minilua,用于平台判断和执行lua脚本;buildvm生成库函数映射;以及lua库的编译和最终LuaJIT的生成。该脚本需在Visual Studio Command Prompt环境中以管理员权限运行,且有四个可选编译参数。
在调试时,我们无需这些选项,但需要保留中间代码。因此,需要在脚本中注释掉清理代码的部分。在Visual Studio 的位命令提示符中,切换到src目录并运行`msvcbuild.bat`。编译过程快速,成功时会看到日志信息。在src目录下,luajit.exe即为lua虚拟机。
接着,在src目录的同级目录创建一个VS工程,将源文件和头文件添加进来。初次尝试调试可能会遇到关于strerror函数安全性的警告,这可以通过在工程属性中添加_CRT_SECURE_NO_WARNINGS宏来解决。然而,链接阶段可能会出现重复定义的错误,这与ljamalg.c文件的编译选项有关。amalg选项用于生成单个大文件,以优化代码,但我们通常不启用它。
排除ljamalg.c后,再次尝试调试,可能还需要手动添加buildvm阶段生成的目标文件。当LuaJIT启动并设置好断点后,就可以开始调试源码了。至此,你已经成功搭建了一个LuaJIT的调试环境,为深入理解其工作原理铺平了道路。
lua文件怎么打开?
Lua文件可以使用文本编辑器或专用的Lua开发环境来打开和编辑。 Lua文件本质上是一种文本文件,包含了Lua语言的源代码。因此,任何能够编辑文本的软件都可以用来打开和编辑Lua文件。常见的文本编辑器有Windows系统上的记事本,macOS系统上的TextEdit,以及跨平台的Sublime Text、VS Code等。这些编辑器都提供了基本的文本编辑功能,如打开、编辑、保存文件等。 除了文本编辑器,还可以使用专用的Lua开发环境来打开和编辑Lua文件。Lua IDE提供了更加丰富的功能,如代码高亮、代码提示、代码调试等,这些功能可以大大提高Lua开发的效率。常见的Lua IDE有ZeroBrane Studio、Decoda IDE等。这些IDE通常都提供了友好的用户界面和丰富的开发工具,使得Lua开发变得更加便捷和高效。 在使用文本编辑器或Lua IDE打开Lua文件时,只需要在软件中选择“打开”功能,然后浏览到Lua文件的存储位置,选择文件并打开即可。如果使用的是命令行界面,也可以使用命令行文本编辑器来打开和编辑Lua文件。 总之,Lua文件可以使用任何文本编辑器或专用的Lua开发环境来打开和编辑。选择哪种方式取决于个人的开发习惯和需求。对于简单的Lua脚本编辑,文本编辑器可能就足够了;而对于复杂的Lua项目开发,使用专用的Lua IDE可能会更加高效。如何在Window系统安装Luarocks(踩坑篇)
了解Luarocks,Lua的软件包管理器,为Lua编程语言用户提供了安装和管理模块的便捷方式。Luarocks相当于Python的pip,允许用户从中央存储库、本地文件或URL安装Lua模块。模块集成为Lua扩展,增强其功能。Luarocks还能协助管理模块间的依赖关系,确保所有必需模块自动安装。
若要安装Luarocks,首先需准备安装包和第三方工具。推荐使用Mingw作为编译环境,避免安装错误版本导致的编译问题。下载Mingw后,解压并放置在个人偏好位置,将可执行文件添加至环境变量中。通过命令行运行gcc --version,确认环境配置成功。
接着,下载Lua源码并解压,新建build.cmd文件,输入编译指令。双击执行,待编译完成,即可在build文件夹中找到Lua执行文件。
下载Luarocks源码并解压,使用提供的编译指令在命令行中执行。配置环境变量后,便能轻松安装Lua的第三方库。
安装第三方Lua库时,可能遇到编译问题,如找不到md5对比工具,需要安装OpenSSL解决。在命令行输入lua命令后,可能会遇到找不到.dll文件的情况,可通过以下两种方法解决,确保顺利使用第三方库。
Luarocks简化了Lua开发者的软件包管理,提供了一种高效、便捷的方式扩展Lua功能,助力开发者更轻松地开发Lua应用程序。
《Lua5.4 源码剖析——基本数据类型 之 Function》
在编程语言中,函数作为重要的元素,可以分为第一类值语言和第二类值语言。第一类值语言如Lua,其函数与数值类型、布尔类型地位相同,可动态创建、存储与销毁;第二类值语言则无法实现这些操作。Lua是第一类值语言,支持动态函数创建与销毁。
在Lua中,函数的基本类型枚举为LUA_TFUNCTION,对应8位二进制为 。函数类型变体包括三种:LUA_VLCL(Lua闭包)、LUA_VLCF(C函数指针)和LUA_CCCL(C语言闭包)。闭包由函数与UpValue组成,UpValue为在当前函数外声明但函数内可以访问的变量,类似于局部变量但具备一定作用域。
闭包分为C类型闭包与Lua类型闭包。C类型闭包在Lua源代码中由C语言实现,主要用于调用C函数。Lua类型闭包则在Lua中动态创建,支持多层嵌套与UpValue管理。闭包实现方式包括C语言闭包和Lua闭包。
Lua闭包由ClosureHeader宏定义,包含闭包的类型标识、UpValue数组长度、垃圾回收列表等信息。闭包内部的函数通过Proto数据结构定义,包含参数数量、最大寄存器数量、UpValue数量等属性。Lua闭包中的UpValue通过UpVal类型管理,UpVal状态分为open和close两种,open状态时UpVal存储在链表中,close状态时UpVal的值被保存,直到函数返回时才被销毁。
在实现多返回值时,Lua通过调整运行堆栈的结构,将多个返回值合并,减少内存使用。在尾调用消除中,Lua在函数执行结束时,复用当前函数的栈空间进行下一次函数调用,避免了堆栈溢出的问题。Lua的尾调用优化使得函数调用效率更高,程序运行更稳定。
自制组态软件()lua编译器之语法分析
前文已经完成了词法分析,将lua源码切割成一系列的token,接下来我们将处理这些token。以下是我们需要分析的lua文件内容:
该文件首先定义了一个lua函数,并随后调用它。为了简化处理,我们先支持以下lua文件内容,我们移除了函数定义和调用,突出显示if语句。
首先,我们定义了block,因为所有语句都在block中。例如,下面的block中包含了两条if语句。
block的定义如下:stats是语句数组,表示该block中的所有语句,retExps是返回语句的表达式。
接下来,我们定义if语句。exps是表达式的数组,用于记录if语句的表达式,blocks用于表示if语句的语句块。现在,让我们来看看函数调用表达式。prefixExp为前缀,在此例中为"setValue",args为函数的参数。
语句分析完毕,我们再来看表达式的解析,我们要支持的表达式为:加法和相等判断都是二元运算,因此我们定义了二元运算。
材料已经准备就绪,我们现在来实现语法分析。从block开始。
上述内容都很好理解,我们通过循环调用parseStat函数处理一条条语句,生成block。接下来看看处理语句的parseStat。
可以看出,我们现在支持两种语句:一是if语句,二是函数调用语句。先看看if语句。
nextTokenOfKind函数用于判断当前token是否为参数中的类型,如果不是,则直接报错。
然后调用parseExp解析if的表达式,表达式将在语句解析完成后处理。
parseBlock函数用于解析if条件满足时运行的语句。
如果if语句有else语句,则同样调用parseBlock函数来解析else条件满足时运行的语句。
解析完if语句,再看看函数调用语句的解析,即parseAssignOrFuncCallStat函数。
首先会创建nameExp表达式,因为函数名是一个标识符,将在parsePrefixExp中被处理。然后调用_finishPrefixExp函数,由于标识符后是"("符号,所以会调用_finishFuncCallExp函数,在这个函数中会调用_parseArgs处理函数调用的参数,最后生成funcCallExp表达式,函数解析完成。
语句分析完毕,我们再来看表达式的解析,即parseExp函数。
这段逻辑与系列()讲的内容一致,这里不再过多解释,不明白的可以参考()讲。
最后,我们来看看函数参数的解析,即_parseArgs函数。
_parseArgs函数首先跳过函数调用开头的"(",然后调用parseExpList函数,这个函数调用parseExp函数完成函数参数的解析。
好了,本文到此结束。
项目地址:GitHub - zhzhz/iscada