记录开发中实际涉及的lua知识
更新于2022-12-17
使用Docker构建交叉编译lua文件的容器
假设我们需要使用aarch64-poky-linux的toolchain去编译一些lua源码文件,那我们首先要获得aarch64架构的lua和luac。
luac
luac是lua编译器,和传统意思上的编译器不同的是,Luac只是将源码明文件转换成二级制文件,虽然这个二进制文件不能直接运行还是得转换成字节码才能运行,但是有几点好处:
- 虽然代码最终执行速度不会提升,但是加载的速度快了,最终还是表现为程序执行的快了。
- 明文源码编译后起到了保护source code的作用。
- 在编译时完成了语法检查,避免了一些在最终执行才暴露的问题。
还有一点很重要,只要硬件平台架构的字节顺序和宽度一样,编译出来的二级制文件可以通用,每个编译出来的二级制文件在开头都有记录字节宽度和顺序的chunk。
源码编译Lua得到luac
- 下载lua5.3.3的源码
- 下载lua的依赖源码, readline-8.0.tar.gz 和 ncurses-6.2.tar.gz
- 先交叉编译两个依赖库的代码,假设分别安装在
/opt/readline_install64
和/opt/ncurse_install64
目录下 - 然后设置lua源码根目录下的build.sh,支持交叉编译环境
- 然后在src/Makefile中添加如下: ``` MYCFLAGS= -I/opt/readline_install64/include -I/opt/ncurse_install64/include MYLIBS= -L/opt/readline_install64/lib -L/opt/ncurse_install64/lib
* 然后执行交叉编译,生成aarch64-arm版本的lua和luac
## 构建Dockerfile
---
luac准备好了之后,我们需要构建可以运行aarch64-arm程序的环境容器, 主要是安装qemu。
FROM ubuntu:18.04
RUN apt-get update && \
apt-get install -y libc-dev-armhf-cross qemu-user-static libc6-dev-arm64-cross gcc-aarch64-linux-gnu python openssl
COPY aarch64_libs/* /lib/
COPY lua /bin/
COPY luac /bin/
WORKDIR /build
```
然后执行docker build -t lua-image .
创建image, 然后执行docker run -i -v xxx:/build lua-image bash -c "qemu-arm-static /bin/luac yyy.lua -o yyy"
BackGroud
Lua是巴西里约热内卢天主教大学于1993年开发的,其设计目的是为了嵌入应用程序中,从而为应用程序提灵活的扩展和定制功能。
Lua和python一样都是由C编写而成,但是不同于python,Lua没有提供强大的库,这是由于它的定位决定的,所以Lua不适合作为开发独立应用程序的语言。
Lua有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。
基本语法
Tables
table是lua中唯一的一个原生数据结构,它是基于hash-lookup dicts实现的,所以非常的灵活,可以很方便的将table当成字典、map、list等使用。
Using table as list / array
用lua中的table实现的list / array 要比python中的list和c语言中的array灵活很多。
v = {'monday', 99, 'value2', 1.21}
for i = 1, #v do -- #v is the size of v for lists.
print(v[i])
end
Using table as dictionary / map
使用lua中的table实现字典或者map时,要注意和上面实现的list在访问元素上的区别。
t = {key1 = 'value', key2 = false, key3 = 2.2}
-- 只要key是字符串,就可以通过dot notation访问value
print(t.key1)
t.newkey = {} -- 添加newkey = {}
t.key2 = nil -- 从table中删除key2
lua中除了默认使用字符串作为字典的key,还可以使用任何非nil值作为key:
u = {['@!#'] = 'qbert', [{}] = 666, [2.68] = 'saiyn'}
print(u[2.68]) -- prints "saiyn"
特性
Metamethods
lua通过metatable和metamethod特性实现对lua对象方法的重载,而lua中的对象都是通过table实现的,所以所谓的重载就是操作table中的key。
lua中table已经默认包括__add
、__index
、__newindex
等等这样的key.
__index
__index
这个Key或者说这个metamethod的作用是,让你可以定义一个”fallback”函数作为缺省的value。
local func_example = setmetatable({}, {__index = function(t,k) -- {}定义了一个空对象
return "key doesn't exist"
end})
local fallback_tbl = setmetatable({ -- 定义了{foo = "bar", [123] = 456}对象
foo = "bar",
[123] = 456,
}, {__index = func_example})
local fallback_example = setmetatable({}, {__index = fallback_tbl})
print(func_example[1]) --> key doesn't exist
print(fallback_example.foo) --> bar
print(fallback_example[456]) --> key doesn't exist
__newindex
__newindex
这个metamethod可以让你添加一个新的key,value。
local t = {}
local m = setmetatable({}, {__newindex = function(table, key, value)
t[key] = value
end})
m[123] = 456
print(m[123]) --> nil
print(t[123]) --> 456
__metatable
如果设置了这个key,对应对象的整个metatable会变成只读。
协程(coroutine)
coroutine和thread相同点在于它们都是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时又与其他协程共享全局变量和其他大部分东西。但是它们的主要区别在于,一个具有多个线程的程序可以同时运行多个线程,而一个具有多个协程的程序在任意时刻只能运行一个协程,并且正在运行的协程只会在其显式地要求挂起(suspend)时,它才会暂停。
基本语法
lua将所有关于协程的函数放置在一个名为”coroutine”的table中。
create
函数create用于创建新的协程,它只有一个参数,就是协程需要执行的函数。它返回一个thread类型的值,用以表示新的协程。
co = coroutine.create(function() print("hi") end)
print(co) --> thread:0x8071d98
一个协程有4种不同的状态:挂起(suspended)、运行(running)、死亡(dead)和正常(normal)。当使用create创建一个协程时,它处于suspend状态。
print(coroutine.status(co)) --> suspended
resume
函数coroutine.resume用于启动或者再次启动一个协程的执行。
coroutine.resume(co) --> hi
print(coroutine.status(co)) --> dead
上面程序中,协程在简单地打印了”hi”后就终止了,然后它处于死亡状态,也就再也不会返回了。
yield
上面的例子看上去,协程只是像一种复杂的函数调用方法。其实协程的真正强大之处在于函数yield的使用上,该函数可以让一个运行中的协程挂起,而之后可以再恢复它的运行。
co = coroutine.create(function()
for i=1, 10 do
print("co", i)
coroutine.yield()
end
end)
现在,当唤醒这个协程时,它就会开始执行,直到第一个yield:
coroutine.resume(co) -->co 1
如果此时检查其状态,会发现协程处于挂起状态,因此可以再次恢复其运行:
print(coroutine.status(co)) -->suspended
coroutine.resume(co) --> co 2
...
coroutine.resume(co) --> co 10
coroutine.resume(co) -- 什么都不打印
在最后一处调用resume时,协程内容已经执行完毕,并已经返回。因此,这时协程处于死亡状态,如果再次恢复它的运行,resume将返回错误:
print(coroutine.resume(co))
--> false cannot resume dead coroutine
当一个协程A唤醒另一个协程B时,协程A就处于一个特殊状态,既不是挂起(无法继续A的执行),也不是运行状态(是B在运行)。所以将这时的状态称为“”正常”状态。
特性
lua的协程可以通过一对resume-yield来交换数据。在调用resume时,如果没有对应的yield在等待它,那么所有传递给resunme的额外参数都将视为协程主函数的参数:
co = coroutine.create(function(a,b,b)
print("co", a,b,c)
end)
coroutine.resume(co,1,2,3) --> co 1 2 3
在resume调用返回的内容中,第一个值为true则表示没有错误,而后面所有的值都是对应yield传入的参数:
co = coroutine.create(function(a,b)
coroutine.yield(a+b, a-b)
end)
print(coroutine.resume(co, 20, 10)) ---> true 30 10
与此对应的是,yield返回的额外值就是对应resume传入的参数:
co = coroutine.create(function()
print("co", coroutine.yield())
end)
coroutine.resume(co, 4,5) -- 没有打印
coroutine.resume(co, 4,5) --> co 4 5
最后,当一个协程结束时,它的主函数所返回的值都将作为对应resume的返回值:
co = coroutine.create(function()
return 6,7
end)
print(coroutine.resume(co)) -->true 6 7
进阶应用
function permutations(a)
local co = coroutine.create(function() permgen(a) end)
return function() --迭代器
local code, res = coroutine.resume(co)
return res
end
end
permutations函数使用了一种在lua中比较常见的模式,就是将一条唤醒协同程序的调用包装在一个函数中。由于这种模式比较常见,所以lua专门提供了一个函数 coroutine.wrap来完成这个功能。使用wrap改写上面代码如下:
function permutations(a)
coroutine.wrap(function() permgen(a) end)
end
类似于create,wrap创建了一个新的协同程序,但不同的是,wrap并不返回协同程序本身,而是返回一个函数。每当调用这个函数,即可唤醒一次协同协程。
与C语言交互
Lua是一种嵌入式语言。即Lua不是一个单独运行的程序,而是一个可以链接到其他程序的库。通过链接就可以将Lua的功能合并入这些程序。Lua和C语言之间一种最常见的交互方式是:
-
- C程序中使用Lua提供的C API编写一些特定模式的模块,然后注册到Lua中。
-
- 使用Lua编写功能代码,lua代码中可以调用之前注册进来的C函数。
-
- C程序中使用Lua提供的C API编译加载lua代码文件,运行代码。
从上面的交互过程可以看出,Lua确实是依附着C语言运行的。那么这就引出一个问题,为啥不直接运行C代码,而绕一个弯的去执行Lua代码?
这是因为Lua作为High-level语言,具备很多C语言没有的优势,比如:
- Lua支持一些高级特性,如function closures、垃圾回收等等。
- Lua比C语言安全。
- Lua is a dynamic language: it requires no “off-line” compilation.
将C模块注册到Lua中有多种方式,包括:
-
- 如果这个模块作为独立的库,那么可以编译成动态链接库
.so
,并将这个库放入C路径(LUA_CPATH)中。然后,便可以用require从Lua中加载这个模块。
- 如果这个模块作为独立的库,那么可以编译成动态链接库
-
- 将这个模块登记到Lua标准库中的linit.c文件中,然后从新编译整个Lua,这样调用luaL_openlibs函数时就可以打开这个模块。
-
- 这个C模块作为C程序的一部分,而这个C程序主体部分包含了加载lua文件执行的功能。这个方式也就是上面提到的最常见的C和lua交互方式的一部分。
C API概述
栈操作API
void lua_pushvalue(lua_State *L, int index);
lua_pushvalue函数会将指定索引上的副本压人栈。
table操作API
因为table操作是lua中主要的交互手段,而且往往比较抽象,所以下面以图解来说明table操作中堆栈的变化情况。
void lua_gettable(lua_State *L, int index)
lua_gettable会从栈中先弹出key,并压人相应的value。
void lua_settable(lua_State *L, int index)
lua_settable会先后压人key和value,设置完table后再pop出key和value。
数组操作API
API为数组操作提供了两个函数:
void lua_rawgeti(lua_State *L, int index, int key);
void lua_rawseti(lua_State *L, int index, int key);
key指定数组元素的下表,index指定table在栈中的位置。其实这两个函数操作堆栈的过程和上面两幅图示流程基本一样。
元表操作API
int lua_setmetatble(lua_State *L, int objindex)
lua_setmetatble函数会从栈中弹出一个table,并将其设为指定索引上对象的元表。
要注意和luaL_setmetatble函数的区别,前者操作的是函数的私有堆栈,后者操作的是全局的注册表。
void lua_setfield(lua_State *L, int index, const char *k)
lua_setfield函数完成一个t[k] = v
的操作,其中t是栈上index对应的值,v是栈顶值。示意图如下: