Lua程序设计
Table of Contents
1. 第一部分 基本语法(C1-C10)
1.1. 类型,表达式,语句
Lua有8种基础类型,通过函数 `type` 来了解具体类型
- nil(无效值)
- boolean(true/false)
- number(整数或者是双精度浮点)
- string
- userdata(自定义类型)
- function
- thread(线程)
- table
如果要写入长字符串的话,可以使用下面这种格式.
s = [[this is a very long string. could be multiple lines]]
获得字符串长度以及table的大小,都可以使用 `#var` 这样的写法。
table既可以认为是一个dict, 也可以认为是array, 这点和Javascript很像。 作为字典的话可以可以使用类似属性的方式 `a.x = 10; a["x"] = 10 ` 进行访问。 此外Javascript也只有数字类型而不区分整形和浮点。需要注意的是lua数组通常以1作为起始索引。
有两个方法可以用来编译表格: ipairs和pairs. 其中ipairs从1开始遍历知道data[i] = nil结束, 而pairs则是真正遍历里面所有的keys.
printf = function(s, ...) return io.write(string.format(s,...)); end a = {[1] = 10, [2] = 20, [10] = 10, x = 30, y = 40} print("using ipairs to iterate") for i, v in ipairs(a) do printf("i = %d, v = %d\n", i, v); end print("using pairs to iterate") for k in pairs(a) do print("k = " .. k); end print(#a) -- [[ ➜ playbook lua test.lua using ipairs to iterate i = 1, v = 10 i = 2, v = 20 using pairs to iterate k = x k = 1 k = 2 k = 10 k = y 2 -- ]]
table constructor(table构造式)很有趣,同时兼容key/value和array的构造。
days = {'Sun', 'Mon', 'Tue'} -- 数组构造,下标从1开始 point = {x = 10, y = 20} -- 字典构造 days_and_point = {'Sun', 'Mon', 'Tue', x = 10, y = 20} -- 混合构造,毕竟数组下标只是一个key而已 -- 此外支持表达式做key. {[expr] = value} days = {["*"] = mul} -- days["*"] 来引用 days = {[ 0 ] = 20} -- days[0] 来引用
多变量赋值时,如果没有匹配上的话,那么剩余的变量自动匹配到 nil. 多余的自动忽略。 或者是如果直接声明 `local a` 的话,那么 `a` 的默认值也是 nil. 整个lua环境对 nil 有非常特殊的处理。
块(block)(通常是do-end部分)是规定了local(局部)变量的作用范围。常见控制结构有
- if then(else/elseif) end
- while do … end
- repeat … until
- for var=exp1, exp2, exp3 do … end(数字型for, numeric for)
- 如果exp2很大的话可以用 `math.huge` 来表示无线循环
- `var` 作用域仅限于这个block,不要对 `var` 做任何赋值
- 例子: for i=1,10,2 do; print(i); end;
- for var1, var2 in func do … end(泛型for, generic for) 这个在上面ipairs和pairs例子使用到了。
- break和return必须是block的最后一条语句。所以如果想提前返回的话,那么可以把break/return包装在 do .. end 中间另起一个block.
1.2. 函数/深入函数
函数调用形参和实参的绑定和多变量赋值很像:如果实参不够,那么以nil代替;如果实参太多,那么就被丢弃。
function hello(a, b) print("a = " .. tostring(a) .. ", b = " .. tostring(b)) end hello(10) hello(10, 20) hello(10, 20, 30) -- [[ > dofile("test.lua") a = 10, b = nil a = 10, b = 20 a = 10, b = 20 -- ]]
lua的函数定义和scheme很像,默认地都是匿名函数,至于 `function a()` 不过是 `a = function()` 这种语法糖形式。
函数调用中比较有意思的是,如果只有一个参数并且该参数是字符串或者是table构造式的话,可以省略 `()`. 这样的话写出来就非常漂亮。
print 'hello, world' a, b = table.unpack{10, 20} print(a, b)
多值和列表/元组本身还是存在一定差异的,虽然他们逻辑结构类似。比如我们可以直接将多值传入函数作为参数,这和传入列表/元组到函数是不同的。 我们可以用 `table.unpack` 函数将列表拆分成为多值,然后传入到函数。这里我们模拟 `table.unpack` 给出了一个自己的unpack实现.
function hello(a, b) print("a = " .. tostring(a) .. ", b = " .. tostring(b)) end -- 一种table.unpack的实现方法 function unpack(t, i) -- 如果没有那么多参数的话,那么i=nil i = i or 1 if t[i] then return t[i], unpack(t, i+1) end end params = {10, 20} hello(params) hello(unpack(params))
变长参数在C语言里面需要花费很大的力气才能解开,但是lua里面使用却很容易。 `…` 都被复制给了arg, 其中arg.n表示参数个数
function test_vargs(a, b, ...) print('a = ' .. a .. " , b = " .. b) for i = 1, select('#', ...) do print('varg #' .. i .. " = " .. select(i, ...)) end end test_vargs(10,20, table.unpack{30, 40 , 50}) -- [[ arg是一个空table. 没有绑定到...上. 非常奇怪 function test_vargs(a, b, ...) print('a = ' .. a .. " , b = " .. b) for i, v in ipairs(arg) do print('varg #' .. i .. " = " .. v) end end -- ]]
Lua本身并不支持具名实参 `named arguments`. 但是有个workaround, 就是传入table/字典
function get_named_args(args) keys = {"height", "width", "depth"} for i, k in ipairs(keys) do local arg = args[k] print(k .. ' = ' .. arg) end end get_named_args({height = 100, width = 200, depth = 50})
table.sort支持对table进行排序,可以传入一个匿名参数比较两个key. 对于数组默认就是升序排序。
nums = {5,4,3,2,1} table.sort(nums, function(a,b) return a < b end) for i, v in ipairs(nums) do print(v) end
变量可以是局部的,那么函数也可以是局部的。局部函数有一些限制,但是可以通过提前声明来解决
do local fact; fact = function(n) if (n == 0) then return 1 end return n * fact(n-1) end print(fact(10)) end
1.3. 迭代器与泛型for
泛型for的执行过程如下:
下面我们看看范性for的执行过程:
- 首先,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数,状态常量和控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。
- 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
- 第三,将迭代函数返回的值赋给变量列表。
- 第四,如果返回的第一个值为nil循环结束,否则执行循环体。
- 第五,回到第二步再次调用迭代函数。
for var_1, ..., var_n in explist do block end -- 等价于下面这样的形式 do local _f, _s, _var = explist while true do local var_1, ... , var_n = _f(_s, _var) _var = var_1 if _var == nil then break end block end end
可以看到我们有几个东西可以控制:
- f. 生成函数,根据s, var来产生新的值
- s. 状态,这个需要在生成函数里面更新。尽可能地不要涉及状态。
- var. var1比较特殊,会进行生成函数,其他value则只用于访问数据。var1可以用来做简单的状态控制,比s这种类似table代价要小。
基本上这种迭代器都可以通过闭包和状态变量控制搞定,除非是复杂的状态机需要维持一个s. 下面是ipairs和values的实现, 其中ipairs把这几个东西都用上了,而values只使用了最简单的闭包。
function values(t) local i = 0 return function() i = i + 1 return t[i] end end function my_ipairs(t) local iter = function(t, i) i = i + 1 if t[i] then return i, t[i] end end return iter, t, 0 end print('========== values ==========') for v in values({10, 20, 30, 40}) do print(v) end print('========== my_ipairs ==========') for i, v in my_ipairs({10, 20, 30, 40}) do print(i, v) end
1.4. 编译执行与错误
`loadstring` 可以载入外部代码, `loadfile` 可以载入代码文件。两者都会编译代码,并且返回local function对象。 只有执行这个function对象代码才会变真正执行,在执行的时候也是可以传入参数(但是这个参数可能只能通过比较特殊手段拿到)
`dofile` 执行的是 `f = loadfile(file_path); f() ` ,所以每次都会去编译代码文件。
`require` 函数用于导入模块,类似python里面的import语句。模块查找路径是由环境变量LUA_PATH控制的。
f = loadstring("i = i + 1") -- 等价于下面的形式 f = function() i = i + 1 end
这样得到的local function只能访问到两处的变量:1. 全局变量 2. local function内部变量。所以它不是通常意义上的词法作用域(lexical scoping).
i = 10 function f() local i = 20 f2 = loadfile("test2.lua") -- i = i + 1 f2() print("f.i = " .. i) end f() print("global i = " .. i) -- [[ 结果如下 ➜ playbook lua test.lua f.i = 20 global i = 11 -- ]]
`package.loadlib` 可以载入C代码(动态加载)。这个函数不是标准ANSI C的实现,但是因为这个函数太重要的,所以lua在每个平台上都有特定实现。 同样这个函数只是将动态库加载进来(需要传入动态库路径和初始化函数名称),返回一个function对象。
下面几个函数涉及到错误处理:
- `errro("error message")` 汇报错误;(产生异常)
- `assert` 做断言;
- `pcall` 可以在保护模式(protected mode下面)调用函数,分别(true, value) | (nil, error);(捕捉异常)
- `xpcall` 传入调用函数和错误处理函数
- `debug.traceback` 可以打印出错堆栈
下面的代码把这几个函数都串起来了
printf=function (fmt, ...) print(string.format(fmt, ...)) end function my_func(v) if v == 10 then error("value == 10") else return "good" end end function test_pcall() print("========== test_pcall ==========") local bad_func = function () print('calling bad_func') return my_func(10) end local good_func = function () print('calling good_func') return my_func(20) end local status, err = pcall(bad_func) printf("status = %s, err = '%s'", tostring(status), tostring(err)) local status, value = pcall(good_func) printf("status = %s, value = %s", tostring(status), tostring(value)) end function test_xpcall() print("========== test_xpcall ==========") local bad_func = function () print('calling bad_func') return my_func(10) end local good_func = function () print('calling good_func') return my_func(20) end local on_failed_fn = function(err) printf("on failed. err = '%s'", tostring(err)) print(debug.traceback()) end xpcall(bad_func, on_failed_fn) xpcall(good_func, on_failed_fn) end test_pcall() test_xpcall() -- [[ output ➜ playbook lua test.lua ========== test_pcall ========== calling bad_func status = false, err = 'test.lua:5: value == 10' calling good_func status = true, value = good ========== test_xpcall ========== calling bad_func on failed. err = 'test.lua:5: value == 10' stack traceback: test.lua:41: in function <test.lua:39> [C]: in function 'error' test.lua:5: in function 'my_func' (...tail calls...) [C]: in function 'xpcall' test.lua:44: in function 'test_xpcall' test.lua:49: in main chunk [C]: in ? calling good_func -- ]]
1.5. 协同程序(coroutine)
coroutine的几个相关操作
- co = coroutine.create(func)
- coroutine.resume(co, …) 让co继续执行
- 初始阶段传入参数,被传入 `func`
- 返回值(ok, `yield` 传入的参数)
- coroutine.yield 传入的参数被 `resume` 返回,只能在co里面调用
- coroutine.status 查询co的状态
- suspended 挂起
- running 运行
- dead 死亡
- normal 正常
lua coroutine应该是stackful coroutine, coroutine.yield只能返回caller. stackless coroutine 则更具有灵活性,能够yield到任何其他coroutine上面。但是我没有想到有什么场景是一定需要stackless而非stackful的。
书里面producer/consumer的例子改写成为coroutine方式如下。我稍微改动了一些代码为了更好地理解coroutine. 为了能让"quit"能最后一次回显,producer不能立即退出,必须等待consumer再次请求,并且上次command=="quit"的时候才能退出。
producer = coroutine.create( function() local send = function(x) last_command = coroutine.yield(x) return last_command end while true do local x = io.read() last_command = send(x) if last_command == 'quit' then break end end end ) function consumer() local last_command = nil local receive = function(x) local status, value = coroutine.resume(producer, last_command) last_command = value return value end while true do local x = receive() if x == nil then break end io.write("ECHO ", x, "\n") end assert(last_command == nil) end consumer()
2. 第二部分 运行环境(C11~C17)
2.1. 数据结构/数据文件与持久化
lua在函数单参数调用的时候不要求加上(), 这样可以非常适合DSL,比如类似BibTex这样的格式。 我们在外部定义好环境(函数),然后就可以使用 dofile 函数将数据直接载入进来。
-- main.lua function Entry(t) fields = {"author", "book_name", "publisher", "year"} print("----- Entry -----") for i, v in ipairs(fields) do print(string.format("%s = %s", v, tostring(t[i]))) end end dofile("datafile") -- datafile Entry{"Donald E. Knuth", "Literate Programming", "CSLI", 1992} Entry{"Jon Bentley", "More Programming Pearls", "Addison-Wesley", 1990} -- [[ output ➜ playbook lua test.lua ----- Entry ----- author = Donald E. Knuth book_name = Literate Programming publisher = CSLI year = 1992 ----- Entry ----- author = Jon Bentley book_name = More Programming Pearls publisher = Addison-Wesley year = 1990 -- ]]
2.2. 元表和元方法
元表(metatable)本质上是一个table,我们可以在这个table里面设置,然后来影响和扩展使用这个metatable的table的行为。 在Lua代码里面只能设置table的metatable, 其他类型的metatable的设置只能在C代码里面完成。
下面代码片段说明了metatable的使用
- `_m` 是 `make_obj` 里面对象o的metatable
- __tostring 函数影响到如何输出这个对象
- __add 函数影响到如何叠加两个对象
- __index 函数影响到如何查找某个不断在的字段
- rawget 可以不理会 __index 这个函数, rawset 可以不理会 __newindex这个函数.
因为 __index 使用非常频繁,所以lua允许 __index还可以是一个table对象。如果是table对象而非函数的话,那么直接在这个table对象里面查找。
除了 __index 之外,还有个 __newindex 函数是影响如果某个字段不存在,如何给这个字段赋值。所以可以结合 __index 和 __newindex 两个函数,来实现追踪table的读写。
local _m = { __tostring = function () return o.c end, __add = function (a, b) return a.c + b.c end, __index = function (t, k) -- t是调用对象,而非metatable print(t == obj1, t == obj2, t == _m) print('request key = ' .. k) if k == 'e' then return 10 else return 20 end end } local function make_obj(c) o = {c = c} setmetatable(o, _m) return o end local function inspect_obj(o) for k,v in pairs(o) do print('key = ' .. k .. ', value = ' .. v) end end obj1 = make_obj(10) obj2 = make_obj(20) print(obj1 + obj2) inspect_obj(obj1) print(obj1.e, obj1.f) print(rawget(obj1, 'e'), rawget(obj1, 'c')) -- [[ output ➜ workspace lua test.lua 30 key = c, value = 10 true false false request key = e true false false request key = f 10 20 nil 10 -- ]]
这里我在摘抄两个书里面的代码片段,一个是实现Set数据结构,一个是实现代理类(可以监控字段的读写)。因为我觉得这两个例子里面有点启发性。
Set数据结构的启发性是:
- 任何数据结构类型是一个表,里面包含类字段,以及mt(表示metatable)
- 在new方法里面创建另外一个实例表
- 在实例表里面设置metatable,设置为类里面的mt字段
- 这样所有实例的 getmetatable(t) == cls.mt
Set = {} -- 数据结构类 Set.mt = {} -- 类的metatable -- 类字段 Set.version = "1.0.0" Set.name = "yan" function Set.new(t) local inst = {} for i, v in ipairs(t) do inst[v] = true end setmetatable(inst, Set.mt) return inst end Set.mt.__tostring = function (obj) local tmp = {} for k in pairs(obj) do table.insert(tmp, k) end return "{" .. table.concat(tmp, ",") .. "}" end Set.mt.__add = function(a, b) local tmp = Set.new({}) assert (getmetatable(a) == Set.mt) assert (getmetatable(b) == Set.mt) for k in pairs(a) do tmp[k] = true end for k in pairs(b) do tmp[k] = true end return tmp end a = Set.new({1,2,3,4}) b = Set.new({5,6,7}) print("a = " .. tostring(a)) print("b = " .. tostring(b)) c = a + b print("c = " .. tostring(c))
代理类(proxy)的启发性是,任何对象都可以作为table的key,另外这个代理类也非常容易使用
local pk = function () end -- 使用函数对象可以作为key. -- local pk = {} -- 或者是空表(空表地址) function track(t) local proxy = {} proxy[pk] = t setmetatable(proxy, mt) return proxy end mt = { __index = function(proxy, k) t = proxy[pk] print(string.format("access table %s field %s ...", tostring(t), tostring(k))) return t[k] end } t = { a= 10, b = 20} pt = track(t) print(pt.a) for k in pairs(pt) do print(k) end -- [[ 扫描下面所有的keys的话,这个pk还是可以看到的 > dofile("test.lua") access table table: 0x7fc62c407030 field a ... 10 function: 0x7fc62c402bd0 -- ]]
2.3. 环境
Lua所有的全局变量都保存在一个table里面,这个table称为环境(environment). 可以使用 `_G` 来获得环境。结合上面元表(metatable)和元方法(metamethod), 可以做蛮多事情的。
此外我们还可以使用 `setfenv(current_stack, env)` 来改变执行函数的环境,其中current_stack=1表示当前函数,2表示上一层函数,以此类推。另外current_stack还可以传递函数对象, 这样在执行函数的时候会使用到这个环境。
2.4. 模块与包
模块可以通过 `require` 来加载。加载模块会有返回值,这个由模块来定义的,通常返回的是一个table.
加载模块搜索路径存放在 `package.path` 里面,这个路径可以通过 LUA_PATH 环境变量控制。当loader没有办法找到对应Lua模块的时候,会去寻找C模块。 C模块对应的路径分别是 `package.cpath` 和 `LUA_CPATH`.
一旦模块加载上来后,就会在 `package.loaded` 里面创建一个条目,之后再遇到 `require` 的话就从这里面读取。所以如果希望重新加载的话,可以将里面条目置nil.
模块在编写上有许多技巧,似乎都比较复杂。下面我汇编了个可以work的boilerplate (copy from here)
-- /usr/bin/env lua -- coding:utf-8 -- Copyright (C) dirlt 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_square(self) return self.width * self.height end function _M.get_circumference(self) return (self.width + self.height) * 2 end return _M
在调用的时候如下
local rect = require 'kv' -- 上面module命名为kv.lua obj = rect:new(10, 20) print(obj:get_square(), obj:get_circumference()) for k in pairs(obj) do print(k) end
2.5. 面向对象编程
为了方便对象引用,lua引入一个新语法 `:` ,实际上是一个语法糖 `a:method(x, y, z)` = `a.method(a, x, y z)` 。在函数体内可以使用 `self` 关键字引用到调用对象。
下面是书中Account(账号)实现代码:
- Account 是个类(class),字段 `balance` 默认值是0
- `account` 是个对象(instance), `new` 出来的时候并没有 `balance` 字段
- 第一次调用 `add_balance` 的时候, `account` 对象里面才创建了 `balance` 对象
Account = {balance = 0} function Account:new (o) -- same as Account.new(self, o), self = Account o = o or {} setmetatable(o, self) self.__index = self -- 这样可以找到类字段 return o end function Account:add_balance(value) self.balance = self.balance + value return self end account = Account:new() print(rawget(account, 'balance'), account.balance) -- nil, 0 account:add_balance(10) account:add_balance(20) print(account.balance)
实现SubAccount继承于Account. 在 SubAccount:new 函数里面注意:
- setmetatable(o, SubAccount)
- SubAccount.__index= SubAccount
所以使用self好处就是,子类继承并且调用方法的时候,self可以替换成为子类。
SubAccount = Account:new() function SubAccount:add_level(value) self.level = self.level + value return self end sub_account = SubAccount:new({level = 10}) sub_account:add_balance(10) sub_account:add_level(50) print(sub_account.balance, sub_account.level)
如果是多重继承的话,需要修改 `setmetatable(o, self)` 这段代码,需要传入所有的parent class, 然后在 `__index` 里面查找所有parent class. 书里面给了例子,我觉得写起来还挺有技巧性的,所以复制一份代码放在这里。
local function search(k, plist) for i = 1, #plist do local v = plist[i][k] if v then return v end end end function createClass(...) local c = {} local parents = { ... } setmetatable(c, {__index = function(t, k) return search(k, parents) end}) c.__index = c function c:new(o) o = o or {} setmetatable(o, c) return o end return c end
2.6. 弱引用table(weak table)
一个table里面所有的keys和values都是存在引用的,所以它们永远都不会被释放(除非这个table被释放了)。但是如果这些key, value只是对外界对象的引用, 而table本身并不关心这些key, value的存在与否(或者它只是一个lookup结构的话),那么就可以将table设置成为weak table.
注意弱引用仅仅对于table/function有用,对于number/string是没有用的。你可以认为1, "hello"这样的对象永远不会被GC.
将普通的table变成弱引用table的方式是修改metatable. `{__mode = 'k'}` 说明key是弱引用, `{__mode = 'v'}` 说明value是弱引用, 'kv'的话说明就是key,value弱引用。 弱引用的效果是:如果弱引用的对象在外部没有引用的话,那么这key/value就会从这个table里面删除。以下面代码为例
t = {} mt = getmetatable(t) if mt == nil then mt = {} setmetatable(t, mt) end mt.__mode = "k" -- 设置成为弱引用 key = function() end t[key] = 10 key = function() end -- 原来的key被gc, 所以(key, 10)这个entry会被删除 t[key] = 20 -- 现在还有两个entries. print('===== before gc =====') for k in pairs(t) do; print(k, t[k]); end print('===== after gc =====') collectgarbage() -- 现在只有(key, 20)这个entry for k in pairs(t) do; print(k, t[k]); end
书里面给了两个weak table使用的场景:
- 记忆函数(memoiziation). 如果value对象不再被引用,那么我们可以从table里面释放掉。那么我们可以设置table为weak value table.
- 关联对象属性. 我们想知道某个对象的属性,比如数组长度等。这些属性是和对象关联起来的,一旦对象释放,我们也没有必要保留这些属性。
所以我们可以设置table为weak key table. 其中key为关联对象,value为关联属性。
3. 第三部分 标准库(C18~C23)
Lua各种库的使用方法。书里面介绍了下面这些库
- 数学库 math
- 表格库 table
- 字符串库 string
- IO库 io
- 操作系统库 os
- 调试库 debug
最后这个调试库debug比较有意思。这个库并没有提供一个Lua调试器,只是提供了一些primitives, 使用这些primitives可以来完成调试功能。primitives可以分为两类:
- 自省函数(introspective function).
- 调用调试库的栈层stack level = 1
- `debug.getinfo`, 某个栈层的函数信息
- `debug.getlocal` 某个栈层的局部变量
- `debug.getupvalue` 某个函数的非局部变量(closure里面包含的变量)
- 钩子(hook).
- 在函数调用和返回处会调用钩子函数
- `debug.sethook` 参数包括回调函数,监控事件,以及可选数字指定多久获得一次事件
注意这些primitives的性能并不高,Lua以一种不会影响程序正确执行的方式来保存调试信息而已。所以在production环境下面这些调试语句最好需要去除掉。
4. 第四部分 C API(C24~C31)
如何将Lua和C混合编程,包括用C扩展Lua以及在C里面调用Lua代码。
lua解释器的全部状态都存储在lua_State对象里面。Lua和C之间的交互,是通过栈(stack)来完成的。
API用索引来访问栈中的元素。在栈中的第一个元素(也就是第一个被压入栈的)有索引1,下一个有索引2,以此类推。 我们也可以用栈顶作为参照来存取元素,利用负索引。在这种情况下,-1指出栈顶元素(也就是最后被压入的),-2指出它的前一个元素,以此类推。 例如,调用lua_tostring(L, -1)以字符串的形式返回栈顶的值。我们下面将看到,在某些场合使用正索引访问栈比较方便,另外一些情况下,使用负索引访问栈更方便。
通过栈来交互数据有两个考虑:
- 是否可以很容易地接入其他语言比如Java, C#.
- 因为Lua是有垃圾收集的,如果使用栈来保存交互数据的话,那么可以追踪到活跃对象。
使用栈来交互数据并不是LuaVM才这么做的,JVM也是stack-based VM,Scala/Kotlin都可以和Java语言来做交互。
下面是一个在C里面调用lua解释器的示例代码:
- lua.h 是 lua解释器的头文件,包含基本函数和数据结构。
- lualib.h 是 lua自带库的头文件,最主要的就是调用 luaL_openlibs.
- lauxlib.h 是 对lua解释器封装的头文件,以luaL开头,大部分使用这个库就行。
#include <stdio.h> #include <string.h> #include <lualib.h> #include <lauxlib.h> const char *text = " \ function add(x, y) \ return math.sin(x) + math.sin(y) \ end \ print(add(10, 20)) \ "; int main() { int error; lua_State* L = luaL_newstate(); luaL_openlibs(L); error = luaL_loadbuffer(L, text, strlen(text), "test_lua::") || lua_pcall(L, 0, 0, 0); if (error) { fprintf(stderr, "%s", lua_tostring(L, -1)); lua_pop(L, 1); } lua_close(L); return 0; }
SRC=$(HOME)/utils/lua-5.3.5/src test_lua:test_lua.c gcc test_lua.c -I$(SRC) -L$(SRC) -llua
如果运行出错的话,那么会出现类似下面这样的错误
➜ playbook ./a.out [string "test_lua::"]:1: attempt to call a nil value (field 'sinx')
下面是一个C提供扩展函数的示例代码:
#include <stdio.h> #include <string.h> #include <lauxlib.h> static int l_add(lua_State* L) { double a = luaL_checknumber(L, -1); double b = luaL_checknumber(L, -2); lua_pop(L, 2); lua_pushnumber(L, a + b); return 1; } static const struct luaL_Reg funcs[] = { {"l_add", l_add}, {NULL, NULL}, }; int luaopen_mylib(lua_State* L) { lua_newtable(L); luaL_setfuncs(L, funcs, 0); return 1; }
SRC=$(HOME)/utils/lua-5.3.5/src test_lua.so:test_lua.c gcc test_lua.c -I$(SRC) -L$(SRC) -fPIC -shared -o $@ -llua
local open_mylib, err = package.loadlib("test_lua.so", "luaopen_mylib"); if (err ~= nil) then print(err) os.exit(1) end local mylib = open_mylib() print(mylib.l_add(10, 20));
用户自定义类型可以设置 `__gc` 字段,这个字段对应的函数会在对象被Lua执行GC的时候调用。
Lua允许在分配解释器状态 lua_State 使用自定义的内存分配函数
垃圾收集在5.0之前使用的是mark-and-sweep(stop-the-world)垃圾收集器,每个周期分为4个阶段:
- 标记(mark): 从根集合找到所有活跃的对象并且标记下来。
- 整理(cleaning):
- 找到未被标记对象并且有_gc字段的userdata单独保存下来(这个在回收阶段需要调用回调函数)
- 遍历所有弱引用table,根据选项删除里面没有被标记的key/value
- 清扫(sweep): 释放内存
- 收尾(finalization). 调用阶段2收集到的特殊userdata回调函数
Lua在5.1之后开始使用增量式的收集器:GCSTEP就是增量收集器标志。调用增量收集器有几个时机:
- GCSTEPPAUSE. 一轮结束之后假设我们正在使用m字节的内存,那么等待到使用 m * pause 字节再出发下一轮。
- GCSTESTEPMUL. 根据内存分配的速度假设是x bytes/s, 那么垃圾回收速度需要控制在 x * step bytes/s左右。
此外gc函数还可以临时地关闭gc和打开gc, 这样对某些延迟敏感的应用可以很好地控制延迟。
#+BEGIN_SRC C /*
5. garbage-collection function and options
*/
#define LUA_GCSTOP 0 #define LUA_GCRESTART 1 #define LUA_GCCOLLECT 2 #define LUA_GCCOUNT 3 #define LUA_GCCOUNTB 4 #define LUA_GCSTEP 5 #define LUA_GCSETPAUSE 6 #define LUA_GCSETSTEPMUL 7 #define LUA_GCISRUNNING 9
LUA_API int (lua_gc) (lua_State *L, int what, int data);
#+END_SRC