PIL.16Lua的编译、执行

4.7k 词

尽管我们说Lua是一个解释型的语言,但Lua总是在运行代码前会编译成一种中间格式。(这并不重要,很多解释型也会这样做)编译阶段的存在对于解释型语言听起来有点不太对。然而,解释型语言的重要特性不是说他们不会被编译,而是说其轻易执行在空中生成的代码。我们可以说,一个dofile这样的函数存在给为了我们把Lua称为解释型语言的资格。

我们会讨论Lua执行代码chunks的过程,编译意味着什么(做了什么),Lua怎么样运行编译了的代码,在这过程中怎么控制错误。

前面,我们把 dofile 介绍为一种Lua中执行代码的基本方式,但是 dofile 其实是一个辅助函数:loadfile 才做了真正的工作。

类似 dofileloadfile 从一个文件加载 Lua chunk,但是不会运行这个 chunk。他只会编译这个 chunk,然后把编译后的 chunk 以一个函数返回。而且,loadfile 不会和 dofile 一样返回错误,其只会返回错误代码。我们可以如下定义 dofile

function  (filename)
local f = assert(loadfile(filename))
return f()
end

loadfile 失败时用 assert 来抛出错误。

对于简单的任务, dofile 是很方便的,因为其在一个调用中就完成了工作。 然而, loadfile 更灵活。如果出错, loadfile 返回 nil 加上错误消息,这就允许我们以自定义的方式处理错误。 然后,如果我们需要多次运行一个文件,我们可以调用 loadfile一次,然后调用其结果多次。这个方式比多次调用 dofile 更廉价,因为只编译文件一次。(在语言中,编译对比其他操作始终是比较昂贵的)

load函数和 loadfile 类似,不同的是其从一个字符串或一个函数读取 chunk,而不是从一个文件。考虑下面的代码:

f = load("i = i + 1")

在这个代码后,f 将会是一个函数,在调用的时候会执行 i = i + 1

i = 0
f(); print(i)
f(); print(i) -- 2

load 是非常强大的;但我们要小心使用。但它也是昂贵的函数(和其他操作对比而言)而且有可能得到费解的代码。在用它之前,确定实在没有更简单的办法来解决问题。

如果我们想要做一个 快速但脏 的 dostring(加载并运行一个chunk),我们可以load的结果:

load(s)()

然而,如果这里有语法错误,load 将会返回 nil 和最后的错误消息(类似 attempt to call a nil value)这样。对于更清楚的错误消息,最好使用:

assert(load(s))()

通常,在一个字符串上使用 load 并没有什么意义。

f = load("i = i + 1")
f = function () i = i + 1 end

这两种方式是相等的,但是后面这种方式会更快,因为Lua这把函数及其包围的chunk一起编译。第一种方式中,load 会导致一次单独的编译。

load并不以词法范围来编译,前面例子中的两行并不真正的相等。为了看到不同,我们稍微改变一下例子:

i = 32
local i = 0
f = load("i = i + 1; print(i) ")
g = function () i = i + 1; print(i) end
f() -- 33
g()

函数 g 操纵的是局部变量 i,正是我们想要的,但是 f 操纵的是 全局 的 i,因为load总是在全局环境中编译其 chunk。

load最典型的用法是用来运行外部的代码(程序外的)或者动态生成的代码。比如我们可能想要策划一个被用户定义的函数;用户进入这个函数代码,然后我们使用 load 来执行它。注意,load 期望一个chunk,也就是语句。如果我们要执行一个计算一个表达式,我们可以用 return 放在表达式前:

print "enter your expression:"
local line = io.read()
local func = assert(load("return " .. line))
print("the value of your expression is " .. func())

因为load返回的是一个普通函数,我们可以多次调用它:

print "enter function to be plotted (with variable 'x'):"
local line = io.read()
local f = assert(load("return " .. line))
for i = 1, 20 do
x = i -- global 'x' (to be visible from the chunk)
print(string.rep("*", f()))
end

我们可以以一个 阅读器函数 来作为 load 的第一个参数。一个阅读器函数可以按部分返回chunk;load会成功调用阅读器直到其返回 nil,这个nil 代表着chunk的结束。下面的代码,和loadfile 相等:

f = load(io.lines(filename, "*L"))

每次调用中,io.lines(filename, "*L")会从给定的文件返回一个新行。所以,load会从文件逐行读取chunk。下面的版本是类似的,但是更高效:

f = load(io.lines(filename, 1024))

这里,被 io.lines返回的迭代器从 1024 字节的快读取文件。

Lua把每个独立的chunk当做匿名可变函数的主体对待。load("a = 1")返回和下面相等的表达式:

function (...) a = 1 end

和其他函数一样,chunks 可以声明局部变量:

f = load("local a = 10; print(a + 20)")
f() -- 30

使用这些特性,我们可以重写我们的策划例子来避免使用全局变量 x

print "enter function to be plotted (with variable 'x'):"
local line = io.read()
local f = assert(load("local x = ...; return " .. line))
for i = 1, 20 do
print(string.rep("*", f(i)))
end

load, loadfile 不会抛出错误。如果有,他们会返回 nil 和错误消息:

print(load("i i"))
-- > nil [strng "i i"]:1: '=' expected near 'i'

重要的是,这些函数从不会有什么副作用,这就说,他们不会改变或者创建变量,不写出文件等等。他们只是把chunk编译为一个内部格式然后以一个匿名函数运行编译结果。一个常常错误的假设就是 加载一个chunk定义了函数。在Lua中,函数定义其实是赋值;这是在运行时发生的,而不是编译时。现在我们有 foo.lua文件:

-- file foo.lua
function foo (x)
print(x)
end

当执行命令:

f = loadfile("foo.lua")

这个命令编译了 foo,但是并没有定义它。为了定义它,我们必须运行下面的chunk:

f = loadfile("foo.lua")
print(foo) -- nil
f() -- run the chunk
foo("ok") ok

这个行为听起来有点奇怪,但如果我们重写一下我们的文件就明白了:

-- file 'foo.lua'
foo = function (x)
print(x)
end

在一个生产力程序中,如果需要运行外部代码,我们必须处理任何加载chunk产生的错误。而且,我们可能想要在保护环境下运行新的chunk,来避免不友好的副作用。

预编译代码

Lua会在运行前预编译代码,也允许我们以预编译的格式发布代码

最简单的方式来产生预编译文件————术语叫 二进制chunk————是使用luac程序。下面的调用会建立一个新文件prog.lc,其中存有 文件 prog.lua的预编译版本:

$luac -o prog.lc prog.lua

Lua解释器可以像其他Lua文件一样执行这个新文件:

$lua prog.lc

Lua在接受源代码的地方就能接受预编译代码。实际上,loadfile, load都接受预编译代码。

我们可以在Lua中写一个最小的 luac:

p = loadfile(arg[1])
f = io.open(arg[2], "wb")
f:write(string.dump(p))
f:close()

关键的函数是 string.dump:其接受一个Lua函数,然后返回其预编译的代码为一个字符(已合适的格式化,能被Lua载入回去)

luac提供了一切有趣的选项。实际上,-l 选项列出了编译器为一个给定chunk产生的操作码。下面这行:

a = x + y - z

用 luac -l 产生的输出如下:

      main <stdin:0,0> (7 instructions, 28 bytes at 0x988cb30)
0+ params, 2 slots, 0 upvalues, 0 locals, 4 constants, 0 functions
1 [1] GETGLOBAL 0 -2 ; x
2 [1] GETGLOBAL 1 -3 ; y
3 [1] ADD 0 0 1
4 [1] GETGLOBAL 1 -4 ; z
5 [1] SUB 0 0 1
“Lua.

The luac program offers some other interesting options. In particular, option -l lists the opcodes that the compiler generates for a given chunk. As an example, Figure 16.1, “Example of output from luac -l” shows the output of luac with option -l on the following one-line file:

a = x + y - z
Figure 16.1. Example of output from luac -l

main <stdin:0,0> (7 instructions, 28 bytes at 0x988cb30)
0+ params, 2 slots, 0 upvalues, 0 locals, 4 constants, 0 functions
1 [1] GETGLOBAL 0 -2 ; x
2 [1] GETGLOBAL 1 -3 ; y
3 [1] ADD 0 0 1
4 [1] GETGLOBAL 1 -4 ; z
5 [1] SUB 0 0 1
6 [1] SETGLOBAL 0 -1 ; a
7 [1] RETURN 0 1”

预编译格式的代码并不总是比源代码小,但是加载更快。另外一个好处是其对意外的修改源文件做了一个保护。和源代码不同,恶意的崩溃二进制代码会让Lua解释器崩溃设置用户提供的机器代码。当运行普通代码时,没有什么好担心的。然而,请不要以预编译格式运行不可信的代码。load 有一个选项可以来干这个工作。

load有四个参数,后面三个是可选的。第二个是chunk的名字,只会在错误消息中使用。第四个参数是一个环境。我们感兴趣的是第三个;其控制了什么类型的chunk可以被加载。如果存在第三个参数,其必须是一个字符串:t 只允许文本(正常)chunk;b 只允许二进制(预编译)chunk;bt,默认值,允许两种格式。