lua 去除小数点有效数字后面的0_如何实现Lua虚拟机

准确来说,这是一篇十分粗糙甚至有些跑题的阅读笔记(原书手机端1143页)源自张秀宏编写的《自己动手实现Lua:虚拟机、编译器和标准库》。有兴趣的可以自行阅读

一、二进制chunk

Lua是一门以高效率著称的脚本语言,为了达到较高的执行效率,Lua从1.0版本就开始内置了虚拟机,也就是说,Lua脚本并不是直接被Lua解释器解释执行,而是类似Java语言那样,先由Lua编译器编译为字节码,然后再交给Lua虚拟机执行。Lua字节码需要一个载体,这个载体就是二进制chunk.

1.什么是二进制chunk

在Lua的行话里,一段可以被Lua解释器解释执行的代码就叫作chunk,Lua并不是直接解释执行chunk,而是先由编译器编译成内部结构(其中包含字节码等信息),然后由虚拟机执行字节码。这种内部结构在Lua中就叫作预编译chunk,由于采用了二进制格式,所以也叫作二进制chunk。

2.Luac命令介绍

Luac命令主要有两个用途:第一,作为编译器,把Lua源文件编译成二进制chunk文件;第二,作为反编译器,分析二进制chunk,将信息输出到控制台。

2.二进制chunk格式

(1)Lua二进制本质上也是一个字节流。二进制chunk格式没有考虑跨平台的需求,Lua官方实现的做法比较简单:编译Lua脚本时,直接按照本机的大小端方式生成二进制chunk文件,当加载二进制chunk文件时,会探测被加载二进制文件的大小端方式,如果和本机不匹配,就拒绝加载。

(2)编译Lua脚本时,直接按照当时的Lua版本生成二进制chunk文件,当加载二进制chunk文件时,会检测被加载文件的版本号,如果和当前Lua版本不匹配,则拒绝加载。

(3)把Lua脚本预编译成二进制chunk的主要目的是为了获得更快的加载速度。

3.数据类型

在讨论二进制chunk格式时,我们称被编码为一个或多个字节的信息单位为数据类型,二进制chunk内部使用的数据类型大致可以分为数字、字符串和列表三种。

(1)数字类型主要包括字节、C语言整型,C语言size_t类型,Lua整数、Lua浮点数五种。

(2)字符串在二进制chunk里,其实就是一个字节数组。因为字符串长度是不固定的,所以需要把字节数组的长度也记录到二进制chunk里。

(3)在二进制chunk内部,指令表、常量表、子函数原型表等信息都是按照列表的方式存储的。

4.头部

头部总共占有约30个字节(因平台而异),其中包含签名、版本号、格式号、各种整型类型占用的字节数,以及大小端和浮点数格式识别信息。

(1)签名:Lua二进制chuk的签名是四个字节,分别是ESC、L、u、a的ASCII码 。用十六进制表示为0X1B4C7561,签名主要用来快速识别文件格式。

(2)版本号:签名之后的一个字节,记录二进制chunk文件所对应的Lua版本号。Lua语言的版本号由三个部分构成:比如Lua的当前版本5.3.4,其中大版本好是5,小版本号是3,发布号是4.

(3)格式号:版本号之后的一个字节记录二进制chunk格式号。Lua虚拟机在加载二进制chunk时,会检查其格式号,如果和虚拟机本身的格式号不匹配,就拒绝加载该文件。Lua官方实现使用的格式号是0。

(4)LUAC_DATA

格式号之后的6个字节在Lua官方实现里叫作LUAC_DATA.其中前两个字节是0x1993,这是Lua1.0发布的年份。这个留个字节主要起进一步校验的作用。

(5)整数和Lua虚拟机指令宽度

接下来的5个字节分别记录cint、size_t、Lua虚拟机指令、Lua整数和Lua浮点数这5种数据类型在二进制chunk里占用的字节数。

(6)LUAC_INT

