Lua函数
在 Lua 中,函数是一种对语句和表达式进行抽象的主要机制。函数既可以完成某项特定的任务,也可以只做一些计算并返回结果。在第一种情况中,一句函数调用被视为一条语句;而在第二种情况中,则将其视为一句表达式。 示例代码:
print ( "hello world!" )
local m = math. max ( 1 , 5 )
使用函数的好处:
降低程序的复杂性:把函数作为一个独立的模块,写完函数后,只关心它的功能,而不再考虑函数里面的细节。 增加程序的可读性:当我们调用 math.max()
函数时,很明显函数是用于求最大值的,实现细节就不关心了。 避免重复代码:当程序中有相同的代码部分时,可以把这部分写成一个函数,通过调用函数来实现这部分代码的功能,节约空间,减少代码长度。 隐含局部变量:在函数中使用局部变量,变量的作用范围不会超出函数,这样它就不会给外界带来干扰。
函数的定义
Lua 使用关键字 function 定义函数,语法如下:
function function_name ( arc)
end
上面的语法定义了一个全局函数,名为 function_name
。全局函数本质上就是函数类型的值赋给了一个全局变量,即上面的语法等价于:
function_name = function ( arc)
end
由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local
修饰符:
local function function_name ( arc)
end
由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。 示例代码:
local function max ( a, b)
local temp = nil
if ( a > b) then
temp = a
else
temp = b
end
return temp
end
local m = max ( - 12 , 20 )
print ( m)
如果参数列表为空,必须使用 ()
表明是函数调用。示例代码:
local function func ( )
print ( "no parameter" )
end
func ( )
no parameter
在定义函数时要注意几点:
利用名字来解释函数、变量的目的,使人通过名字就能看出来函数、变量的作用。 每个函数的长度要尽量控制在一个屏幕内,一眼可以看明白。 让代码自己说话,不需要注释最好。 由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如:
function foo. bar ( a, b, c)
end
此时我们是把一个函数类型的值赋给了 foo
表的 bar
字段。换言之,上面的定义等价于:
foo. bar = function ( a, b, c)
print ( a, b, c)
end
对于此种形式的函数定义,不能再使用 local
修饰符了,因为不存在定义新的局部变量了。
函数的参数
按值传递
Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。 示例代码:
local function swap ( a, b)
local temp = a
a = b
b = temp
print ( a, b)
end
local x = "hello"
local y = 20
print ( x, y)
print ( swap ( x, y) )
print ( x, y)
hello 20
20 hello
hello 20
在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则是:若实参个数大于形参个数,从左向右,多余的实参被忽略;若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。 示例代码:
local function fun1 ( a, b)
print ( a, b)
end
local function fun2 ( a, b, c, d)
print ( a, b, c, d)
end
local x = 1
local y = 2
local z = 3
fun1 ( x, y, z)
fun2 ( x, y, z)
1 2
1 2 3 nil
变长参数
上面函数的参数都是固定的,其实 Lua 还支持变长参数。若形参为 ...
,表示该函数可以接收不同长度的参数。访问参数的时候也要使用 ...
。 示例代码:
local function func ( ... )
local temp = { ... }
local ans = table. concat ( temp, " " )
print ( ans)
end
func ( 1 , 2 )
func ( 1 , 2 , 3 , 4 )
1 2
1 2 3 4
值得一提的是,LuaJIT 2 尚不能 JIT 编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式。
具名参数
Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。 示例代码:
local function change ( arg)
arg. width = arg. width * 2
arg. height = arg. height * 2
return arg
end
local rectangle = { width = 20 , height = 15 }
print ( "before change:" , "width =" , rectangle. width, "height = " , rectangle. height)
rectangle = change ( rectangle)
print ( "after change:" , "width =" , rectangle. width, "height = " , rectangle. height)
before change: width = 20 height = 15
after change: width = 40 height = 30
按引用传递
当函数参数是 table 类型时,传递进来的是实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效,而无需自己返回结果和让调用者进行赋值。 我们把上面改变长方形长和宽的例子修改一下。 示例代码:
function change ( arg)
arg. width = arg. width * 2
arg. height = arg. height * 2
end
local rectangle = { width = 20 , height = 15 }
print ( "before change:" , "width = " , rectangle. width, "height = " , rectangle. height)
change ( rectangle)
print ( "after change:" , "width = " , rectangle. width, "height =" , rectangle. height)
在常用基本类型中,除了 table 是按址传递类型外,其它的都是按值传递参数。 用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。
函数的返回值
Lua 具有一项与众不同的特性,允许函数返回多个值。Lua 的库函数中,有一些就是返回多个值。 示例代码:使用库函数 string.find
,在源字符串中查找目标字符串,若查找成功,则返回目标字符串在源字符串中的起始位置和结束位置的下标。
local s, e = string. find ( "hello, world" , "llo" )
print ( s, e)
local function swap ( a, b)
return b, a
end
local x = 1
local y = 20
x, y = swap ( x, y)
print ( x, y)
当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。调整规则: 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉; 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。 示例代码:
function init ( )
return 1 , "lua"
end
x = init ( )
print ( x)
x, y, z = init ( )
print ( x, y, z)
1
1 lua nil
当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值,也就是第一个返回值。
local x, y, z = init ( ) , 2
print ( x, y, z)
local a, b, c = 2 , init ( )
print ( a, b, c)
函数调用的实参列表也是一个列表表达式。考虑下面的例子:
print ( init ( ) , 2 )
print ( 2 , init ( ) )
如果你确保只取函数返回值的第一个值,可以使用括号运算符,例如:
print ( ( init ( ) ) , 2 )
print ( 2 , ( init ( ) ) )
值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。
全动态函数调用
调用回调函数,并把一个数组参数作为回调函数的参数。
local args = { ... } or { }
method_name ( unpack ( args, 1 , table. maxn ( args) ) )
使用场景
如果你的实参 table 中确定没有 nil 空洞,则可以简化为:
method_name ( unpack ( args) )
add_task ( end_time, callback, params)
if os. time ( ) >= endTime then
callback ( unpack ( params, 1 , table. maxn ( params) ) )
end
值得一提的是,unpack
内建函数还不能为 LuaJIT 所 JIT 编译,因此这种用法总是会被解释执行。对性能敏感的代码路径应避免这种用法。
牛刀小试
local function run ( x, y)
print ( 'run' , x, y)
end
local function attack ( targetId)
print ( 'targetId' , targetId)
end
local function do_action ( method, ... )
local args = { ... } or { }
method ( unpack ( args, 1 , table. maxn ( args) ) )
end
do_action ( run, 1 , 2 )
do_action ( attack, 1111 )
Lua模块
从 Lua 5.1 语言添加了对模块和包的支持。一个 Lua 模块的数据结构是用一个 Lua 值(通常是一个 Lua 表或者 Lua 函数)。一个 Lua 模块代码就是一个会返回这个 Lua 值的代码块。 可以使用内建函数 require()
来加载和缓存模块。简单的说,一个代码模块就是一个程序库,可以通过 require
来加载。模块加载后的结果通常是一个 Lua table,这个表就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和变量。require
函数会返回 Lua 模块加载后的结果,即用于表示该 Lua 模块的 Lua 值。
require函数
Lua 提供了一个名为 require
的函数用来加载模块。要加载一个模块,只需要简单地调用 require
“file” 就可以了,file 指模块所在的文件名。这个调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。 在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。相当于将导出的函数作为 table 的一个字段,在 Lua 中函数是第一类值,提供了天然的优势。 创建一个 lua 文件my.lua
,内容如下:
local _M = { }
local function get_name ( )
return "Lucy"
end
function _M. greeting ( )
print ( "hello " .. get_name ( ) )
end
return _M
再定义一个 lua 文件main.lua
,调用上面的模块,内容如下:
local my = require ( "my" )
my. greeting ( )
注意:对于需要导出给外部使用的公共模块,处于安全考虑,是要避免全局变量的出现。 我们可以使用 lj-releng 或 luacheck 工具完成全局变量的检测。 另一个要注意的是,由于在 LuaJIT 中,require 函数内不能进行上下文切换,所以不能够在模块的顶级上下文中调用 cosocket 一类的 API。 否则会报 attempt to yield across C-call boundary
错误。
Lua特别之处
Lua下标从1开始
Lua 是我知道的唯一一个下标从 1 开始的编程语言。这一点,虽然对于非程序员背景的人来说更好理解,但却容易导致程序的 bug。 举个例子:
resty - e 't={100}; ngx.say(t[0])'
你自然期望打印出 100,或者报错说下标 0 不存在。但结果出乎意料,什么都没有打印出来,也没有报错。既然如此,让我们加上 type 命令,来看下输出到底是什么:
resty - e 't={100};ngx.say(type(t[0]))'
原来是空值。事实上,在 OpenResty 中,对于空值的判断和处理也是一个容易让人迷惑的点。
拼接字符串
和大部分语言使用 + 不同,Lua 中使用两个点号来拼接字符串:
resty - e "ngx.say('hello' .. ', world')"
在实际的项目开发中,我们一般都会使用多种开发语言,而Lua 这种不走寻常路的设计,总是会让开发者的思维,在字符串拼接的时候卡顿一下,也是让人哭笑不得。
只有table一种数据结构
不同于 Python 这种内置数据结构丰富的语言,Lua 中只有一种数据结构,那就是 table,它里面可以包括数组和哈希表:
local color = { first = "red" , "blue" , third = "green" , "yellow" }
print ( color[ "first" ] )
print ( color[ 1 ] )
print ( color[ "third" ] )
print ( color[ 2 ] )
print ( color[ 3 ] )
如果不显式地用_键值对 _的方式赋值,table 就会默认用数字作为下标,从 1 开始。所以 color[1] 就是 blue。另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:
local t1 = { 1 , 2 , 3 }
print ( "Test1 " .. table. getn ( t1) )
local t2 = { 1 , a = 2 , 3 }
print ( "Test2 " .. table. getn ( t2) )
local t3 = { 1 , nil }
print ( "Test3 " .. table. getn ( t3) )
local t4 = { 1 , nil , 2 }
print ( "Test4 " .. table. getn ( t4) )
Test1 3
Test2 2
Test3 1
Test4 1
你可以看到,除了第一个返回长度为 3 的测试案例外,后面的测试都是我们预期之外的结果。事实上,想要在Lua 中获取 table 长度,必须注意到,只有在 table 是 序列 的时候,才能返回正确的值。 那什么是序列呢?首先序列是数组(array)的子集,也就是说,table 中的元素都可以用正整数下标访问到,不存在键值对的情况。对应到上面的代码中,除了 t2 外,其他的 table 都是 array。其次,序列中不包含空洞(hole),即 nil。综合这两点来看,上面的 table 中, t1 是一个序列,而 t3 和 t4 是 array,却不是序列(sequence)。 到这里,你可能还有一个疑问,为什么 t4 的长度会是 1 呢?其实这是因为,在遇到 nil 时,获取长度的逻辑就不继续往下运行,而是直接返回了。
默认全局变量
我想先强调一点,除非你相当确定,否则在 Lua 中声明变量时,前面都要加上 local。这是因为在 Lua 中,变量默认是全局的,会被放到名为 _G 的 table 中。不加 local 的变量会在全局表中查找,这是昂贵的操作。如果再加上一些变量名的拼写错误,就会造成难以定位的 bug。 所以,在 OpenResty 编程中,我强烈建议你总是使用 local 来声明变量,即使在 require module 的时候也是一样:
local xxx = require ( 'xxx' )
require ( 'xxx' )
FFI
FFI 库,是 LuaJIT 中最重要的一个扩展库。它允许从纯 Lua 代码调用外部 C 函数,使用 C 数据结构。有了它,就不用再像 Lua 标准 math
库一样,编写 Lua 扩展库。把开发者从开发 Lua 扩展 C 库(语言/功能绑定库)的繁重工作中释放出来。
简介
简单解释一下 Lua 扩展 C 库,对于那些能够被 Lua 调用的 C 函数来说,它的接口必须遵循 Lua 要求的形式,就是 typedef int (*lua_CFunction)(lua_State* L)
,这个函数包含的参数是 lua_State
类型的指针 L 。可以通过这个指针进一步获取通过 Lua 代码传入的参数。这个函数的返回值类型是一个整型,表示返回值的数量。需要注意的是,用 C 编写的函数无法把返回值返回给 Lua 代码,而是通过虚拟栈来传递 Lua 和 C 之间的调用参数和返回值。不仅在编程上开发效率变低,而且性能上比不上 FFI 库调用 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
库的时候,必须要有下面的一行。
local ffi = require "ffi"
ffi.cdef
语法: ffi.cdef(def) 功能: 声明 C 函数或者 C 的数据结构,数据结构可以是结构体、枚举或者是联合体,函数可以是 C 标准函数,或者第三方库函数,也可以是自定义的函数,注意这里只是函数的声明,并不是函数的定义。声明的函数应该要和原来的函数保持一致。
ffi. cdef[[
typedef struct foo { int a, b; } foo_t; /* Declare a struct and typedef. */
int printf(const char *fmt, ...); /* Declare a typical printf function. */
]]
注意: 所有使用的库函数都要对其进行声明,这和我们写 C 语言时候引入 .h 头文件是一样的。 顺带一提的是,并不是所有的 C 标准函数都能满足我们的需求,那么如何使用 第三方库函数 或 自定义的函数 呢,这会稍微麻烦一点,不用担心,你可以很快学会。: ) 首先创建一个 myffi.c
,其内容是:
int add ( int x, int y)
{
return x + y;
}
gcc -g -o libmyffi.so -fpic -shared myffi.c
为了方便我们测试,我们在 LD_LIBRARY_PATH
这个环境变量中加入了刚刚库所在的路径,因为编译器在查找动态库所在的路径的时候其中一个环节就是在 LD_LIBRARY_PATH
这个环境变量中的所有路径进行查找。命令如下所示:
export LD_LIBRARY_PATH= $LD_LIBRARY_PATH :your_lib_path
ffi. load ( name [ , global] )
ffi.load
会通过给定的 name
加载动态库,返回一个绑定到这个库符号的新的 C 库命名空间,在 POSIX
系统中,如果 global
被设置为 ture
,这个库符号被加载到一个全局命名空间。另外这个 name
可以是一个动态库的路径,那么会根据路径来查找,否则的话会在默认的搜索路径中去找动态库。在 POSIX
系统中,如果在 name
这个字段中没有写上点符号 .
,那么 .so
将会被自动添加进去,例如 ffi.load("z")
会在默认的共享库搜寻路径中去查找 libz.so
,在 windows
系统,如果没有包含点号,那么 .dll
会被自动加上。除此之外,还能使用 ffi.C
(调用 ffi.cdef
中声明的系统函数) 来直接调用 add
函数,记得要在 ffi.load
的时候加上参数 true
,例如 ffi.load('myffi', true)
。 下面是一个完整的例子:
local ffi = require "ffi"
ffi. load ( 'myffi' , true )
ffi. cdef [[
int add(int x, int y); /* don't forget to declare */
]]
local res = ffi. C. add ( 1 , 2 )
print ( res)
ffi.typeof
语法: ctype = ffi.typeof(ct) 功能: 创建一个 ctype 对象,会解析一个抽象的 C 类型定义。
local uintptr_t = ffi. typeof ( "uintptr_t" )
local c_str_t = ffi. typeof ( "const char*" )
local int_t = ffi. typeof ( "int" )
local int_array_t = ffi. typeof ( "int[?]" )
ffi.new
语法: cdata = ffi.new(ct [,nelem] [,init…]) 功能: 开辟空间,第一个参数为 ctype 对象,ctype 对象最好通过 ctype = ffi.typeof(ct) 构建。 顺便一提,可能很多人会有疑问,到底 ffi.new
和 ffi.C.malloc
有什么区别呢? 如果使用 ffi.new
分配的 cdata
对象指向的内存块是由垃圾回收器 LuaJIT GC
自动管理的,所以不需要用户去释放内存。 如果使用 ffi.C.malloc
分配的空间便不再使用 LuaJIT 自己的分配器了,所以不是由 LuaJIT GC
来管理的,但是,要注意的是 ffi.C.malloc
返回的指针本身所对应的 cdata
对象还是由 LuaJIT GC
来管理的,也就是这个指针的cdata
对象指向的是用 ffi.C.malloc
分配的内存空间。这个时候,你应该通过 ffi.gc()
函数在这个 C 指针的 cdata
对象上面注册自己的析构函数,这个析构函数里面你可以再调用 ffi.C.free
,这样的话当 C 指针所对应的 cdata
对象被 Luajit GC
管理器垃圾回收时候,也会自动调用你注册的那个析构函数来执行 C 级别的内存释放。 请尽可能使用最新版本的 Luajit
,x86_64
上由 LuaJIT GC
管理的内存已经由 1G->2G
,虽然管理的内存变大了,但是如果要使用很大的内存,还是用 ffi.C.malloc
来分配会比较好,避免耗尽了 LuaJIT GC
管理内存的上限,不过还是建议不要一下子分配很大的内存。
local int_array_t = ffi. typeof ( "int[?]" )
local bucket_v = ffi. new ( int_array_t, bucket_sz)
local queue_arr_type = ffi. typeof ( "lrucache_pureffi_queue_t[?]" )
local q = ffi. new ( queue_arr_type, size + 1 )
ffi.fill
语法: ffi.fill(dst, len [,c]) 功能: 填充数据,此函数和 memset(dst, c, len) 类似,注意参数的顺序。
ffi. fill ( self. bucket_v, ffi_sizeof ( int_t, bucket_sz) , 0 )
ffi. fill ( q, ffi_sizeof ( queue_type, size + 1 ) , 0 )
ffi.cast
语法: cdata = ffi.cast(ct, init) 功能: 创建一个 scalar cdata 对象。
local c_str_t = ffi. typeof ( "const char*" )
local c_str = ffi. cast ( c_str_t, str)
local uintptr_t = ffi. typeof ( "uintptr_t" )
tonumber ( ffi. cast ( uintptr_t, c_str) )
cdata 对象的垃圾回收
所有由显式的 ffi.new(), ffi.cast() etc.
或者隐式的 accessors
所创建的 cdata
对象都是能被垃圾回收的,当他们被使用的时候,你需要确保有在 Lua stack
,upvalue
,或者 Lua table
上保留有对 cdata
对象的有效引用,一旦最后一个 cdata
对象的有效引用失效了,那么垃圾回收器将自动释放内存(在下一个 GC
周期结束时候)。另外如果你要分配一个 cdata
数组给一个指针的话,你必须保持这个持有这个数据的 cdata
对象活跃,下面给出一个官方的示例:
ffi. cdef[[
typedef struct { int *a; } foo_t;
]]
local s = ffi. new ( "foo_t" , ffi. new ( "int[10]" ) )
local a = ffi. new ( "int[10]" )
local s = ffi. new ( "foo_t" , a)
相信看完上面的 API
你已经很累了,再坚持一下吧!休息几分钟后,让我们来看看下面对官方文档中的示例做剖析,希望能再加深你对 ffi
的理解。
调用C函数
local ffi = require ( "ffi" )
ffi. cdef[[
int printf(const char *fmt, ...);
]]
ffi. C. printf ( "Hello %s!" , "world" )
以上操作步骤,如下:
① 加载 FFI 库。 ② 为函数增加一个函数声明。这个包含在 中括号
对之间的部分,是标准 C 语法。 ③ 调用命名的 C 函数——非常简单。 事实上,背后的实现远非如此简单:③ 使用标准 C 库的命名空间 ffi.C
。通过符号名 printf
索引这个命名空间,自动绑定标准 C 库。索引结果是一个特殊类型的对象,当被调用时,执行 printf
函数。传递给这个函数的参数,从 Lua 对象自动转换为相应的 C 类型。 再来一个源自官方的示例代码:
local ffi = require ( "ffi" )
ffi. cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen, int level);
int uncompress(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen);
]]
local zlib = ffi. load ( ffi. os == "Windows" and "zlib1" or "z" )
local function compress ( txt)
local n = zlib. compressBound ( # txt)
local buf = ffi. new ( "uint8_t[?]" , n)
local buflen = ffi. new ( "unsigned long[1]" , n)
local res = zlib. compress2 ( buf, buflen, txt, # txt, 9 )
assert ( res == 0 )
return ffi. string ( buf, buflen[ 0 ] )
end
local function uncompress ( comp, n)
local buf = ffi. new ( "uint8_t[?]" , n)
local buflen = ffi. new ( "unsigned long[1]" , n)
local res = zlib. uncompress ( buf, buflen, comp, # comp)
assert ( res == 0 )
return ffi. string ( buf, buflen[ 0 ] )
end
local txt = string. rep ( "abcd" , 1000 )
print ( "Uncompressed size: " , # txt)
local c = compress ( txt)
print ( "Compressed size: " , # c)
local txt2 = uncompress ( c, # txt)
assert ( txt2 == txt)
解释一下这段代码。我们首先使用 ffi.cdef
声明了一些被 zlib 库提供的 C 函数。然后加载 zlib 共享库,在 Windows 系统上,则需要我们手动从网上下载 zlib1.dll 文件,而在 POSIX 系统上 libz 库一般都会被预安装。因为 ffi.load
函数会自动填补前缀和后缀,所以我们简单地使用 z 这个字母就可以加载了。我们检查 ffi.os
,以确保我们传递给 ffi.load
函数正确的名字。 一开始,压缩缓冲区的最大值被传递给 compressBound
函数,下一行代码分配了一个要压缩字符串长度的字节缓冲区。[?]
意味着他是一个变长数组。它的实际长度由 ffi.new
函数的第二个参数指定。 我们仔细审视一下 compress2
函数的声明就会发现,目标长度是用指针传递的!这是因为我们要传递进去缓冲区的最大值,并且得到缓冲区实际被使用的大小。 在 C 语言中,我们可以传递变量地址。但因为在 Lua 中并没有地址相关的操作符,所以我们使用只有一个元素的数组来代替。我们先用最大缓冲区大小初始化这唯一一个元素,接下来就是很直观地调用 zlib.compress2
函数了。使用 ffi.string
函数得到一个存储着压缩数据的 Lua 字符串,这个函数需要一个指向数据起始区的指针和实际长度。实际长度将会在 buflen
这个数组中返回。因为压缩数据并不包括原始字符串的长度,所以我们要显式地传递进去。
使用C数据结构
cdata
类型用来将任意 C 数据保存在 Lua 变量中。这个类型相当于一块原生的内存,除了赋值和相同性判断,Lua 没有为之预定义任何操作。然而,通过使用 metatable
(元表),程序员可以为 cdata
自定义一组操作。cdata
不能在 Lua 中创建出来,也不能在 Lua 中修改。这样的操作只能通过 C API。这一点保证了宿主程序完全掌管其中的数据。我们将 C 语言类型与 metamethod
(元方法)关联起来,这个操作只用做一次。ffi.metatype
会返回一个该类型的构造函数。原始 C 类型也可以被用来创建数组,元方法会被自动地应用到每个元素。 尤其需要指出的是,metatable
与 C 类型的关联是永久的,而且不允许被修改,__index
元方法也是。 下面是一个使用 C 数据结构的实例:
local ffi = require ( "ffi" )
ffi. cdef[[
typedef struct { double x, y; } point_t;
]]
local point
local mt = {
__add = function ( a, b) return point ( a. x+ b. x, a. y+ b. y) end ,
__len = function ( a) return math. sqrt ( a. x* a. x + a. y* a. y) end ,
__index = {
area = function ( a) return a. x* a. x + a. y* a. y end ,
} ,
}
point = ffi. metatype ( "point_t" , mt)
local a = point ( 3 , 4 )
print ( a. x, a. y)
print ( # a)
print ( a: area ( ) )
local b = a + point ( 0.5 , 8 )
print ( # b)
Idiom C code Lua code Pointer dereference x = *p x = p[0] int *p *p = y p[0] = y Pointer indexing x = p[i] x = p[i] int i, *p p[i+1] = y p[i+1] = y Array indexing x = a[i] x = a[i] int i, a[] a[i+1] = y a[i+1] = y struct/union dereference x = s.field x = s.field struct foo s s.field = y s.field = y struct/union pointer deref x = sp->field x = sp.field struct foo *sp sp->field = y s.field = y int i, *p y = p - i y = p - i Pointer dereference x = p1 - p2 x = p1 - p2 Array element pointer x = &a[i] x = a + i
小心内存泄漏
所谓“能力越大,责任越大”,FFI 库在允许我们调用 C 函数的同时,也把内存管理的重担压到我们的肩上。 还好 FFI 库提供了很好用的 ffi.gc
方法。该方法允许给 cdata 对象注册在 GC 时调用的回调,它能让你在 Lua 领域里完成 C 手工释放资源的事。 C++ 提倡用一种叫 RAII 的方式管理你的资源。简单地说,就是创建对象时获取,销毁对象时释放。我们可以在 LuaJIT 的 FFI 里借鉴同样的做法,在调用 resource = ffi.C.xx_create
等申请资源的函数之后,立即补上一行 ffi.gc(resource, ...)
来注册释放资源的函数。尽量避免尝试手动释放资源!即使不考虑 error
对执行路径的影响,在每个出口都补上一模一样的逻辑会够你受的(用 goto
也差不多,只是稍稍好一点)。 有些时候,ffi.C.xx_create
返回的不是具体的 cdata,而是整型的 handle。这会儿需要用 ffi.metatype
把 ffi.gc
包装一下:
local resource_type = ffi. metatype ( "struct {int handle;}" , {
__gc = free_resource
} )
local function free_resource ( handle)
...
end
resource = ffi. new ( resource_type)
resource. handle = ffi. C. xx_create ( )
如果你没能把申请资源和释放资源的步骤放一起,那么内存泄露多半会在前方等你。写代码的时候切记这一点。
LuaJIT&Lua
LuaJIT
OpenResty 的另一块基石:LuaJIT。 当然,在 OpenResty 中,写出正确的 LuaJIT 代码的门槛并不高,但要写出高效的 LuaJIT 代码绝非易事。 OpenResty 的 worker 进程都是 fork master 进程而得到的, 其实, master 进程中的LuaJIT 虚拟机也会一起 fork 过来。在同一个 worker 内的所有协程,都会共享这个 LuaJIT 虚拟机,Lua 代码的执行也是在这个虚拟机中完成的。
Lua&JIT
先把重要的事情放在前面说:标准 Lua 和 LuaJIT 是两回事儿,LuaJIT 只是兼容了 Lua 5.1 的语法。 标准 Lua 现在的最新版本是 5.3,LuaJIT 的最新版本则是 2.1.0-beta3。在 OpenResty 几年前的老版本中,编译的时候,你可以选择使用标准 Lua VM ,或者 LuaJIT VM 来作为执行环境,不过,现在已经去掉了对标准 Lua 的支持,只支持 LuaJIT。 LuaJIT 的语法兼容 Lua 5.1,并对 Lua 5.2 和 5.3 做了选择性支持。所以我们应该先学习 Lua 5.1 的语法,并在此基础上学习 LuaJIT 的特性。
LuaJIT特别之处
明白了Lua这四点特别之处,我们继续来说LuaJIT。除了兼容 Lua 5.1 的语法并支持 JIT 外,LuaJIT 还紧密结合了 FFI(Foreign Function Interface),可以让你直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。 例如:
local ffi = require ( "ffi" )
ffi. cdef[[
int printf(const char *fmt, ...);
]]
ffi. C. printf ( "Hello %s!" , "world" )
短短这几行代码,就可以直接在 Lua 中调用 C 的 printf 函数,打印出 Hello world!。你可以使用resty 命令来运行它,看下是否成功。 类似的,我们可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。实际上,FFI 方式比传统的 Lua/C API 方式的性能更优,这也是 lua-resty-core 项目存在的意义。
初步认识OpenResty
OpenResty的发展
OpenResty 并不像其他的开发语言一样从零开始搭建,而是基于成熟的开源组件——NGINX 和 LuaJIT。 OpenResty 诞生于 2007 年,不过,它的第一个版本并没有选择 Lua,而是用了 Perl,这跟作者章亦春的技术偏好有很大关系。 但 Perl 的性能远远不能达到要求,于是,在第二个版本中,Perl 就被 Lua 给替换了。 不过, 在 OpenResty 官方的项目中,Perl 依然占据着重要的角色,OpenResty 工程化方面都是用 Perl 来构建,比如测试框架、Linter、CLI 等,后面我们也会逐步介绍。 后来,章亦春离开了淘宝,加入了美国的 CDN 公司 Cloudflare。因为 OpenResty 高性能和动态的优势很适合 CDN 的业务需求,很快 OpenResty 就成为 CDN 的技术标准。通过丰富的 lua-resty 库,OpenResty 开始逐渐摆脱 NGINX 的影子,形成自己的生态体系,在 API 网关、软WAF 等领域被广泛使用。 OpenResty 是一个被广泛使用的技术,但它并不能算得上是热门技术,这听上去有点矛盾,到底什么意思呢? 说它应用广,是因为 OpenResty 现在是全球排名第五的 Web 服务器。我们经常用到的 12306 的余票查询功能,或者是京东的商品详情页,这些高流量的背后,其实都是 OpenResty 在默默地提供服务。 说它并不热门,那是因为使用 OpenResty 来构建业务系统的比例并不高。使用者大都用 OpenResty 来处理入口流量,并没有深入到业务里面去,自然,对于 OpenResty 的使用也是浅尝辄止,满足当前的需求就可以了。这当然也与 OpenResty 没有像 Java、Python 那样有成熟的 Web 框架和生态有关。
OpenResty三大特性
详尽的文档和测试用例
没错,文档和测试是判断开源项目是否靠谱的关键指标,甚至是排在代码质量和性能之前的。 OpenResty 的文档非常详细,作者把每一个需要注意的点都写在了文档中。绝大部分时候,我们只需要仔细查看文档,就能解决遇到的问题,而不用谷歌搜索或者是跟踪到源码中。为了方便起见,OpenResty 还自带了一个命令行工具restydoc
,专门用来帮助你通过 shell 查看文档,避免编码过程被打断。 不过,文档中只会有一两个通用的代码片段,并没有完整和复杂的示例,到哪里可以找到这样的例子呢? 对于 OpenResty 来说,自然是/t目录,它里面就是所有的测试案例。每一个测试案例都包含完整的 NGINX 配置和 Lua 代码,以及测试的输入数据和预期的输出数据。不过,OpenResty 使用的测试框架,与其他断言风格的测试框架完全不同。
同步非阻塞
协程,是很多脚本语言为了提升性能,在近几年新增的特性。但它们实现得并不完美,有些是语法糖,有些还需要显式的关键字声明。 OpenResty 则没有历史包袱,在诞生之初就支持了协程,并基于此实现了 同步非阻塞的编程模式。这一点是很重要的,毕竟,程序员也是人,代码应该更符合人的思维习惯。显式的回调和异步关键字会打断思路,也给调试带来了困难。 这里我解释一下,什么是同步非阻塞。先说同步,这个很简单,就是按照代码来顺序执行。比如下面这段伪:
local res, err = query- mysql ( sql)
local value, err = query- redis ( key)
在同一请求连接中,如果要等 MySQL 的查询结果返回后,才能继续去查询 Redis,那就是同步;如果不用等 MySQL 的返回,就能继续往下走,去查询 Redis,那就是异步。对于 OpenResty 来说,绝大部分都是同步操作,只有 ngx.timer 这种后台定时器相关的 API,才是异步操作。 再来说说非阻塞,这是一个很容易和“异步”混淆的概念。这里我们说的“阻塞”,特指阻塞操作系统线程。我们继续看上面的例子,假设查询 MySQL 需要1s 的时间,如果在这1s 内,操作系统的资源(CPU)是空闲着并傻傻地等待返回,那就是阻塞;如果 CPU 趁机去处理其他连接的请求,那就是非阻塞。非阻塞也是 C10K、C100K 这些高并发能够实现的关键。 同步非阻塞这个概念很重要,建议你仔细琢磨一下。我认为,这一概念最好不要通过类比来理解,因为不恰当的类比,很可能把你搞得更糊涂。 在 OpenResty 中,上面的伪码就可以直接实现同步非阻塞,而不用任何显式的关键字。这里也再次体现了,让开发者用起来更简单,是 OpenResty 的理念之一。
动态
OpenResty 有一个非常大的优势,并且还没有被充分挖掘,就是它的 动态。 传统的 Web 服务器,比如 NGINX,如果发生任何的变动,都需要你去修改磁盘上的配置文件,然后重新加载才能生效,这也是因为它们并没有提供 API,来控制运行时的行为。所以,在需要频繁变动的微服务领域,NGINX 虽然有多次尝试,但毫无建树。而异军突起的 Envoy, 正是凭着 xDS 这种动态控制的 API,大有对 NGINX 造成降维攻击的威胁。 和 NGINX 、 Envoy 不同的是,OpenResty 是由脚本语言 Lua 来控制逻辑的,而动态,便是 Lua 天生的优势。通过 OpenResty 中 lua-nginx-module 模块中提供的 Lua API,我们可以动态地控制路由、上游、SSL 证书、请求、响应等。甚至更进一步,你可以在不重启 OpenResty 的前提下,修改业务的处理逻辑,并不局限于 OpenResty 提供的 Lua API。 这里有一个很合适的类比,可以帮你理解上面关于动态的说明。你可以把 Web 服务器当做是一个正在高速公路上飞驰的汽车,NGINX 需要停车才能更换轮胎,更换车漆颜色;Envoy 可以一边跑一边换轮胎和颜色;而 OpenResty 除了具备前者能力外,还可以在不停车的情况下,直接把汽车从 SUV 变成跑车。 显然,掌握这种“逆天”的能力后,OpenResty 的能力圈和想象力就扩展到了其他领域,比如 Serverless和边缘计算等。
OpenResty学习重点
讲了这么多OpenResty的重点特性,你又该怎么学呢?我认为,学习需要抓重点,围绕主线来展开,而不是眉毛胡子一把抓,这样,你才能构建出脉络清晰的知识体系。 要知道,不管多么全面的课程,都不可能覆盖所有问题,更不能直接帮你解决线上的每个 bug 和异常。 回到OpenResty的学习,在我看来,想要学好 OpenResty,你必须理解下面几个重点:
OpenResty安装部署
Windows安装部署
https://github.com/LomoX-Offical/nginx-openresty-windows/releases/download/1.13.5.1001/Openresty_For_Windows_1.13.5.1001_64Bit.zip
安装:解压到要安装的目录,进入到 openresty 解压的根目录,双击执行 nginx.exe 或者使用命令 start nginx 启动 nginx,如果没有错误现在 nginx 已经开始运行了。 验证 nginx 是否成功启动:
tasklist /fi "imagename eq nginx.exe"
在浏览器的地址栏输入 localhost,加载 nginx 的欢迎页面。成功加载说明 nginx 正在运行。如下图:
另外当 nginx 成功启动后,master 进程的 pid 存放在 logs\nginx.pid
文件中。
Linux安装部署
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
sudo yum check-update
sudo yum install openresty
sudo yum install openresty-resty
nginx version: openresty/1.19.3.1
/var/lib/yum/repos/x86_64/7/openresty
/var/cache/yum/x86_64/7/openresty
/usr/bin/openresty
/usr/local/openresty
/usr/local/openresty/bin/openresty
/usr/local/openresty/nginx/conf/nginx.conf
yum --disablerepo= "*" --enablerepo= "openresty" list available
MacOS安装部署
brew untap homebrew/nginx
brew tap openresty/brew
brew install openresty
brew install openresty-debug
/usr/local/etc/openresty/nginx.conf
Hello World
# . / resty - e "ngx.say('Hello World')"
Hello World
使用content_by_lua来引入lua 的使用方式:
location /lua {
default_type text/html;
content_by_lua 'ngx.say("hello lua!!")' ;
}
把lua代码从nginx.conf里面抽取出来,保持代码的可读性和可维护性:
ngx.say( "Hello World!!" )
location /lua1 {
default_type text/html;
content_by_lua_file lua/01.lua;
}
local args = ngx.reg.get_uri_args( )
ngx.say( "Hello OpenResty! Lua is so easy!===" .. args.id)
location /lua2 {
content_by_lua_file lua/02.lua;
}
cat .. /lua/03.lua
ngx.exec( '/seckill/goods/detail/1' ) ;
location /lua3 {
content_by_lua_file lua/03.lua;
}