《Lua5.4 源码剖析——基本数据类型 之 Function》
在编程语言中,函数作为重要的码游码分元素,可以分为第一类值语言和第二类值语言。戏源享论第一类值语言如Lua,游戏a源其函数与数值类型、码游码分布尔类型地位相同,戏源享论东方财富通板块地区源码可动态创建、游戏a源存储与销毁;第二类值语言则无法实现这些操作。码游码分Lua是戏源享论第一类值语言,支持动态函数创建与销毁。游戏a源
在Lua中,码游码分函数的戏源享论基本类型枚举为LUA_TFUNCTION,对应8位二进制为 。游戏a源函数类型变体包括三种: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的word源码包尾调用优化使得函数调用效率更高,程序运行更稳定。
前端开发Lua篇——LuaJIT
三十六计手游采用LuaJIT实现游戏逻辑,但在特定场景下禁用了JIT模式。具体操作步骤如下:
1. 首先,从LuaJIT官网获取与cocos2dx引擎版本一致的库文件。例如,针对cocos2dx版本号3.,需确保lua和jit的版本信息与库文件相匹配,避免因版本不一致而导致"cannot load incompatible bytecode"错误。
2. 利用命令行工具进行编译。在mac操作系统中,直接执行"make"即可完成编译;对于win用户,需先配置VSCommandPrompt,执行参数为"/k \"C:\\Program Files (x)\\Microsoft Visual Studio .0\\Common7\\Tools\\VsDevCmd.bat\"",然后进入jit源代码目录并运行"msvcbuild.bat"进行编译。
3. 使用"luajit -b"命令生成bytecode,此步骤生成的bytecode在runtime中通过interpreter模式运行。值得注意的是,jit bytecode生成后,行号钩子失效,可能影响基于行号的debug或profile操作,需要进行相应的调整。
考虑到不同平台对JIT模式的处理,ios系统默认关闭JIT,而android则需通过"jit.off()"进行手动关闭。在游戏开发中,对JIT模式的使用需谨慎考虑,以避免可能的性能损耗。
在禁用JIT模式后,游戏开发者可能会考虑使用luac而非jit的bytecode。然而,针对iOS禁用JIT、Android主动关闭JIT,以及可能面临其他平台不稳定情况,仍选择使用jit的bytecode具有以下优势:
1. 减少体积,提高包体、内存、转化率和热更文件大小的效率。相较于luac,jit的bytecode体积减少了约%。
2. 加速require代码时的load过程,性能提升达倍。在禁用JIT的环境下,性能特性与luac保持一致,无需对代码进行额外优化。CAD素材源码
LuaJIT源码分析(二)数据类型
LuaJIT,作为Lua的高性能版本,其源码分析中关于数据类型处理的细节颇值得研究。它在数据结构的定义上与Lua 5.1稍有不同,通过通用的数据结构TValue来表示各种Lua数据类型,但其复杂性体现在了内含的若干宏上,增加了理解的难度。这些宏如LJ_ALIGN、LJ_GC、LJ_ENDIAN_LOHI、LJ_FR2等,分别用于内存对齐、GC模式的选择、大小端判断以及浮点数编码格式的选择。
LJ_ALIGN宏用于确保struct内存对齐,以提高内存访问效率。LJ_GC宏在当前平台为位且无强制禁用的情况下生效,表明LuaJIT支持位GC(垃圾回收)模式。LJ_ENDIAN_LOHI宏则根据平台的字节顺序来确定结构的布局,而x平台采用小端序。
对于TValue结构的定义,通过处理宏后可以简化为一个位的结构体,包含一个union,用于统一表示Lua的各种数据类型。这种设计利用了NaN Boxing技术,即通过在浮点数编码中预留空间来实现不同类型数据的紧凑存储。每个类型通过4位的itype指针来标识,使得数据的解析与存储变得高效。
对于number数据类型,其值被存储在一个double中,而其他类型如nil、true、false等则利用剩余的空间来标识其类型。这种设计允许LuaJIT在内存中以一种紧凑且高效的方式存储各种数据类型,同时通过简单的位操作就能识别出具体的数据类型。
对于GC对象(如string、table等),LuaJIT通过特定的itype值来区分它们与普通数据类型,以及与值类型(如nil和bool)和轻量级用户数据的差异。通过宏判断,LuaJIT能够快速识别出TValue是否为GC对象,以及具体是哪种类型的GC对象。
在开启LJ_GC模式下,GC对象的地址被存储在TValue的特定字段gcr中,提供位的gtk十源码地址支持。虽然前位用于标识数据类型,但实际使用时仅利用了低位的地址空间,对于大多数实际应用而言,这部分内存已经绰绰有余。
在GCobj数据结构中,通过union的特性实现不同类型对象的共通性与特定性。GChead提供了通用的接口来获取对象的通用信息,而nextgc、marked等字段用于实现垃圾回收机制。通过gct字段,LuaJIT能够将一个GCObj转换为实际的类型对象,进一步增强了内存管理的灵活性。
对于整数类型,默认情况下LuaJIT使用double进行存储以确保精度,但在实际应用中,频繁使用的整数通过宏LJ_DUALNUM启用,以int类型存储,提高了数据处理的效率。此时,TValue的i字段用于保存int值,同时通过位移操作确保了数据的正确存储与解析。
Lua如何进行大数运算(附源码)
在游戏服务器开发中,大数计算是常见但难以避免的问题。一般数值计算在math.maxinteger范围内可直接使用Lua常规计算,超出范围则需大数计算。本文介绍了两种基于Lua的大数计算库:基于Boost的Lua库和基于GNU bc的Lua库lbc。
基于Boost的Lua库通过安装Lua、Boost和GCC,编译生成Lua直接引用的so库。编译方式有正常编译和捆绑编译。捆绑编译通过make_boost.sh脚本将boost文件复制到boost文件夹,简化编译过程。但需要注意,捆绑编译可能不适用于最新版本的boost。
基于GNU bc的Lua库lbc由Lua的作者之一编写,具有简单、小巧、易用等特点。编译简单,几乎只需执行make。测试结果显示,lbc在位字符的数字上,执行加减乘除各一次,其时间在1秒以下,符合要求。设计学院源码
本文还介绍了基于MAPM的Lua库lmapm,其特点与lbc类似。两种库在测试中表现稳定,但lbc提供了详细的位数信息,而lmapm采用科学计数法表示结果。
最后,本文建议根据实际需求选择合适的大数计算库。对于简单、方便、源码、可修改、可移植和精度要求较高的项目,lbc是不错的选择。同时,还介绍了其他开源的大数计算库,供读者参考。
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 虚拟机数据与逻辑的流向。
Unlua源码解析(附) 读源码的前置知识
在解析Unlua源码时,需要熟悉Lua的基本API和交互机制。以下为关键API及功能解析:
1. lua_getfield(L, k):获取指定表中由key k指向的值,压入栈顶。
2. lua_gettop(L):返回栈顶元素的索引,即栈的大小。
3. lua_rawget(L, -2):与lua_getfield类似,获取t[k]的值压入栈顶,但不调用元方法。
4. lua_rawset(L, -4):设置t[k] = v,同样不通过元方法。
5. lua_remove(L, -2):移除栈中index为-2的内容,之后所有元素下移。
6. Lua与C++交互机制:调用开始时,Lua参数依次压入栈;调用结束时,C++返回值压入栈,同时返回值数量。
在lua.h中,lua与C交互的API如下:
1.1 lua_register:将C函数设置为全局名称的新值,允许Lua端调用。
1.2 lua_gettop:返回栈顶元素的索引,用于获取栈大小。
1.3 lua_pop:弹出栈中指定数量的值。
1.4 lua_tolstring:将指定位置的值转换为C字符串,并返回字符串长度。
1.5 lua_tostring:与lua_tolstring类似,但返回长度为NULL。
1.6 lua_getfield:将表中key指向的值压入栈顶。
1.7 luaL_getmetatable:获取指定表的元表并入栈。
1.8 luaL_newmetatable:创建新元表并入栈,或重用已有。
1.9 lua_getmetatable:获取指定索引处的表的元表。
1. lua_pushstring:将字符串入栈,Lua会做拷贝。
1. lua_settable:设置表中key对应的值。
1. lua_rawset:与lua_settable类似,不调用元方法。
1. lua_gettable:从表中获取key对应的值。
1. lua_rawget:与lua_gettable类似,不调用元方法。
1. lua_pushinteger:将数字入栈。
1. lua_pushlightuserdata:将指针入栈。
1. lua_pushcclosure:创建闭包入栈。
1. lua_pushvalue:复制指定位置的值入栈。
1. lua_setmetatable:设置表元表。
1. lua_getglobal:获取全局变量并入栈。
1. lua_setglobal:设置全局变量值。
1. lua_pushnil:入栈nil值。
1. lua_upvalueindex:获取闭包中的upvalue。
1. lua_touserdata:返回完整 userdata 或 light userdata 指针。
1. lua_newtable:创建空表并入栈。
1. lua_createtable:预分配空间后创建空表。
1. lua_next:用于遍历表元素。
1. lua_tolstring:将指定位置的值转换为C字符串。
1. lua_tostring:与lua_tolstring类似,但不返回长度。
1. lua_newuserdata:分配内存并创建 userdata。
1. lua_call:调用Lua函数。
1. lua_pcall:与lua_call类似,用于调用Lua函数。
在Lua中,存在一些全局方法如rawset和rawget,用于直接写入或读取表元素而避免元方法的调用。
综上所述,通过掌握这些API,开发者能有效利用Lua与C++的交互机制,实现复杂、高效的数据处理和逻辑交互。
lua是怎样一门语言?
谈及Lua,其特性与优势不容忽视。首当其冲的是Lua的轻量级特性。
其源码包仅约多KB,二进制包也只有KB左右,携带丰富示例的完整语法描述打印成A4纸不到页。如此小巧的规模,彰显了Lua的轻量化优势,其简洁与效率令人赞叹。
在性能方面,Lua展现出了与传统解释型语言截然不同的表现。尤其是在游戏领域,Lua与JavaScript常常被相提并论。JavaScript虽能在浏览器中运行,但Lua在性能上占据优势,语言简洁且易于上手。更值得一提的是,LuaJIT技术可执行编译后的Lua字节码,使得运行速度更快。
Lua在编程范式上也别具一格,它并未提供类的语言一级支持,而是基于原型的语言。这使得在Lua中模拟面向对象变得异常简单,且易于实现。此外,Lua框架Lua-OOP的实现,支持多继承,性能卓越,适用于多线上游戏的开发,受到开发者的一致好评。
在数据结构方面,Table是Lua中的基石,它不仅是一般意义上的数组,更是哈希表的完美结合。Table能够实现数组与哈希表的功能,提供强大的数据管理能力。相较于PHP的Array,Lua的Table在表达能力与性能上表现更佳。
最后,Lua的注释采用两个减号开头,简洁明了。如果你对编程感兴趣,欢迎关注我的公众号dingshukai,一起探索编程的奥秘。
2024-12-01 00:14
2024-12-01 00:09
2024-11-30 23:10
2024-11-30 23:00
2024-11-30 22:35