接下来的n个字节存放Lua整数0x5678。

(7)LUAC_NUM

头部的最后n个字节存放Lua浮点数370.5

ea2683e90680b90a14a6bf3b97ea5b76.png

5.函数原型

函数原型主要包含函数基本信息、指令表、常量表、upvalue表、子函数原型表以及调试信息。

6.解析二进制chunk

(1)读取基本数据类型

(2)检查头部

(3)读取函数原型

总结:Lua虽然是解释型脚本语言,但Lua解释器的内部执行方式

二、指令集

1.指令集介绍

高级编程语言虚拟机是对真实计算机的模拟和抽象。按照实现方式,虚拟机大致可以分为两类:基于栈和基于寄存器的,本书讨论的Lua虚拟机是基于寄存器的虚拟机,如同真实机器有一套自己的指令集一样,虚拟机也有自己的指令集:基于栈的虚拟机需要使用PUSH类指令往栈顶推入值,使用POP类指令从栈顶弹出值,其他指令则是对栈顶值进行操作,因此指令集相对比较大,但是指令的平均长度比较短;基于寄存器的虚拟机由于可以直接对寄存器进行寻址,所以不需要PUSH或者POP类指令,指令集相对比较小,但是由于需要把寄存器地址编码进指令里,所以指令的平均长度比较长。

2.指令编码格式

(1)编码模式(每条Lua虚拟机指令占用4个字节,共32个比特,其中低6个比特用于操作码,高26位个比特用于操作码。按照高26个比特的分配(以及解释)方式,Lua虚拟机指令可以分为四类,分别对应四种便秘吗模式(Mode):iABC,iABx,iAsBx,iAx.)

(2)操作码用于识别指令。

(3)操作数是指令的参数,每条指令(因编码模式而异)可以携带1到3个操作数。

3.指令表

为了便于在代码中使用,Lua官方实现把每一条指令的基本信息(包括编码模式、是否设置寄存器A、操作数B和C的使用类型等)都编码成一个字节。

4.指令解码

我们使用unit32类型来表示存储在二进制chunk里的指令,为了便于操作。

总结:Lua使用了基于寄存器的虚拟机,并且采用了定长指令集。

三、Lua API

1.LuaAPI介绍

Lua将自己定位为一门强大、高效、轻量级的可嵌入脚本语言。为了很方便地嵌入到其他宿主环境中,Lua核心是以库的形式被实现的,其他应用程序只需要链接Lua库就可以使用Lua提供的API轻松获得脚本执行能力。作为例子,Lua发布版包含的两个命令行程序,也就是我们已经很熟悉的lua和luac,实际上就是Lua库的两个特殊的宿主程序。

为了便于移植,官方Lua使用CleanC(其语法是C和C++语言的子集)编写,LuaAPI主要是指一系列以“lua_”开头的C语言函数。最开始Lua解释器的状态是完全隐藏在API后面的,散落在各种全局变量里。由于某些宿主环境(比如Web服务器)需要同时使用多个Lua解释器实例。

2.Lua栈

Lua State是Lua API非常核心的概念,全部的API函数都是围绕Lua State进行操作,而Lua State内部封装的最为基础的一个状态就是虚拟栈(后面我们称为Lua栈)。Lua栈是宿主语言(对于官方Lua来说是C语言)和Lua语言进行狗头的桥梁,LuaAPI函数有很大一部分是专门用来操作Lua栈的。

四、Lua运算符

Lua语言层面一共有25个运算符,按类别可以分为算术运算符、按位运算符、比较运算符、长度运算符和字符串拼接运算符。

五、虚拟机雏形

虚拟机的核心任务就是执行指令,到目前为止,我们已经能够解析二进制chunk,并且可以解码Lua虚拟接指令。我们还实现了一部分LuaAPI函数,可以进行基本的栈操作和各种运算。在这些基础上,本章将实现一台我们期待已久的Lua虚拟机,而Lua栈会在上面扮演一个十分重要的角色:虚拟寄存器。

