Lua 简介
Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组并于 1993 年开发。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua 由标准 C 编写而成,几乎在所有操作系统和平台上都可以编译、运行。Lua 并没有提供强大的库,这是由它的定位决定的。所以 Lua 不适合作为开发独立应用程序的语言。Lua 有一个同时进行的 JIT 项目,提供在特定平台上的即时编译功能。
-
Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中可以被广泛应用。
-
不仅仅作为扩展脚本,也可以作为普通的配置文件,代替 XML、ini 等文件格式,并且更容易理解和维护。
-
标准 Lua 5.1 解释器由标准 C 编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译和运行;
-
一个完整的标准 Lua 5.1 解释器不足 200 KB。而本书推荐使用的 LuaJIT 2 的代码大小也只有不足 500 KB
-
同时也支持大部分常见的体系结构。在目前所有脚本语言引擎中,LuaJIT 2 实现的速度应该算是最快的之一。这一切都决定了 Lua 是作为嵌入式脚本的最佳选择。
Lua 语言的各个版本是不相兼容的。因此本书只介绍 Lua 5.1 语言,这是为标准 Lua 5.1 解释器和 LuaJIT 2 所共同支持的。LuaJIT 支持的对 Lua 5.1 向后兼容的 Lua 5.2 和 Lua 5.3 的特性,我们也会在方便的时候予以介绍。
Lua 环境搭建
Helloworld
基本数据类型
输出
Nil
Nil 是一种类型,Lua 将 nil 用于表示“无效值”。
-
一个变量在第一次赋值前的默认值是 nil,
-
将 nil 赋予给一个全局变量就等同于删除它。
Boolean (布尔)
布尔类型,可选值 true/false;
-
Lua 中 nil 和 false 为“假”
-
其它所有值均为“真”。比如 0 和空字符串就是“真”;
number(数字)
Number 类型用于表示实数,和 C/C++ 里面的 double 类型很类似。可以使用数学函数 math. Floor(向下取整)和 math. Ceil(向上取整)进行取整操作。
一般地,Lua 的 number 类型就是用双精度浮点数来实现的。值得一提的是,LuaJIT 支持所谓的“dual-number”(双数)模式,
- 即 LuaJIT 会根据上下文用整型来存储整数,而用双精度浮点数来存放浮点数。
String(字符串)
Lua 中有三种方式表示字符串:
-
使用一对匹配的单引号。例:‘hello’。
-
使用一对匹配的双引号。例:“abclua”。
-
-
我们把两个正的方括号(即[[)间插入 n 个等号定义为第 n 级正长括号。
-
0 级正的长括号写作 [[ ,一级正的长括号写作 [=[
-
反的长括号也作类似定义;举个例子,4 级反的长括号写作 ]====]
-
一个长字符串可以由任何一级的正的长括号开始,而由第一个碰到的同级反的长括号结束。整个词法分析过程将不受分行限制,不处理任何转义符,并且忽略掉任何不同级别的长括号
-
在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中
-
创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销),
-
内容相同的 Lua 字符串不会占用多份存储空间,
-
已经创建好的 Lua 字符串之间进行相等性比较时是
O(1)
时间度的开销,而不是通常见到的O(n)
.
Table (表)
Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,
- 索引通常是字符串(string)或者 number 类型,但也可以是除
nil
以外的任意类型的值
在内部实现上,table 通常实现为一个哈希表、一个数组、或者两者的混合。具体的实现为何种形式,动态依赖于具体的 table 的键分布特点。
Function (函数)
在 Lua 中,函数 也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值
表达式
算术运算符
算术运算符 | 说明 |
+ | 加法 |
- | 减法 |
* | 乘法 |
/ | 除法 |
^ | 指数 |
% | 取模 |
关系运算符
关系运算符 | 说明 |
< | 小于 |
> | 大于 |
⇐ | 小于等于 |
>= | 大于等于 |
== | 等于 |
~= | 不等于 |
- 在使用“==”做等于判断时,要注意对于 table, userdate 和函数, Lua 是作引用比较的。也就是说,只有当两个变量引用同一个对象时,才认为它们相等
-
Lua 字符串总是会被“内化”,即相同内容的字符串只会被保存一份,因此 Lua 字符串之间的相等性比较可以简化为其内部存储地址的比较。
-
这意味着 Lua 字符串的相等性比较总是为 O (1)
逻辑运算符
逻辑运算符 | 说明 |
and | 逻辑与 |
or | 逻辑或 |
not | 逻辑非 |
在 c 语言中,and 和 or 只得到两个值 1 和 0,其中 1 表示真,0 表示假。而 Lua 中 and 的执行过程是这样的:
-
a and b
如果 a 为 nil,则返回 a,否则返回 b; -
a or b
如果 a 为 nil,则返回 b,否则返回 a。 -
所有逻辑操作符将 false 和 nil 视作假,其他任何值视作真,对于 and 和 or,“短路求值”,对于 not,永远只返回 true 或者 false。
字符串连接
Lua 中连接两个字符串,可以使用操作符“..”(两个点)
-
如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串。
-
注意,连接操作符只会创建一个新字符串,而不会改变原操作数
-
也可以使用 string 库函数
string.format
连接字符串
于 Lua 字符串本质上是只读的,因此字符串连接运算符几乎总会创建一个新的(更大的)字符串。这意味着如果有很多这样的连接操作(比如在循环中使用 .. 来拼接最终结果),则性能损耗会非常大。在这种情况下,推荐使用 table 和 table.concat()
来进行很多字符串的拼接
上面的例子还可以使用 LuaJIT 独有的 table.new
来恰当地初始化 pieces
表的空间,以避免该表的动态生长。
优先级
f | |
---|---|
^ | |
not # - | |
* / % | |
+ - | |
.. | |
< > ⇐ >= == ~= | |
and | |
or |
控制结构
If-else
单个 if 分支型
两个分支 if-else 型
多个分支的 if-elseif-else
与 C 语言的不同之处是 else 与 if 是连在一起的,若将 else 与 if 写成 “else if” 则相当于在 else 里嵌套另一个 if 语句,如下代码:
While
Repeat
Lua 中的 repeat 控制结构类似于其他语言(如:C++ 语言)中的 do-while,但是控制方式是刚好相反的。简单点说,执行 repeat 循环体后,直到 until 的条件为真时才结束
For
for 数字型
-
Var 从 begin 变化到 finish,每次变化都以 step 作为步长递增 var
-
Begin、finish、step 三个表达式只会在循环开始时执行一次
-
第三个表达式 step 是可选的,默认为 1
-
控制变量 var 的作用域仅在 for 循环内,需要在外面控制,则需将值赋给一个新的变量
-
循环过程中不要改变控制变量的值,那样会带来不可预知的影响
For 泛型
泛型 for 循环通过一个迭代器(iterator)函数来遍历所有值:
Lua 的基础库提供了 ipairs,这是一个用于遍历数组的迭代器函数。在每次循环中,i 会被赋予一个索引值,同时 v 被赋予一个对应于该索引的数组元素值。
通过不同的迭代器,几乎可以遍历所有的东西,而且写出的代码极具可读性。标准库提供了几种迭代器,包括用于迭代文件中每行的(io. Lines)、迭代 table 元素的(pairs)、迭代数组元素的(ipairs)、迭代字符串中单词的(string. Gmatch)
泛型 for 循环与数字型 for 循环有两个相同点:
-
循环变量是循环体的局部变量;
-
决不应该对循环变量作任何赋值。
在 LuaJIT 2.1 中,ipairs()
内建函数是可以被 JIT 编译的,而 pairs()
则只能被解释执行。因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历
Break
语句 break
用来终止 while
、repeat
和 for
三种循环的执行,并跳出当前循环体,继续执行当前循环之后的语句
Return
return
主要用于从函数中返回结果,或者用于简单的结束一个函数的执行。
Goto
有了 goto
,我们可以实现 continue
的功能:
输出结果
函数
定义
上面的语法定义了一个全局函数,名为 function_name
. 全局函数本质上就是函数类型的值赋给了一个全局变量,即上面的语法等价于
由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local
修饰符:
定义函数
-
利用名字来解释函数、变量的目的,使人通过名字就能看出来函数、变量的作用。
-
每个函数的长度要尽量控制在一个屏幕内,一眼可以看明白。
-
让代码自己说话,不需要注释最好。
由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如
参数
按值传递
Lua 函数的参数大部分是按值传递的。当函数参数是 table 类型时,传递进来的是实际参数的引用
值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。
在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:
-
若实参个数大于形参个数,从左向右,多余的实参被忽略;
-
若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil
变长参数
其实 Lua 还支持变长参数。若形参为 ...
,表示该函数可以接收不同长度的参数。访问参数的时候也要使用 ...
具名参数
Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。
按引用传递
当函数参数是 table 类型时,传递进来的是实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效,而无需自己返回结果和让调用者进行赋值
函数返回值
Lua 具有一项与众不同的特性,允许函数返回多个值。
当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数调整规则:
-
若返回值个数大于接收变量的个数,多余的返回值会被忽略掉;
-
若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。
当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。
函数调用的实参列表也是一个列表表达式。考虑下面的例子:
如果你确保只取函数返回值的第一个值,可以使用括号运算符
值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。
全动态函数调用
调用回调函数,并把一个数组参数作为回调函数的参数。
模块
从 Lua 5.1 语言添加了对模块和包的支持。一个 Lua 模块的数据结构是用一个 Lua 值(通常是一个 Lua 表或者 Lua 函数)。一个 Lua 模块代码就是一个会返回这个 Lua 值的代码块
-
可以使用内建函数
require()
来加载和缓存模块。 -
简单的说,一个代码模块就是一个程序库,可以通过
require
来加载。模块加载后的结果通过是一个 Lua table -
这个表就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和变量。
require
函数会返回 Lua 模块加载后的结果,即用于表示该 Lua 模块的 Lua 值。
Lua 提供了一个名为 require
的函数用来加载模块。要加载一个模块,只需要简单地调用 require
“file” 就可以了,file 指模块所在的文件名。这个调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。
在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。相当于将导出的函数作为 table 的一个字段,在 Lua 中函数是第一类值,提供了天然的优势。
- 创建 my. Lua
- 把下面代码保存在文件 main. Lua 中,然后执行 main. Lua,调用上述模块。
对于需要导出给外部使用的公共模块,处于安全考虑,是要避免全局变量的出现。我们可以使用 lj-releng 或 luacheck 工具完成全局变量的检测。至于如何做,到后面再讲。
另一个要注意的是,由于在 LuaJIT 中,require 函数内不能进行上下文切换,所以不能够在模块的顶级上下文中调用 cosocket 一类的 API。否则会报
attempt to yield across C-call boundary
错误。
String
Lua 字符串总是由字节构成的。Lua 核心并不尝试理解具体的字符集编码(比如 GBK 和 UTF-8 这样的多字节字符编码)
Lua 字符串内部用来标识各个组成字节的下标是从 1 开始的,这不同于像 C 和 Perl 这样的编程语言。这样数字符串位置的时候再也不用调整,对于非专业的开发者来说可能也是一个好事情,string.Sub (str, 3, 7) 直接表示从第三个字符开始到第七个字符(含)为止的子串。
string.Byte (s [, i [, j ]])
返回字符 s[i]、s[i + 1]、s[i + 2]、······、s[j] 所对应的 ASCII 码
string. Char (…)
接收 0 个或更多的整数(整数范围:0~255),返回这些整数所对应的 ASCII 码字符组成的字符串。当参数为空时,默认是一个 0。
string.Upper (s)
接收一个字符串 s,返回一个把所有小写字母变成大写字母的字符串。
string.Lower (s)
接收一个字符串 s,返回一个把所有大写字母变成小写字母的字符串。
string.Len (s)
接收一个字符串,返回它的长度。
使用此函数是不推荐的。应当总是使用 #
运算符来获取 Lua 字符串的长度
string.Find (s, p [, init [, plain]])
在 s 字符串中第一次匹配 p 字符串。若匹配成功,则返回 p 字符串在 s 字符串中出现的开始位置和结束位置;若匹配失败,则返回 nil,
第三个参数第三个参数 init 默认为 1,并且可以为负整数,
当 init 为负数时,表示从 s 字符串的 string.Len (s) + init + 1 索引处开始向后匹配字符串 p 。
第四个参数默认为 false,当其为 true 时,只会把 p 看成一个字符串对待。
string.Format (formatstring, …)
按照格式化参数 formatstring,返回后面 ...
内容的格式化版本
string.Match (s, p [, init])
在字符串 s 中匹配(模式)字符串 p,若匹配成功,则返回目标字符串中与模式匹配的子串;否则返回 nil。第三个参数 init 默认为 1,并且可以为负整数,当 init 为负数时,表示从 s 字符串的 string.Len (s) + init + 1 索引处开始向后匹配字符串 p。
string.Gmatch (s, p)
返回一个迭代器函数,通过这个迭代器函数可以遍历到在字符串 s 中出现模式串 p 的所有地方。
string.Rep (s, n)
返回字符串 s 的 n 次拷贝。
string.Sub (s, i [, j])
返回字符串 s 中,索引 i 到索引 j 之间的子字符串。当 j 缺省时,默认为 -1,也就是字符串 s 的最后位置。I 可以为负数。当索引 i 在字符串 s 的位置在索引 j 的后面时,将返回一个空字符串。
string.Gsub (s, p, r [, n])
将目标字符串 s 中所有的子串 p 替换成字符串 r。可选参数 n,表示限制替换次数。返回值有两个,第一个是被替换后的字符串,第二个是替换了多少次。
string. Reverse (s)
接收一个字符串 s,返回这个字符串的反转
Table
下标从 1 开始
数组下标从 1 开始计数。
而 Lua 最初设计是一种类似 XML 的数据描述语言,所以索引(index)反应的是数据在里面的位置,而不是偏移量。
在初始化一个数组的时候,若不显式地用键值对方式赋值,则会默认用数字作为下标,从 1 开始。由于在 Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式
-
当我们把 table 当作栈或者队列使用的时候,容易犯错,追加到 table 的末尾用的是
s[#s+1] = something
****, 而不是s[#s] = something
-
而且如果这个 something 是一个 nil 的话**,会导致这一次压栈(或者入队列)没有存入任何东西**, s 的值没有变
-
如果
s = { 1, 2, 3, 4, 5, 6 }
,你令s[4] = nil
, s 会令你“匪夷所思”地变成 3。
table. Getn 获取长度
取长度操作符写作一元操作 。字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)
-
对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标
-
如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么 t 可能是指向任何一个是 nil 值的前一个位置的下标
-
这也就说明对于有“空洞”的情况,table 的长度存在一定的 不可确定性
我们使用 Lua 5.1 和 LuaJIT 2.1 分别执行这个用例,结果如下:
不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替。
table. Concat (table [, sep [, i [, j ] ] ])
对于元素是 string 或者 number 类型的表 table,返回 table[i]..sep..table[i+1] ··· sep..table[j]
连接成的字符串。填充字符串 sep 默认为空白字符串。起始索引位置 i 默认为 1,结束索引位置 j 默认是 table 的长度。
table. Insert (table, [pos ,] value)
在(数组型)表 table 的 pos 索引位置插入 value,其它元素向后移动到空的地方。Pos 的默认值是表的长度加一,即默认是插在表的最后
table. Maxn (table)
返回(数组型)表 table 的最大索引编号;如果此表没有正的索引编号,返回 0。
table. Remove (table [, pos])
在表 table 中删除索引为 pos(pos 只能是 number 型)的元素,并返回这个被删除的元素,它后面所有元素的索引值都会减一。Pos 的默认值是表的长度,即默认是删除表的最后一个元素。
table. Sort (table [, comp])
按照给定的比较函数 comp 给表 table 排序,也就是从 table[1] 到 table[n],这里 n 表示 table 的长度。比较函数有两个参数,如果希望第一个参数排在第二个的前面,就应该返回 true,否则返回 false。如果比较函数 comp 没有给出,默认从小到大排序。
其他
LuaJIT 2.1 新增加的 table.new
和 table.clear
函数是非常有用的。前者主要用来预分配 Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的
日期时间
函数 time、date 和 difftime 提供了所有的日期和时间功能。
在 OpenResty 的世界里,不推荐使用这里的标准时间函数,因为这些函数通常会引发不止一个昂贵的系统调用,同时无法为 LuaJIT JIT 编译,对性能造成较大影响。推荐使用 ngx_lua 模块提供的带缓存的时间接口,如 ngx.today
, ngx.time
, ngx.utctime
, ngx.localtime
, ngx.now
, ngx.http_time
,以及 ngx.cookie_time
等。
os. Time ([table])
如果不使用参数 table 调用 time 函数,
-
它会返回当前的时间和日期(它表示从某一时刻到现在的秒数)。
-
如果用 table 参数,它会返回一个数字,表示该 table 中所描述的日期和时间(它表示从某一时刻到 table 中描述日期和时间的秒数)。Table 的字段如下:
字段名称 | 取值范围 |
year | 四位数字 |
month | 1—12 |
day | 1—31 |
hour | 0—23 |
min | 0—59 |
sec | 0—61 |
isdst | boolean(true 表示夏令时) |
对于 time 函数,如果参数为 table,那么 table 中必须含有 year、month、day 字段。其他字缺省时段默认为中午(12:00:00)。
示例代码:(地点为北京)
os. Difftime (t 2, t 1)
返回 t 1 到 t 2 的时间差,单位为秒。
示例代码:
os. Date ([format [, time]])
把一个表示日期和时间的数值,转换成更高级的表现形式。
-
其第一个参数 format 是一个格式化字符串,描述了要返回的时间形式。
-
第二个参数 time 就是日期和时间的数字表示,缺省时默认为当前的时间。
-
使用格式字符 “*t”,创建一个时间表。
示例代码:
该表中除了使用到了 time 函数参数 table 的字段外,这还提供了星期(wday,星期天为 1)和一年中的第几天(yday,一月一日为 1)。除了使用 “*t” 格式字符串外,如果使用带标记(见下表)的特殊字符串,os. Date 函数会将相应的标记位以时间信息进行填充,得到一个包含时间的字符串。表如下:
格式字符 | 含义 |
%a | 一星期中天数的简写(例如:Wed) |
%A | 一星期中天数的全称(例如:Wednesday) |
%b | 月份的简写(例如:Sep) |
%B | 月份的全称(例如:September) |
%c | 日期和时间(例如:07/30/15 16:57:24) |
%d | 一个月中的第几天[01 ~ 31] |
%H | 24 小时制中的小时数[00 ~ 23] |
%I | 12 小时制中的小时数[01 ~ 12] |
%j | 一年中的第几天[001 ~ 366] |
%M | 分钟数[00 ~ 59] |
%m | 月份数[01 ~ 12] |
%p | “上午(am)”或“下午(pm)” |
%S | 秒数[00 ~ 59] |
%w | 一星期中的第几天[1 ~ 7 = 星期天 ~ 星期六] |
%x | 日期(例如:07/30/15) |
%X | 时间(例如:16:57:24) |
%y | 两位数的年份[00 ~ 99] |
%Y | 完整的年份(例如:2015) |
%% | 字符’%‘ |
示例代码:
数学库
Ua 数学库由一组标准的数学函数构成。数学库的引入丰富了 Lua 编程语言的功能,同时也方便了程序的编写。常用数学函数见下表:
asd | sdfa |
---|---|
函数名 | 函数功能 |
math.Rad (x) | 角度 x 转换成弧度 |
math.Deg (x) | 弧度 x 转换成角度 |
math.Max (x, …) | 返回参数中值最大的那个数,参数必须是 number 型 |
math.Min (x, …) | 返回参数中值最小的那个数,参数必须是 number 型 |
math. Random ([m [, n]]) | 不传入参数时,返回一个在区间[0,1)内均匀分布的伪随机实数;只使用一个整数参数 m 时,返回一个在区间[1, m]内均匀分布的伪随机整数;使用两个整数参数时,返回一个在区间[m, n]内均匀分布的伪随机整数 |
math. Randomseed (x) | 为伪随机数生成器设置一个种子 x,相同的种子将会生成相同的数字序列 |
math.Abs (x) | 返回 x 的绝对值 |
math.Fmod (x, y) | 返回 x 对 y 取余数 |
math.Pow (x, y) | 返回 x 的 y 次方 |
math.Sqrt (x) | 返回 x 的算术平方根 |
math.Exp (x) | 返回自然数 e 的 x 次方 |
math.Log (x) | 返回 x 的自然对数 |
math. Log 10 (x) | 返回以 10 为底,x 的对数 |
math.Floor (x) | 返回最大且不大于 x 的整数 |
math.Ceil (x) | 返回最小且不小于 x 的整数 |
math. Pi | 圆周率 |
math.Sin (x) | 求弧度 x 的正弦值 |
math.Cos (x) | 求弧度 x 的余弦值 |
math.Tan (x) | 求弧度 x 的正切值 |
math.Asin (x) | 求 x 的反正弦值 |
math.Acos (x) | 求 x 的反余弦值 |
math.Atan (x) | 求 x 的反正切值 |
使用 math.random()
函数获得伪随机数时,如果不使用 math.randomseed()
设置伪随机数生成种子或者设置相同的伪随机数生成种子,那么得得到的伪随机数序列是一样的。
稍等片刻,再次运行上面的代码。
两次运行的结果一样。为了避免每次程序启动时得到的都是相同的伪随机数序列,通常是使用当前时间作为种子。
修改上例中的代码:
稍等片刻,再次运行上面的代码。
文件
Lua I/O 库提供两种不同的方式处理文件:隐式文件描述,显式文件描述。
这些文件 I/O 操作,在 OpenResty 的上下文中对事件循环是会产生阻塞效应。OpenResty 比较擅长的是高并发网络处理,在这个环境中,任何文件的操作,都将阻塞其他并行执行的请求。实际中的应用,在 OpenResty 项目中应尽可能让网络处理部分、文件 I/0 操作部分相互独立,不要揉和在一起。
隐式文件描述
设置一个默认的输入或输出文件,然后在这个文件上进行所有的输入或输出操作。所有的操作函数由 io 表提供。
打开已经存在的
test1.txt
文件,并读取里面的内容
在
test1.txt
文件的最后添加一行 “hello world”
在相应目录下打开 test1.txt
文件,查看文件内容发生的变化。
显式文件描述
使用 file: XXX () 函数方式进行操作, 其中 file 为 io.Open () 返回的文件句柄。
打开已经存在的 test 2. Txt 文件,并读取里面的内容
在 test 2. Txt 文件的最后添加一行 “hello world”
在相应目录下打开 test2.txt
文件,查看文件内容发生的变化。
文件操作函数
io. Open (filename [, mode])
按指定的模式 mode,打开一个文件名为 filename
的文件,成功则返回文件句柄,失败则返回 nil 加错误信息。模式:
模式 | 含义 | 文件不存在时 |
”r” | 读模式 (默认) | 返回 nil 加错误信息 |
”w” | 写模式 | 创建文件 |
”a” | 添加模式 | 创建文件 |
”r+“ | 更新模式,保存之前的数据 | 返回 nil 加错误信息 |
”w+“ | 更新模式,清除之前的数据 | 创建文件 |
”a+“ | 添加更新模式,保存之前的数据, 在文件尾进行添加 | 创建文件 |
模式字符串后面可以有一个 ‘b’,用于在某些系统中打开二进制文件。
注意 “w” 和 “wb” 的区别
-
”w” 表示文本文件。某些文件系统 (如 Linux 的文件系统)认为 0 x 0 A 为文本文件的换行符,Windows 的文件系统认为 0 x 0 D 0 A 为文本文件的换行符。为了兼容其他文件系统(如从 Linux 拷贝来的文件),Windows 的文件系统在写文件时,会在文件中 0 x 0 A 的前面加上 0 x 0 D。使用 “w”,其属性要看所在的平台。
-
“wb” 表示二进制文件。文件系统会按纯粹的二进制格式进行写操作,因此也就不存在格式转换的问题。(Linux 文件系统下 “w” 和 “wb” 没有区别)
file: close ()
关闭文件。注意:当文件句柄被垃圾收集后,文件将自动关闭。句柄将变为一个不可预知的值。
io. Close ([file])
关闭文件,和 file: close () 的作用相同。没有参数 file 时,关闭默认输出文件。
file: flush ()
把写入缓冲区的所有数据写入到文件 file 中。
io. Flush ()
相当于 file: flush (),把写入缓冲区的所有数据写入到默认输出文件。
io. Input ([file])
当使用一个文件名调用时,打开这个文件(以文本模式),并设置文件句柄为默认输入文件;当使用一个文件句柄调用时,设置此文件句柄为默认输入文件;当不使用参数调用时,返回默认输入文件句柄。
file: lines ()
返回一个迭代函数, 每次调用将获得文件中的一行内容, 当到文件尾时,将返回 nil,但不关闭文件。
io. Lines ([filename])
打开指定的文件 filename 为读模式并返回一个迭代函数, 每次调用将获得文件中的一行内容, 当到文件尾时,将返回 nil,并自动关闭文件。若不带参数时 io.Lines () 等价于 io.Input (): lines () 读取默认输入设备的内容,结束时不关闭文件。
io. Output ([file])
类似于 io. Input,但操作在默认输出文件上。
file: read (…)
按指定的格式读取一个文件。按每个格式将返回一个字符串或数字, 如果不能正确读取将返回 nil,若没有指定格式将指默认按行方式进行读取。格式:
格式 | 含义 |
”*n” | 读取一个数字 |
”*a” | 从当前位置读取整个文件。若当前位置为文件尾,则返回空字符串 |
”*l” | 读取下一行的内容。若为文件尾,则返回 nil。(默认) |
number | 读取指定字节数的字符。若为文件尾,则返回 nil。如果 number 为 0, 则返回空字符串,若为文件尾, 则返回 nil |
io. Read (…)
相当于 io.Input ():read
io. Type (obj)
检测 obj 是否一个可用的文件句柄。如果 obj 是一个打开的文件句柄,则返回 “file” 如果 obj 是一个已关闭的文件句柄,则返回 “closed file” 如果 obj 不是一个文件句柄,则返回 nil。
file: write (…)
把每一个参数的值写入文件。参数必须为字符串或数字,若要输出其它值,则需通过 tostring 或 string. Format 进行转换。
io. Write (…)
相当于 io.Output (): write。
file: seek ([whence] [, offset])
设置和获取当前文件位置,成功则返回最终的文件位置 (按字节,相对于文件开头), 失败则返回 nil 加错误信息。缺省时,whence 默认为 “cur”,offset 默认为 0 。参数 whence:
whence | 含义 |
”set” | 文件开始 |
”cur” | 文件当前位置 (默认) |
“end” | 文件结束 |
file: setvbuf (mode [, size])
设置输出文件的缓冲模式。模式:
模式 | 含义 |
”no” | 没有缓冲,即直接输出 |
”full” | 全缓冲,即当缓冲满后才进行输出操作 (也可调用 flush 马上输出) |
“line” | 以行为单位,进行输出 |
最后两种模式,size 可以指定缓冲的大小(按字节),忽略 size 将自动调整为最佳的大小。
元表
元表 (metatable) 的表现行为类似于 C++ 语言中的操作符重载,例如我们可以重载 “__add” 元方法 (metamethod),来计算两个 Lua 数组的并集;或者重载 “__index” 方法,来定义我们自己的 Hash 函数。Lua 提供了两个十分重要的用来处理元表的方法
-
Setmetatable (table, metatable):此方法用于为一个表设置元表。
-
Getmetatable (table):此方法用于获取表的元表对象
设置元表
修改表的操作符行为
通过重载 “__add” 元方法来计算集合的并集实例
除了加法可以被重载之外,Lua 提供的所有操作符都可以被重载:
元方法 | 含义 |
---|---|
”__add | NAME ? |
”__sub | - 操作其行为类似于 “add” 操作 |
”__mul | * 操作其行为类似于 “add” 操作 |
”__div | / 操作其行为类似于 “add” 操作 |
”__mod | % 操作其行为类似于 “add” 操作 |
”__pow | ^ (幂)操作其行为类似于 “add” 操作 |
”__unm” | 一元 - 操作 |
”__concat” | .. (字符串连接)操作 |
”__len” | # 操作 |
”__eq” | == 操作函数 getcomphandler 定义了 Lua 怎样选择一个处理器来作比较操作仅在两个对象类型相同且有对应操作相同的元方法时才起效 |
”__lt” | < 操作 |
”__le” | ⇐ 操作 |
除了操作符之外,如下元方法也可以被重载,下面会依次解释使用方法:
元方法 | 含义 |
”__index” | 取下标操作用于访问 table[key] |
“__newindex” | 赋值给指定下标 table[key] = value |
”__tostring” | 转换成字符串 |
”__call” | 当 Lua 调用一个值时调用 |
”__mode” | 用于弱表 (week table) |
“__metatable” | 用于保护 metatable 不被访问 |
__index 元方法
关于 __index 元方法,有很多比较高阶的技巧,例如:__index 的元方法不需要非是一个函数,他也可以是一个表。
__tostring 元方法
与 Java 中的 toString () 函数类似,可以实现自定义的字符串转换。
__call 元方法
__call 元方法的功能类似于 C++ 中的仿函数,使得普通的表也可以被调用。
__metatable 元方法
假如我们想保护我们的对象使其使用者既看不到也不能修改 metatables。我们可以对 metatable 设置了 __metatable 的值,getmetatable 将返回这个域的值,而调用 setmetatable 将会出错:
面向对象
类
在 Lua 中,我们可以使用表和函数实现面向对象。将函数和相关的数据放置于同一个表中就形成了一个对象。
引用
上面这段代码 “setmetatable ({balance = balance}, mt)“,其中 mt 代表 { __index = _M }
,这句话值得注意。根据我们在元表这一章学到的知识,我们明白,setmetatable 将 _M
作为新建表的原型,所以在自己的表内找不到 ‘deposit’、‘withdraw’ 这些方法和变量的时候,便会到 __index 所指定的 _M 类型中去寻找。
继承
继承可以用元表实现,它提供了在父类中查找存在的方法和变量的机制。在 Lua 中是不推荐使用继承方式完成构造的,这样做引入的问题可能比解决的问题要多,下面一个是字符串操作类库,给大家演示一下。
成员私有性
在动态语言中引入成员私有性并没有太大的必要,反而会显著增加运行时的开销,毕竟这种检查无法像许多静态语言那样在编译期完成。下面的技巧把对象作为各方法的 upvalue,本身是很巧妙的,但会让子类继承变得困难,同时构造函数动态创建了函数,会导致构造函数无法被 JIT 编译。
在 Lua 中,成员的私有性,使用类似于函数闭包的形式来实现。在我们之前的银行账户的例子中,我们使用一个工厂方法来创建新的账户实例,通过工厂方法对外提供的闭包来暴露对外接口。而不想暴露在外的例如 balance 成员变量,则被很好的隐藏起来。
局部变量
Lua 的设计有一点很奇怪,在一个 block 中的变量,如果之前没有定义过,那么认为它是一个全局变量,而不是这个 block 的局部变量。这一点和别的语言不同。容易造成不小心覆盖了全局同名变量的错误。
定义
Lua 中的局部变量要用 local 关键字来显式定义,不使用 local 显式定义的变量就是全局变量
作用域
局部变量的生命周期是有限的,它的作用域仅限于声明它的块(block)。一个块是一个控制结构的执行体、或者是一个函数的执行体再或者是一个程序块(chunk)。
使用局部变量的好处
-
局部变量可以避免因为命名问题污染了全局环境
-
Local 变量的访问比全局变量更快
-
由于局部变量出了作用域之后生命周期结束,这样可以被垃圾回收器及时释放
检测模块的函数使用局部变量
Foo. Lua
Use_foo. Lua
因为 A 是全局变量,改变了 A 的值
Lua 上下文中应当严格避免使用自己定义的全局变量。可以使用一个 lj-releng 工具来扫描 Lua 代码,定位使用 Lua 全局变量的地方。Lj-releng 的相关链接:https://github.com/openresty/openresty-devel-utils/blob/master/lj-releng
Windows 用户把 lj-releng 文件所在的目录的绝对路径添加进 PATH 环境变量。然后进入你自己的 Lua 文件所在的工作目录,得到如下结果:
当然,更推荐采用 luacheck 来检查项目中全局变量,之后的“代码静态分析”一节,我们还会讲到如何使用 luacheck。
判断数组的大小
-
Table.Getn (t) 等价于 t 但计算的是数组元素,不包括 hash 键值。而且数组是以第一个 nil 元素来判断数组结束。
-
#
只计算 array 的元素个数,它实际上调用了对象的 metatable 的__len
函数。对于有__len
方法的函数返回函数返回值,不然就返回数组成员数目 -
Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。
-
Lua 数组中允许 nil 值的存在,但是数组默认结束标志却是 nil。这类比于 C 语言中的字符串,字符串中允许 ‘\0’ 存在,但当读到 ‘\0’ 时,就认为字符串已经结束了。
-
初始化是例外,在 Lua 相关源码中,初始化数组时首先判断数组的长度,若长度大于 0 ,并且最后一个值不为 nil,返回包括 nil 的长度;若最后一个值为 nil,则返回截至第一个非 nil 值的长度。
-
如果你要删除一个数组中的元素,请使用 remove 函数,而不是用 nil 赋值
我们分别使用 Lua 和 LuaJIT 来执行一下:
这一段的输出结果,就是这么 匪夷所思。不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替。
非空判断
有时候不小心引用了一个没有赋值的变量,这时它的值默认为 nil。如果对一个 nil 进行索引的话,会导致异常。
会报错
在实际的工程代码中,我们很难这么轻易地发现我们引用了 nil 变量。因此,在很多情况下我们在访问一些 table 型变量时,需要先判断该变量是否为 nil,例如将上面的代码改成
对于简单类型的变量,我们可以用 if (var == nil) then 这样的简单句子来判断。但是对于 table 型的 Lua 对象,就不能这么简单判断它是否为空了。一个 table 型变量的值可能是 {}
,这时它不等于 nil。我们来看下面这段代码:
输出
因此,我们要判断一个 table 是否为 {}
,不能采用 #table == 0
的方式来判断。可以用下面这样的方法来判断:
注意:next
指令是不能被 LuaJIT 的 JIT 编译优化,并且 LuaJIT 貌似没有明确计划支持这个指令优化,在不是必须的情况下,尽量少用。
正则表达式
同时存在两套正则表达式规范:Lua 语言的规范和 ngx. Re.*
的规范,即使您对 Lua 语言中的规范非常熟悉,我们仍不建议使用 Lua 中的正则表达式。
-
一是因为 Lua 中正则表达式的性能并不如
ngx. Re.*
中的正则表达式优秀; -
二是 Lua 中的正则表达式并不符合 POSIX 规范,而
ngx. Re.*
中实现的是标准的 POSIX 规范,后者明显更具备通用性。
ngx. Re.*
中的 o
选项,指明该参数,被编译的 Pattern 将会在工作进程中缓存,并且被当前工作进程的每次请求所共享。Pattern 缓存的上限值通过 lua_regex_cache_max_entries
来修改,它的默认值为 1024。
ngx. Re.*
中的 j
选项,指明该参数,如果使用的 PCRE 库支持 JIT,OpenResty 会在编译 Pattern 时启用 JIT。启用 JIT 后正则匹配会有明显的性能提升。较新的平台,自带的 PCRE 库均支持 JIT。如果系统自带的 PCRE 库不支持 JIT,出于性能考虑,最好自己编译一份 libpcre. So,然后在编译 OpenResty 时链接过去。要想验证当前 PCRE 库是否支持 JIT,可以这么做
-
编译 OpenResty 时在
./configure
中指定--with-debug
选项 -
在
error_log
指令中指定日志级别为debug
-
运行正则匹配代码,查看日志中是否有
pcre JIT compiling result: 1
即使运行在不支持 JIT 的 OpenResty 上,加上 j
选项也不会带来坏的影响。在 OpenResty 官方的 Lua 库中,正则匹配至少都会带上 jo
这两个选项。
Lua 正则简单汇总
Lua 中正则表达式语法上最大的区别,Lua 使用 ’%’ 来进行转义,而其他语言的正则表达式使用 ” 符号来进行转义。其次,Lua 中并不使用 ’?’ 来表示非贪婪匹配,而是定义了不同的字符来表示是否是贪婪匹配。定义如下:
符号 | 匹配次数 | 匹配模式 |
---|---|---|
+ | 匹配前一字符 1 次或多次 | 非贪婪 |
* | 匹配前一字符 0 次或多次 | 贪婪 |
- | 匹配前一字符 0 次或多次 | 非贪婪 |
? | 匹配前一字符 0 次或 1 次 | 仅用于此,不用于标识是否贪婪 |
符号 | 匹配模式 |
---|---|
. | 任意字符 |
%a | 字母 |
%c | 控制字符 |
%d | 数字 |
%l | 小写字母 |
%p | 标点字符 |
%s | 空白符 |
%u | 大写字母 |
%w | 字母和数字 |
%x | 十六进制数字 |
%z | 代表 0 的字符 |
虚变量
当一个方法返回多个值时,有些返回值有时候用不到,要是声明很多变量来一一接收,显然不太合适(不是不能)。Lua 提供了一个虚变量 (dummy variable)的概念,按照**惯例**以一个下划线(“_”)来命名,用它来表示丢弃不需要的数值,仅仅起到占位的作用。
返回值
迭代
输出
抵制使用 module () 定义模块
旧式的模块定义方式是通过 module ("filename"[, package. Seeall])*
来显式声明一个包,现在官方不推荐再使用这种方式
这种方式将会返回一个由 filename
模块函数组成的 table
,并且还会定义一个包含该 table
的全局变量。
-
package. Seeall
这种方式破坏了模块的高内聚,原本引入 “filename” 模块只想调用它的 foobar () 函数,但是它却可以读写全局属性,例如"filename. Os"
。 -
module
函数压栈操作引发的副作用,污染了全局环境变量。例如module ("filename")
会创建一个filename
的table
,并将这个table
注入全局环境变量中,这样使得没有引用它的文件也能调用filename
模块的方法。
推荐的模块定义
使用
另一个跟 Lua 的 module 模块相关需要注意的点是,当 lua_code_cache on 开启时,require 加载的模块是会被缓存下来的,这样我们的模块就会以最高效的方式运行,直到被显式地调用如下语句(这里有点像模块卸载):
调用函数前先定义函数
Lua 里面的函数必须放在调用的代码之前,下面的代码是一个常见的错误:
因此在函数定义之前使用函数相当于在变量赋值之前使用变量,Lua 世界对于没有赋值的变量,默认都是 nil,所以这里也就产生了一个 nil 的错误。
点号操作符和冒号操作符的区别
输出
-
冒号操作会带入一个
self
参数,用来代表自己
****。 -
而点号操作,只是
内容
的展开。
在函数定义时,使用冒号将默认接收一个 self
参数,而使用点号则需要显式传入 self
参数
示例代码:
等价于
Module 的缺点
由于 lua_code_cache off
情况下,缓存的代码会伴随请求完结而释放。Module 的最大好处缓存这时候是无法发挥的,所以本章的内容都是基于 lua_code_cache on
的情况下。
先看看下面代码:
-
对于比较底层的模块,内部使用到的非本地函数,都需要 local 本地化,这样做的好处:
-
避免命名冲突:防止外部是
require (...)
的方法调用造成全局变量污染 -
访问局部变量的速度比全局变量更快、更快、更快(重要事情说三遍)
-
-
每个基础模块最好有自己
_VERSION
标识,方便后期利用_VERSION
完成热代码部署等高级特性,也便于使用者对版本有整体意识。 -
其实
_M
和mt
对于不同的请求实例(require 方法得到的对象)是相同的,因为 module 会被缓存到全局环境中。所以在这个位置千万不要放单请求内个性信息,例如 ngx. Ctx 等变量。 -
这里需要实现的是给每个实例绑定不同的 tcp 对象,后面 setmetatable 确保了每个实例拥有自己的 socket 对象,所以必须放在 new 函数中。如果放在 ③ 的下面,那么这时候所有的不同实例内部将绑定了同一个 socket 对象。
-
Lua 的 module 有两种类型:
-
支持面向对象痕迹可以保留私有属性;静态方法提供者,没有任何私有属性。
-
真正起到区别作用的就是 setmetatable 函数,是否有自己的个性元表,最终导致两种不同的形态。
-
FFI
https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/FFI.html
FFI 库,是 LuaJIT 中最重要的一个扩展库。它允许从纯 Lua 代码调用外部 C 函数,使用 C 数据结构。
FFI 库最大限度的省去了使用 C 手工编写繁重的 Lua/C
绑定的需要。不需要学习一门独立/额外的绑定语言——它解析普通 C 声明。这样可以从 C 头文件或参考手册中,直接剪切,粘贴。它的任务就是绑定很大的库,但不需要捣鼓脆弱的绑定生成器。
FFI 紧紧的整合进了 LuaJIT(几乎不可能作为一个独立的模块)。JIT
编译器在 C 数据结构上所产生的代码,等同于一个 C 编译器应该生产的代码。在 JIT
编译过的代码中,调用 C 函数,可以被内连处理,不同于基于 Lua/C API
函数调用。
ffi 库词汇
noun | Explanation |
cdecl | A definition of an abstract C type (actually, is a lua string) |
ctype | C type object |
cdata | C data object |
ct | C type format, is a template object, may be cdecl, cdata, ctype |
cb | callback object |
VLA | An array of variable length |
VLS | A structure of variable length |
ffi. API*
功能: Lua ffi 库的 API,与 LuaJIT 不可分割。
毫无疑问,在 lua
文件中使用 ffi
库的时候,必须要有下面的一行。
JIT
看一下 LuaJIT 官方的解释:LuaJIT is a Just-In-Time Compilerfor the Lua programming language。
LuaJIT 的运行时环境包括一个用手写汇编实现的 Lua 解释器和一个可以直接生成机器代码的 JIT 编译器
-
一开始的时候,Lua 字节码总是被 LuaJIT 的解释器解释执行。LuaJIT 的解释器会在执行字节码时同时记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。
-
当这些次数超过某个预设的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够的“热”,这时便会触发 JIT 编译器开始工作。
-
JIT 编译器会从热函数的入口或者热循环的某个位置开始尝试编译对应的 Lua 代码路径。编译的过程是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),然后再生成针对目标体系结构的机器码(比如 x 86_64 指令组成的机器码)
-
如果当前 Lua 代码路径上的所有的操作都可以被 JIT 编译器顺利编译,则这条编译过的代码路径便被称为一个“trace”,在物理上对应一个
trace
类型的 GC 对象(即参与 Lua GC 的对象)。
JIT 编译器不支持的原语被称为 NYI(Not Yet Implemented)原语。比较完整的 NYI 列表在这篇文档里面:
所谓“让更多的 Lua 代码被 JIT 编译”,其实就是帮助更多的 Lua 代码路径能为 JIT 编译器所接受。这一般通过两种途径来实现:
-
调整对应的 Lua 代码,避免使用 NYI 原语。
-
增强 JIT 编译器,让越来越多的 NYI 原语能够被编译。
可以被 JIT 编译的元操作
下面给大家列一下截止到目前已经可以被 JIT 编译的元操作。其他还有 IO、Bit、FFI、Coroutine、OS、Package、Debug、JIT 等分类,使用频率相对较低,这里就不罗列了,可以参考官网:http://wiki.luajit.org/NYI。
基础库的支持情况
函数 | 编译? | 备注 |
assert | yes | |
collectgarbage | no | |
dofile | never | |
error | never | |
getfenv | 2.1 partial | 只有 getfenv (0) 能编译 |
getmetatable | yes | |
ipairs | yes | |
load | never | |
loadfile | never | |
loadstring | never | |
next | no | |
pairs | no | |
pcall | yes | |
no | ||
rawequal | yes | |
rawget | yes | |
rawlen (5.2) | yes | |
rawset | yes | |
select | partial | 第一个参数是静态变量的时候可以编译 |
setfenv | no | |
setmetatable | yes | |
tonumber | partial | 不能编译非 10 进制,非预期的异常输入 |
tostring | partial | 只能编译:字符串、数字、布尔、nil 以及支持 __tostring 元方法的类型 |
type | yes | |
unpack | no | |
xpcall | yes |
字符串库
函数 | 编译? | 备注 |
string. Byte | yes | |
string. Char | 2.1 | |
string. Dump | never | |
string. Find | 2.1 partial | 只有字符串样式查找(没有样式) |
string. Format | 2.1 partial | 不支持 %p 或非字符串参数的 %s |
string. Gmatch | no | |
string. Gsub | no | |
string. Len | yes | |
string. Lower | 2.1 | |
string. Match | no | |
string. Rep | 2.1 | |
string. Reverse | 2.1 | |
string. Sub | yes | |
string. Upper | 2.1 |
表
函数 | 编译? | 备注 |
table. Concat | 2.1 | |
table. Foreach | no | 2.1: 内部编译,但还没有外放 |
table. Foreachi | 2.1 | |
table. Getn | yes | |
table. Insert | partial | 只有 push 操作 |
table. Maxn | no | |
table. Pack (5.2) | no | |
table. Remove | 2.1 | 部分,只有 pop 操作 |
table. Sort | no | |
table. Unpack (5.2) | no |
math 库
函数 | 编译? | 备注 |
math. Abs | yes | |
math. Acos | yes | |
math. Asin | yes | |
math. Atan | yes | |
math. Atan 2 | yes | |
math. Ceil | yes | |
math. Cos | yes | |
math. Cosh | yes | |
math. Deg | yes | |
math. Exp | yes | |
math. Floor | yes | |
math. Fmod | no | |
math. Frexp | no | |
math. Ldexp | yes | |
math. Log | yes | |
math. Log 10 | yes | |
math. Max | yes | |
math. Min | yes | |
math. Modf | yes | |
math. Pow | yes | |
math. Rad | yes | |
math. Random | yes | |
math. Randomseed | no | |
math. Sin | yes | |
math. Sinh | yes | |
math. Sqrt | yes | |
math. Tan | yes | |
math. Tanh | yes |