lua笔记

Jun 20, 2018


记录开发中实际涉及的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语言之间一种最常见的交互方式是:

    1. C程序中使用Lua提供的C API编写一些特定模式的模块,然后注册到Lua中。
    1. 使用Lua编写功能代码,lua代码中可以调用之前注册进来的C函数。
    1. 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中有多种方式,包括:

    1. 如果这个模块作为独立的库,那么可以编译成动态链接库.so,并将这个库放入C路径(LUA_CPATH)中。然后,便可以用require从Lua中加载这个模块。
    1. 将这个模块登记到Lua标准库中的linit.c文件中,然后从新编译整个Lua,这样调用luaL_openlibs函数时就可以打开这个模块。
    1. 这个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_0

lua_gettable会从栈中先弹出key,并压人相应的value。


void lua_settable(lua_State *L, int index)


lua_1

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,并将其设为指定索引上对象的元表。

lua_2


要注意和luaL_setmetatble函数的区别,前者操作的是函数的私有堆栈,后者操作的是全局的注册表。


void lua_setfield(lua_State *L, int index, const char *k)

lua_setfield函数完成一个t[k] = v的操作,其中t是栈上index对应的值,v是栈顶值。示意图如下:

lua_3