PIL.16Lua的编译、执行
尽管我们说Lua是一个解释型的语言,但Lua总是在运行代码前会编译成一种中间格式。(这并不重要,很多解释型也会这样做)编译阶段的存在对于解释型语言听起来有点不太对。然而,解释型语言的重要特性不是说他们不会被编译,而是说其轻易执行在空中生成的代码。我们可以说,一个dofile
这样的函数存在给为了我们把Lua称为解释型语言的资格。
我们会讨论Lua执行代码chunks的过程,编译意味着什么(做了什么),Lua怎么样运行编译了的代码,在这过程中怎么控制错误。
前面,我们把 dofile
介绍为一种Lua中执行代码的基本方式,但是 dofile
其实是一个辅助函数:loadfile
才做了真正的工作。
类似 dofile
, loadfile
从一个文件加载 Lua chunk,但是不会运行这个 chunk。他只会编译这个 chunk,然后把编译后的 chunk 以一个函数返回。而且,loadfile
不会和 dofile
一样返回错误,其只会返回错误代码。我们可以如下定义 dofile
:
function (filename) |
当 loadfile
失败时用 assert
来抛出错误。
对于简单的任务, dofile
是很方便的,因为其在一个调用中就完成了工作。 然而, loadfile
更灵活。如果出错, loadfile
返回 nil 加上错误消息,这就允许我们以自定义的方式处理错误。 然后,如果我们需要多次运行一个文件,我们可以调用 loadfile
一次,然后调用其结果多次。这个方式比多次调用 dofile
更廉价,因为只编译文件一次。(在语言中,编译对比其他操作始终是比较昂贵的)
load
函数和 loadfile
类似,不同的是其从一个字符串或一个函数读取 chunk,而不是从一个文件。考虑下面的代码:
f = load("i = i + 1") |
在这个代码后,f 将会是一个函数,在调用的时候会执行 i = i + 1
:
i = 0 |
load
是非常强大的;但我们要小心使用。但它也是昂贵的函数(和其他操作对比而言)而且有可能得到费解的代码。在用它之前,确定实在没有更简单的办法来解决问题。
如果我们想要做一个 快速但脏 的 dostring
(加载并运行一个chunk),我们可以load
的结果:
load(s)() |
然而,如果这里有语法错误,load
将会返回 nil 和最后的错误消息(类似 attempt to call a nil value)这样。对于更清楚的错误消息,最好使用:
assert(load(s))() |
通常,在一个字符串上使用 load
并没有什么意义。
f = load("i = i + 1") |
这两种方式是相等的,但是后面这种方式会更快,因为Lua这把函数及其包围的chunk一起编译。第一种方式中,load
会导致一次单独的编译。
load
并不以词法范围来编译,前面例子中的两行并不真正的相等。为了看到不同,我们稍微改变一下例子:
i = 32 |
函数 g 操纵的是局部变量 i,正是我们想要的,但是 f 操纵的是 全局 的 i,因为load
总是在全局环境中编译其 chunk。
load
最典型的用法是用来运行外部的代码(程序外的)或者动态生成的代码。比如我们可能想要策划一个被用户定义的函数;用户进入这个函数代码,然后我们使用 load
来执行它。注意,load
期望一个chunk,也就是语句。如果我们要执行一个计算一个表达式,我们可以用 return 放在表达式前:
print "enter your expression:" |
因为load
返回的是一个普通函数,我们可以多次调用它:
print "enter function to be plotted (with variable 'x'):" |
我们可以以一个 阅读器函数 来作为 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)") |
使用这些特性,我们可以重写我们的策划例子来避免使用全局变量 x:
print "enter function to be plotted (with variable 'x'):" |
load, loadfile
不会抛出错误。如果有,他们会返回 nil 和错误消息:
print(load("i i")) |
重要的是,这些函数从不会有什么副作用,这就说,他们不会改变或者创建变量,不写出文件等等。他们只是把chunk编译为一个内部格式然后以一个匿名函数运行编译结果。一个常常错误的假设就是 加载一个chunk定义了函数。在Lua中,函数定义其实是赋值;这是在运行时发生的,而不是编译时。现在我们有 foo.lua文件:
-- file foo.lua |
当执行命令:
f = loadfile("foo.lua") |
这个命令编译了 foo,但是并没有定义它。为了定义它,我们必须运行下面的chunk:
f = loadfile("foo.lua") |
这个行为听起来有点奇怪,但如果我们重写一下我们的文件就明白了:
-- file 'foo.lua' |
在一个生产力程序中,如果需要运行外部代码,我们必须处理任何加载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]) |
关键的函数是 string.dump
:其接受一个Lua函数,然后返回其预编译的代码为一个字符(已合适的格式化,能被Lua载入回去)
luac
提供了一切有趣的选项。实际上,-l
选项列出了编译器为一个给定chunk产生的操作码。下面这行:
a = x + y - z |
用 luac -l 产生的输出如下:
main <stdin:0,0> (7 instructions, 28 bytes at 0x988cb30) |
预编译格式的代码并不总是比源代码小,但是加载更快。另外一个好处是其对意外的修改源文件做了一个保护。和源代码不同,恶意的崩溃二进制代码会让Lua解释器崩溃设置用户提供的机器代码。当运行普通代码时,没有什么好担心的。然而,请不要以预编译格式运行不可信的代码。load
有一个选项可以来干这个工作。
load
有四个参数,后面三个是可选的。第二个是chunk的名字,只会在错误消息中使用。第四个参数是一个环境。我们感兴趣的是第三个;其控制了什么类型的chunk可以被加载。如果存在第三个参数,其必须是一个字符串:t 只允许文本(正常)chunk;b 只允许二进制(预编译)chunk;bt,默认值,允许两种格式。