1.添加LuaVM接口

Lua解释器在执行一段Lua脚本之前,会先把它包在一个主函数里编译成Lua虚拟机指令序列,然后连同其他信息一起,打包成一个二进制chunk,然后Lua虚拟机会接管二进制chunk,执行里面的指令。和真实的机器一样,Lua虚拟机也需要使用程序计数器。要 想完整实现Lua虚拟指令集,仅仅依赖Lua API还不够。比如说LOADK指令就需要查看二进制chunk常量表,从中取出某个常量,放到指定寄存器中。但LuaAPI并没有将常量表这种函数内部细节暴露给用户,所以也无法通过API方法来操作常量表。由于我们不想给LuaAPI私自添加任何方法,所以引入一个新的LuaVM接口,让其扩展现有的LuaState接口,然后添加几个必要的方法以满足指令实现函数的需要。

2.实现Lua虚拟机指令

Lua虚拟机指令集一共定义了47条指令。其中EXTRAARG指定实际上只能用来扩展其他指令的操作数,并不能单独执行,所以真正的指令只有46条。

(1)移动和跳转指令:MOVE指令把源寄存器里的值移动到目标寄存器,JMP执行无条件跳转

(2)加载指令:用于nil值、布尔值或者常量表里的常量值加载到寄存器里。

(3)Lua语言里的8个算术运算符和6个按位运算符分别和Lua虚拟指令集里的14条指令一一对应。

(4)LEN指令进行的操作和一元算术运算指令类似。

(5)比较指令,比较寄存器或常量表里的两个值(索引分别由操作数B和C指定),如果比较结果和操作A匹配,则跳过下一条指令。

(6)逻辑运算指令:逻辑运算指令对应Lua语言里的逻辑运算符。

(7)for循环指令:Lua的for循环语句有两种形式(数值形式和通用形式)。

六、表

除了布尔、数字、字符串等基本数据类型外,大部分编程语言都会内置数组、列表、哈希表等多种数据结构。Lua与众不同,其只提供了唯一一种数据结构(表)。Lua表非常强大,不仅可以直接当成数组和列表使用,也可以用它来实现其他各种数据结构。此外,表对于Lua语言本身来讲也非常重要,比如全局变量、元编程、包、和模块机制等,全部都要依赖表。

Lua表本质上是关联数组,里面存放的是两两关联的键值对。Lua语言提供了表构造器表达式,极大方便了表的创建。由于Lua也常被当成数据描述语言来使用。Lua表的构造器语法和JSON语法非常类似,但是更为复杂一些,功能也更强大。

七、函数调用

函数的定义和调用是任何编程语言都必须具备的能力,否则就只能把逻辑全部写在一起。Lua是动态脚本语言,所以函数的调用规则非常灵活。

1.函数调用栈

为了区别于Lua栈,我们称其为函数调用栈( Call Stack) .Lua栈里面存放的是Lua值,调用栈里存放的则是调用栈帧,简称为调用帧(Call Frame)。当我们调用一个函数时,要先往调用栈里推入一个调用帧,然后把参数传递给调用帧。函数依托调用帧执行指令,可能会调用其他函数,以此类推。当函数执行完毕之后,调用帧里会留下函数需要返回的值。我们把调用帧从调用栈顶弹出,并且把返回值返回给底部的调用帧,这样一个函数调用就结束了。

2.Load

Load()方法加载二进制chunk,把主函数原型实例化为闭包并推入栈顶。

3.Call()

Call()方法对Lua函数进行调用。在执行Call()方法之前,必须先把被调函数推入栈顶,然后把参数值依次推入栈顶。Call()方法结束之后,参数值和函数会被弹出栈顶,取而代之的是指定数量的返回值。

八、Lua注册表

