Lua学习笔记
参考如下命令,编译并安装Lua解释器:
1 2 3 4 5 6 |
curl -R -O http://www.lua.org/ftp/lua-5.3.4.tar.gz tar zxf lua-5.3.4.tar.gz cd lua-5.3.4 sudo apt-get install libreadline-dev make linux make install INSTALL_TOP=/home/alex/Lua/sdk/5.3.4 |
要声明一个变量,使用如下语法: varname = value
任何变量,除非显式添加local限定符,否则均为全局变量,不管它在何处声明:
1 2 |
globalVar = true local localVar = nil |
Lua支持块级作用域,局部变量的作用域在所在块内部。
一般的,应当严格避免使用自己定义的全局变量。
Lua在一个名为 _G的表格中管理所有全局变量:
1 2 |
_G.globalVar _G["globalVar"] |
类似于Go语言的空白标识符。如果多返回值中有你不需要的,可以将其赋值给 _
需要注意Lua不支持++、--、+=之类的操作符。
Lua中的逻辑或与非操作符为 or、 and、 not,不等于操作符为 ~=
Lua中有一个特殊操作符 #,它可以获取对象的长度,实际上是调用元方法__len()
Lua中的数字均为双精度浮点型,你不需要担心运算精度或者速度问题。示例:
1 2 3 4 5 |
-- 语句尾部的分号是可选的 num = 4096 num = 6.12 num = 6.02E23 num = 0xff |
可以使用单引号、双引号包围,支持多行字符串,支持C风格的转义字符。示例:
1 2 3 4 |
str = "Hello World" str = 'Hello\n World' str = [[Hello World]] |
访问一个未定义变量时,其值为空,使用关键字 nil表示。
有true和false两个值。类型转换时,仅nil转换为false,其余的例如数字0、空串都转换为true。
Lua不区分数组和映射,以整数作为键的表格,可以作为数组来使用:
1 2 3 4 5 6 7 |
-- 如果不显示指定键,则自动从1开始为每个元素赋予键 users = { "Alex", "Meng", "Cai", "Dang" } -- 数组的第一个元素的索引为1,而不是0 print(users[1]) -- Alex -- 可以使用复数索引 users[-1] = 'Nobody' print(users[-1]) -- Nobody |
以字符串作为键的表格,则相当于其它编程语言中的映射或字典:
1 2 3 4 5 6 7 8 9 10 11 12 |
user = { -- 注意语法,使用=号指定键值对 name = "Alex Wong", age = 32, gender = 'M', -- 也可以使用方括号语法指定键值对 ['nickname'] = 'Alex' } -- 方括号语法、点号导航都支持 user.name = "汪震" user.name = nil user.nickname = '震' |
要获取表格的元素个数,可以使用 #操作符:
1 2 3 4 5 |
users = { [1] = 'Alex', [2] = 'Meng', [3] = 'Cai', [4] = 'Dang' } -- #users 可以获取表格users的长度 for i = 1, #users do print(users[i]) end |
要遍历表格的键值对,可以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
for idx, user in pairs( users ) do print( idx, user ) end array = { ['Z'] = 'z', [0] = 'a', [1] = 'b', [10] = 'b' }; -- pairs打印所有键值对 for i, v in pairs(array) do print(i .. '=' .. v) end -- ipairs只能打印从索引从1开始的,而且不能有中断 for i, v in ipairs(array) do print(i .. '=' .. v) end -- 只打印1=b |
注意:不要在表格中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替:
1 2 3 |
users = { 1, 2, 3 } table.remove(users, 1) print(table.concat(users, ' ')) -- 2 3 |
1 2 3 4 5 |
-- 单行注释 --[[ 多行注释 --]] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
n, i, factorial = 10, 1, 1 -- while循环 while i <= n do factorial = factorial * i i = i + 1 end print(factorial) -- 3628800 -- for循环 for i = 1, n do factorial = factorial / i end print(factorial) -- 1.0 -- until循环 i = 1 repeat factorial = factorial * i i = i + 1 until i > n |
1 2 3 4 5 6 7 8 |
if len > 100 then print("L") elseif len <= 100 and len > 50 then print("M") elseif len ~= 0 then print("S") else end |
Lua中函数必须在先定义,然后再使用。也就是说,在脚本中函数定义出现在前面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
function getUserName(id) return "Alex" end -- 支持返回多个值 function getUser(id) return id, "Alex", 32 end id, name, age = getUser(1) -- 如果左边变量不足,多余的返回值自动丢弃 -- 如果左边变量过多,后面的全部置为nil -- 函数调用出现在列表表达式中时,除非函数调用在列表尾部,否则仅仅返回其第一个值 local function twonums() return 1, 2 end -- 注意下面两个列表表达式的区别 local x, y, z = twonums(), 3; print(x, y, z) -- 1 3 nil local x, y, z = 3, twonums(); print(x, y, z) -- 3 1 2 -- print的实参也是列表表达式 print(twonums(), 3) -- 1 3 print(3, twonums()) -- 3 1 2 -- 要强制仅仅返回第一个值,可以用()包围函数调用 print(3, (twonums())) -- 3 1 -- 支持闭包 function counter() local count = 0 return function() count = count + 1 return count end end c = counter() print(c(), c()) -- 1 2 -- 函数可以递归调用自身 function fib(n) if n < 2 then return 1 end return fib(n - 2) + fib(n - 1) end -- 具有局部作用域的函数 local function f() end -- 函数可以赋值给变量 local f = function() return 0 end f() -- 变长参数 local function join(...) local args = { ... } -- 把变长参数填充到表格 print(table.concat(args, ".")) end -- LuaJIT 2尚不能JIT编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式 join('cc', 'gmem', 'study') -- cc.gmem.study -- 当表格作为参数时,传递的是引用 local function printUser(user) print(user.name, user.age) end printUser({ name = 'Alex', age = 32 }) -- 调用回调 local function call(func, ...) local args = { ... } or {} -- unpack,解包表格 func(...) end local function add(x, y, z) print(x + y + z) end call(add, 1, 2, 3) -- 6 |
作为表格成员的函数,即为方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Person = { name = "Unknown" } -- 方法可以用TableName.MethodName的形式定义,此时第一个形参必须为self -- 代表当前对象 function Person.getName(self) return self.name end -- 方法也可以用TableName:MethodName的形式定义,此时隐式的自动定义self局部变量 function Person:getName() return self.name end print(Person:getName()) -- Unknown |
MetaTable可以用来实现操作符重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-- 元表格 user_ops = {} -- __add是一种元方法(MetaMethod) function user_ops.__add(u1, u2) return 'child of ' .. u1.name .. ' and ' .. u2.name end alex = { name = 'Alex' } meng = { name = 'Meng' } -- 可以为任何表格设置元表格 setmetatable(alex, user_ops) setmetatable(meng, user_ops) print(alex + meng) -- child of Alex and Meng |
所有可用的元方法如下表:
元方法 | 说明 |
__add(a, b) | a + b |
__sub(a, b) | a - b |
__mul(a, b) | a * b |
__div(a, b) | a / b |
__mod(a, b) | a % b |
__pow(a, b) | a ^ b |
__unm(a) | -a |
__concat(a, b) | a .. b |
__len(a) | #a |
__eq(a, b) | a == b |
__lt(a, b) | a < b |
__le(a, b) | a <= b |
__index(a, b) | a.b |
__newindex(a, b, c) | a.b = c |
__call(a, ...) | a(...) |
利用元方法__index,可以实现基于原型的继承机制,类似于JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
Flower = {} print(Flower) -- 0x1cc1400 -- 定义构造器 function Flower:new(f) -- self指向调用者本身 0x1cc1400 也就是Flower这个表格 -- 下面这个表达式将Flower变为元表格 self.__index = self print(self) -- 调用setmetatable(a, {__index = b}),即让b作为a的原型 -- 在这里,就是让self成为f的原型 return setmetatable(f, self) end -- 定义普通方法 function Flower:getColor() return self.color end -- 也可以使用点形式定义,但是self需要作为第一个形参 function Flower.getColor(self) return self.color end -- 实例化新对象 jasmine = Flower:new({ color = 'White' }) print(jasmine.color) -- White -- 调用方法,注意使用冒号而非点号 print(jasmine:getColor()) -- White -- 如果用点号调用,则必须把self作为第一个参数传入 print(jasmine.getColor(jasmine)) -- White -- 继承 Jasmine = Flower:new({ color = 'White' }) function Jasmine:new(j) self.__index = self return setmetatable(j, self) end function Jasmine:getSmell() return self.smell end function Jasmine:toString() -- 可以在方法中定义方法 return self:getColor() .. ' Jasmine Smells ' .. self.smell end jasmine = Jasmine:new({ smell = 'Nice' }) print(jasmine:toString()) -- White Jasmine Smells Nice |
你可以使用 require('XX')语句来引入其它模块,也就是XX.lua文件。require时目标.lua文件会自动被执行。注意:
- 如果多次require同一个文件,则仅仅第一次会执行目标文件
- 如果想多次执行某个文件,调用 dofile('xx')
- 如果仅仅希望载入模块,但是不执行其中的代码,可以调用:
1234local xxModule = loadfile("XX")-- ...-- 下面的调用导致目标模块被执行xxModule()
一个长方形模块的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
-- 此局部变量,一个表格,作为模块成员的容器 local _M = {} -- 此属性,通常作为模块版本 _M._VERSION = '1.0' local mt = { __index = _M } function _M.new(self, width, height) return setmetatable({ width=width, height=height }, mt) end function _M.get_rectangle(self) return self.width * self.height end function _M.get_circumference(self) return (self.width + self.height) * 2 end -- 返回模块容器 return _M |
使用此模块:
1 2 3 4 5 |
local rectangle = require "rectangle" local s = rectangle:new(1, 2) print(s:get_rectangle()) -- 2 print(s:get_circumference()) -- 6 |
要在 Lua 中处理错误,必须使用函数 pcall(protected call)来包装需要执行的代码。 pcall接受以下参数:
- 被调用的函数
- 转发给被调用函数的参数
pcall的返回值有两个:是否调用成功,被调用函数的返回值。
示例:
1 2 3 4 5 6 7 8 |
function json_decode( str ) -- 安全的解码JSON local ok, t = pcall(_json_decode, str) if not ok then return nil end return t end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
-- string.byte(s [, i [, j ]]) -- 获取字符串s从i到j这些字符的ASCII码,返回列表 a, b, c = string.byte("abc", 1, 3) print(a, b, c) -- 97 98 99 -- string.char (...) -- 从列表ASCII码构建字符串 print(string.char(97, 98, 99)) -- abc -- 大小写转换 print(string.upper("Hello Lua")) -- HELLO LUA print(string.lower("Hello Lua")) -- hello lua -- 重复字符串 print(string.rep("abc", 3)) -- abcabcabc -- 返回子串 string.sub(s, i [, j]) print(string.sub(123456,3,5)) -- 345 -- 替换子串 string.gsub(s, p, r [, n]) -- 替换s中的p为r,n表示替换的最大次数 -- 返回两个值:替换后的字符串,替换发生的次数 print(string.gsub("Lua Lua Lua", "Lua", "hello")) -- hello hello hello 3 -- 获得字符串长度 str = "hello lua" print(string.len(str)) print(#str) -- 推荐使用这种方式获得长度,获取字节数 -- string.find(s, p [, init [, plain]]) -- 子串查找,从s中勋撰p,可以指定搜索起始位置init。返回子串的索引 sidx, eidx = string.find('123456789', '45') print(sidx, eidx) -- 4 5 -- C风格的字符串格式化 string.format(formatstring, ...) print(string.format("%.4f", 3.1415926)) -- 3.1416 print(string.format("%d %x %o", 31, 31, 31)) -- 31 1f 37 print(string.format("%02d/%02d/%d", 1, 1, 2018)) -- 01/01/2018 -- 模式匹配,不支持JIT,考虑使用ngx.re.match代替 -- string.match(s, p [, init]) 从s中搜索模式p,返回匹配的子串 print(string.match("Linux Zircon 3.13.0-83-generic", "%d.%d+.%d%-%d+")) -- 3.13.0-83 -- 全局模式匹配,返回一个迭代器,此迭代器依次返回每个匹配的子串 for word in string.gmatch("quick fox jumps over the lazy dog", "%w+") do print(word) end |
此库将表格作为数组来操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
-- table.getn(t) 获取表格长度 -- 注意:获取到的是最后一个数字下标 print(table.getn({ 1, 2, 3 })) -- 3 print(table.getn({ 1, two = 2, 3 })) -- 2 最后一个索引从1开始顺延 print(table.getn({ 1, 2, nil, 4, 5, 6 })) -- 6 如果最后是非nil,其索引被使用 print(table.getn({ 1, 2, nil, 4, 5, nil })) -- 2 如果最后是nil,则第一个nil之前的索引被使用 -- table.maxn (table) 返回最大的索引值 print(table.maxn({ [1000] = 0 })) -- 1000 -- table.concat (table [, sep [, i [, j ] ] ]) 连接数组元素为字符串 -- sep没认为空串,i为起始元素,j为结束元素 print(table.concat({'Alex','Meng'})) -- AlexMeng -- table.insert (table, [pos ,] value) 插入元素到数组,pos默认为表长度+1,即默认为附加元素 array = { 1, 2, 3 } table.insert(array, 4) print(array[4]) -- 4 -- table.remove (table [, pos]) 删除指定位置上的元素,默认从尾部删除 table.remove(array) print(#array) -- 3 -- table.sort (table [, comp]) 根据传入的比较器来排序 local function compare(x, y) --从大到小排序 return x > y end table.sort(array) -- 默认从小到大 table.sort(array,compare) -- 从大到小 |
LuaJIT 2.1 新增加 table.new 和 table.clear。前者用来预分配 Lua table 空间,后者用来高效的释放 table 空间,都可以被 JIT 编译。
os模块的一些函数,提供了日期、时间相关的功能,但是比较重量级,不支持被LuaJIT编译。使用OpenResty时推荐使用ngx_lua模块提供的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
-- os.time ([table]) 返回当前时间,或者table所指定的时间(从UNIX纪元开始流逝的秒数 print(os.time()) -- 当前时间 a = { year = 1970, month = 1, day = 1, hour = 8, min = 0, sec = 10 } print(os.time(a)) -- 10,因为当前是东八区 -- os.difftime (t2, t1) 返回两个时间的差值(t2-t1) 单位秒 a = { year = 1970, month = 1, day = 1, hour = 8, min = 0, sec = 10 } b = { year = 1970, month = 1, day = 1, hour = 8, min = 0, sec = 15 } print(os.difftime(os.time(a), os.time(b))) -- -5 -- os.date ([format [, time]]),返回一个描述时间的表格 -- format 格式说明,time时间点(秒) local tab1 = os.date("*t") local now = "" for k, v in pairs(tab1) do --把tab1转换成一个字符串 now = string.format("%s %s = %s ", now, k, tostring(v)) end print(now) -- hour = 18 min = 3 wday = 2 day = 26 month = 2 year = 2018 sec = 52 yday = 57 isdst = false -- 下面的占位符用于格式化日期 -- %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) -- %% 字符'%' print(os.date('%A')) -- Monday print(os.date('%Y-%m-%d')) -- 2018-02-26 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
math.max(x, ...) -- 返回参数中值最大的那个数 math.min(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.log10(x) -- 返回以10为底,x的对数 math.floor(x) -- 返回最大且不大于x的整数 math.ceil(x) -- 返回最小且不小于x的整数 math.rad(x) -- 角度x转换成弧度 math.deg(x) -- 弧度x转换成角度 math.sin(x) -- 求弧度x的正弦值 math.cos(x) -- 求弧度x的余弦值 math.tan(x) -- 求弧度x的正切值 math.asin(x) -- 求x的反正弦值 math.acos(x) -- 求x的反余弦值 math.atan(x) -- 求x的反正切值 -- 不传入参数时,返回 一个在区间[0,1)内均匀分布的伪随机实数 -- 只使用一个整数参数m时,返回一个在区间[1, m]内均匀分布的伪随机整数 -- 使用两个整数参数时,返回一个在区间[m, n]内均匀分布的伪随机整数 math.random (m,n) -- 为伪随机数生成器设置一个种子,设置后math.random产生的随机序列改变 math.randomseed (x) pi=math.pi -- 圆周率 |
Lua I/O提供的文件操作都是阻塞性的,不应该在OpenResty中和非阻塞的网络IO混合在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
path = [[/home/alex/Lua/projects/luna/logs/error.log]] -- 设置输入输出默认针对的文件 file = io.input(path) -- 逐行读取默认文件 repeat line = io.read() -- 读取一行 if (line) then print(line) end until (nil == line) io.close(file) -- 关闭文件描述符 -- 以追加模式打开文件 file = io.open(path, "a+") -- 所有可用的文件打开模式 -- 默认 说明 文件不存在时的行为 -- "r" 读模式 (默认) 返回nil加错误信息 -- "w" 写模式 创建文件 -- "a" 添加模式 创建文件 -- "r+" 更新模式,保存之前的数据 返回nil加错误信息 -- "w+" 更新模式,清除之前的数据 创建文件 -- "a+" 添加更新模式,保存之前的数据,在文件尾进行添加 创建文件 -- 设置默认输出文件 io.output(file) io.write("\nHello Lua") -- 调用file:***方法,可以针对特定文件(而不是默认文件)进行读写 file = io.open(path, "r") -- file:lines()方法返回所有行的迭代器 for line in file:lines() do print(line) end -- 其它方法: -- file:read (...) 读取数据 -- file:write (...) 写入数据: -- *n 读取一个数字 -- *a 从当前位置读取整个文件。若当前位置为文件尾,则返回空字符串 -- *l 读取下一行的内容。若为文件尾,则返回nil -- 100 读取100字节 print(file:read(10)) -- file:seek ([whence] [, offset]) 设置和获取当前文件位置 -- offset 向前搜索的偏移量 -- whence 偏移量的相对位置:set相对文件开始;cur相对当前位置(默认);end相对文件结束 file:seek('cur', 100) -- file:setvbuf (mode [, size]) 设置输出文件的缓冲模式 -- mode 模式:no无缓冲;full全缓冲(缓冲区满了后才输出);line最多缓冲一行 file:setvbuf('full', 2048) -- file:flush () 刷出文件缓冲区 file:flush() -- file:close () 关闭文件 file:close() |
LuaJIT是Lua的一个即时(Just-In-Time)编译器,是最快的动态语言实现之一。通过编译,Lua的执行效率大大提高。LuaJIT被大量的用在游戏、网络等编程领域。LuaJIT仅仅是一个独立的可执行程序,其命令行选项和标准的Lua没有太大区别。
LuaJIT 的运行时环境包括一个用手写汇编实现的 Lua 解释器和一个可以直接生成机器代码的 JIT 编译器。
1 2 3 4 |
wget http://luajit.org/download/LuaJIT-2.0.5.tar.gz tar xzf LuaJIT-2.0.5.tar.gz && rm LuaJIT-2.0.5.tar.gz cd LuaJIT-2.0.5/ make install PREFIX=/home/alex/Lua/LuaJIT/2.0.5 |
C函数要能够作为Lua扩展库,其接口必须遵循 typedef int (*lua_CFunction)(lua_State* L)的形式。此接口的入参是lua_State类型的指针,从Lua传递来的实参,必须通过该指针间接的读取。返回值int表示返回值的数量,返回值本身无法直接传递回去,必须使用虚拟栈来传递。开发Lua扩展库非常麻烦。
FFI是LuaJIT中最重要的一个扩展库。支持从纯Lua代码调用外部C函数,使用C数据结构。有了FFI,你就不再需要编写Lua扩展库,另外FFI的性能比扩展库更高。
名词 | 说明 |
cdecl | 一个抽象的C类型的定义,表现为Lua字符串 |
ctype | C类型对象,属于一种特殊的cdata,由 ffi.typeof()获得,你可以调用它,获得cdata对象 |
cdata | C数据对象,和ctype类型匹配的值 |
ct | C类型格式,可以指cdecl, cdata, ctype |
cb | 回调对象,持有一个特殊的函数指针,从C代码调用此函数指针,会执行对应的Lua函数 |
VLA | 变长数组,例如 int[?]。数组元素的个数,必须在创建VLA时给出 |
VLS | 变长结构,即最后一个成员是VLA的C结构体 |
用于声明(而不是定义)C函数或者C数据结构。所有使用到的函数都需要声明。
函数可以是 C 标准函数,第三方库函数,或自定义的函数。
据结构可以是结构体、枚举或者是联合体。
示例:
1 2 3 4 |
ffi.cdef [[ typedef struct foo { int a, b; } foo_t; int printf(const char *fmt, ...); ]] |
通过给定的名称来加载动态库,调用格式: ffi.load(name [,global])。
动态库可以指定全限定路径,如果仅仅指定basename则必须位于LD_LIBRARY_PATH,动态库扩展名可以不加:
1 |
ffi.load("z") -- 寻找libz.so,或者z.dll |
如果global设置为true,则在POSIX系统中目标动态库中的符号被加载到命名空间 ffi.C。例如:
1 2 3 4 5 6 7 |
ffi.load('z',true) ffi.cdef[[ int z(int x, int y); ]] local res = ffi.C.z(1, 2) |
创建一个ctype对象。调用格式: ctype = ffi.typeof(ct)。示例:
1 2 3 4 5 6 7 8 9 |
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[?]") -- 示例: local int_t = ffi.typeof("int") local i = int_t(10) print(i) -- cdata<int>: 0x401258e8 |
在堆内存中开辟空间,创建指定类型的ct。调用格式:
1 2 3 4 |
-- 从指定的ct创建cdata,对于VLA/VLS类型,需要指定元素个数(nelem) cdata = ffi.new(ct [,nelem] [,init...]) -- 等价方式: cdata = ctype([nelem,] [init...]) |
使用ffi.new分配的 cdata 对象指向的内存块是由垃圾回收器 LuaJIT GC 自动管理的,不需要用户去释放内存。
使用 ffi.C.malloc()分配的空间不使用 LuaJIT 自己的分配器,需要开发人员自己负责释放。但是ffi.C.malloc返回的cdata对象仍然由LuaJIT GC管理。
你可以通过 ffi.gc()为ffi.C.malloc返回的cdata对象注册析构函数,并在此函数中调用 ffi.C.free()。这样的话,当cdata对象被GC后,C内存也可以被释放。
ADM64上LuaJIT能够管理的内存量在2G这个级别,如果要分配非常大的内存,请使用ffi.C.malloc()
示例代码:
1 2 |
local int_array_t = ffi.typeof("int[?]") local array = ffi.new(int_array_t, 100) |
填充内存。调用格式: ffi.fill(dst, len [,c])。 类似于C函数 memset(dst, c, len)
拷贝内存。调用格式:
1 2 3 4 |
-- src作为const void *看待 -- dst作为void *看待 ffi.copy(dst, src, len) ffi.copy(dst, str) |
将指定的值强制转换为cdata。调用格式: cdata = ffi.cast(ct, init)
cdata的初始值从init得到,使用C类型转换约定。示例:
1 2 |
local c_str_t = ffi.typeof("const char*") local c_str = ffi.cast(c_str_t, "Hello") |
注册一个析构函数(finalizer)。调用格式: cdata = ffi.gc(cdata, finalizer)
示例:
1 2 3 4 5 |
-- 返回注册了析构函数之后的cdata local p = ffi.gc(ffi.C.malloc(n), ffi.C.free) p = nil -- 消除最后一个引用 -- 下一个GC周期中,会自动调用ffi.C.free(p) |
获取上一次C函数调用的error。调用格式: err = ffi.errno([newerr])。如果指定newerr,则返回旧error,并将error设置为新值。
从指针创建Lua字符串。调用格式: str = ffi.string(ptr [,len])
其中ptr为C指针。如果:
- 省略len,则ptr自动作为char*看待,也就是\0结束的字符串
- 否则,ptr被作为void*看待
每当Lua函数被转换为C函数指针时,LuaJIT FFI 都会自动创建特殊的callback。这种情况可以:
- 隐式发生,例如在把Lua函数传递给C函数的函数指针参数时
- 显式发生,你可以调用 ffi.cast()来强制转换Lua函数为C函数指针
目前,仅仅特定的C函数类型可以作为用作回调。
FFI为回调提供了两个特殊方法。
释放和回调相关的资源,关联的Lua函数将可以被GC。对应的函数指针不再有效,不能再被调用。
将回调关联到一个新的Lua函数,此回调的C类型、回调的函数指针保持不变。
不管隐式创建的cdata,还是ffi.new(), ffi.cast()显式创建的cdata,都可以被垃圾回收。使用cdata时,你必须确保在Lua栈、upvalue或Lua table上保留对cdata的有效引用,一旦最后一个引用失效,则在下一个GC周期中cdata会被自动释放。
如果你要分配一个数组类型的cdata给一个指针的话,必须保证cdata有额外的引用:
1 2 3 4 5 6 7 8 9 |
ffi.cdef[[ typedef struct { int *a; } foo_t; // 包含指针成员的结构 ]] local s = ffi.new("foo_t", ffi.new("int[10]")) -- 错误 local a = ffi.new("int[10]") -- 正确,自己持有cdata local s = ffi.new("foo_t", a) -- 然后传递给C指针 -- 直到不需要了,你才能销毁引用a |
在LuaJIT中调用C函数非常的方便,例如:
1 2 3 4 5 6 |
local ffi = require("ffi") ffi.cdef [[ int printf(const char *fmt, ...); ]] -- 标准C库不需要ffi.load,可以直接使用 ffi.C.printf("Hello %s!", "world") |
更加复杂的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
-- 调用zlib库的例子 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); ]] -- 不同操作系统中库的名称不一样 -- 将C共享库绑定到名字空间zlib local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z") -- 压缩缓冲区txt local function compress(txt) -- 将缓冲区长度传递给zlib local n = zlib.compressBound(#txt) -- 创建缓冲区cdata。uint8_t[?]为变长数组,其实际长度为n local buf = ffi.new("uint8_t[?]", n) -- C语言中可以传递变量地址(指针),但是Lua中没有对应的东西 -- 因此只能使用长度为1的数组代替之 local buflen = ffi.new("unsigned long[1]", n) -- 执行压缩 -- buflen在C函数中是一个指针,实际使用buf的长度,需要通过buflen传递回来 local res = zlib.compress2(buf, buflen, txt, #txt, 9) assert(res == 0) -- 从指定的C缓冲区创建Lua字符串,长度为buflen[0] return ffi.string(buf, buflen[0]) end |
cdata用于保存任何C类型到Lua变量中。这种变量实际上指向一块原生的内存区域。除了赋值和相同性判断,LuaJIT没有为cdata预定义任何的操作。
你可以通过元表为cdata自定义一组操作,并调用这些操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
local ffi = require("ffi") ffi.cdef [[ // 定义一个C结构 typedef struct { double x, y; } point_t; ]] -- 一个元表 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方法 area = function(a) return a.x * a.x + a.y * a.y end, }, } -- 为ct设置元表,返回设置后的ctype -- ctype = ffi.metatype(ct, metatable) local point = ffi.metatype("point_t", mt) local a = point(3, 4) print(a.x, a.y) -- 3 4 print(#a) -- 5 print(a:area()) -- 25 local b = a + point(0.5, 8) print(#b) -- 12.5 |
注意:元表与 C 类型的关联是永久的,而且不允许被修改,__index 元方法也是 。
C | Lua |
x = *p | x = p[0] |
*p = y | p[0] = y |
x = p[i] | x = p[i] |
p[i+1] = y | p[i+1] = y |
x = a[i] | x = a[i] |
a[i+1] = y | a[i+1] = y |
x = s.field | x = s.field |
s.field = y | s.field = y |
x = sp->field | x = sp.field |
sp->field = y | s.field = y |
y = p - i | y = p - i |
x = p1 - p2 | x = p1 - p2 |
x = &a[i] | x = a + i |
LuaRocks是一个包管理器,它可以创建、安装Lua模块,将其打包为自包含的包,这种包称为rocks。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
wget https://luarocks.org/releases/luarocks-2.4.3.tar.gz tar zxpf luarocks-2.4.3.tar.gz cd luarocks-2.4.3 ./configure --prefix=/home/alex/Lua/sdk/5.3.4 # 将LuaRock作为一个Rock安装 make bootstrap # 查看环境变量,将其拷贝到~/.profile中 luarocks path # 配合LuaJIT安装 export PATH=/home/alex/Lua/LuaJIT/2.0.5/bin:$PATH ./configure --prefix=/home/alex/Lua/LuaJIT/2.0.5 --lua-suffix=jit \ --with-lua-include=/home/alex/Lua/LuaJIT/2.0.5/include/luajit-2.0 |
示例:
1 |
luarocks install luasocket |
Rock会被安装到:/home/alex/Lua/sdk/5.3.4下。
Leave a Reply