Lua给用户提供了一个注册表,这个注册表实际上就是一个普通的Lua表,所以用户可以在里面放任何Lua值。有趣的是,这个注册表虽然是给用户准备的,但Lua本身也用到了它,比如说Lua全局变量就是借助这个注册表实现的。

九、闭包和Upvalue

闭包是计算机编程语言里非常普遍的一个概念,不过Upvalue却是Lua语言独有的。所谓闭包就是按词法作用域捕获了非局部变量的嵌套函数,实际上Upvalue就是闭包内部捕获的非局部变量。

在Lua里,函数属于一等公民。Lua函数实际上全部是匿名的,函数定义语句只不过是函数定义表达式和赋值语句的语法糖而已。此外,Lua函数本质上是按词法作用域捕获了非局部变量的闭包。

十、元编程

所谓元程序,是指能够处理程序的程序。这里的“处理”包括读取、生成、分析、转换等。而元编程就是指编写元程序的编程技术。元编程有很多种形式,比如C语言的宏和C++的模板可以在编译期生成代码。

1.元表

在Lua中,每个值都可以有一个元表,每个值都有一个元表。如果值的类型是表或者用户类型,则可以拥有自己“专属”的元表,其他类型的值则是每种类型共享一个元表,新创建的表默认没有元表,nil,布尔,数字,函数类型默认也没有元表,不过String标准库给字符串类型设置了元表。

2.元方法

真正让元表变得与众不同的是元方法。比如当我们对两个表进行加法运算,Lua会看这两个表是否有元表;如果有,这进一步看元表里是否有__add元方法;如果有,则将两个表作为参数调用这个方法,将返回结构作为加法运算的结果。

3.支持元表

如前所述,每一个表都可以拥有自己的元表,其他值则是每种类型共享一个元表,所以我们要做的第一件事就是把值和元表关联起来。对应于表来说,很自然的做法就是给底层的结构体添加一个字段,用来存放元表,对于其他类型的值,我们可以把元表存放在注册表里。

十一、迭代器

迭代器模式是一种经典的设计模式,在这种模式里,我们会使用迭代器对集合或者容器里的元素进行变量。

十二、异常和错误处理

Lua语言并没有在语法层面直接支持异常处理,但是izai标准库中提供了一些函数,可以用来抛出或者捕获异常。

1.error()函数:抛出异常,异常抛出之后,正常的函数执行终止,然后异常逐步向外传播,直到被pcall()函数捕获位置。

十三、编译器介绍

从广义上讲,任何一种可以将编程语言(源语言)转换为另外一种编程语言(目标语言)的程序都可以称为编译器。不过我们通常所说的编译器,一般特指高级语言编译器。其源语言是C、C++、Java或者Lua这样的高级编程语言,目标语言则是机器语言或者虚拟机字节码这样的低级语言。其他类型的编译器通常有自己特定的名称,比如将低级语言翻译为高级语言的编译器称为反编译器。

编写编译器并不是一件很轻松的事情,为了降低编写难度,在编写编译器时往往会把编译过程分为不同的阶段,每个阶段单独编写,各个阶段通过输入和输出串联起来最终形成完整的编译器。主要的编译阶段包括预处理、词法分析、语法分析、语义分析、中间代码生成、中间代码优化、目标代码生成等。

为了尽可能重复利用编译器代码,上述编译阶段又可以进一步归为三个大的阶段:前端(Front End)、中端(Middle End)和后端(Back End)。预处理、词法分析、语法分析、语义分析和中间代码生成属于前端;中间代码优化属于中端;目标代码生成属于后端。这样就可以为不同的语言编写前端编译器从而共用后端编译器以节约工作量,或者为同一种语言编写不同的后端编译器从而达到跨平台的目的,如图14-1所示(图片来自Wikipedia)。

Lua语言比较简单,不支持宏等特性,不需要进行预处理。语义分析最重要的一项工作是类型检查。由于Lua是动态类型语言,在编译期不需要进行类型检查,所以我们也不讨论语义分析阶段。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值