原文出处:编程语言 - Lua 全教程

#!/usr/bin/env lua
print("Hello World!")

Lua (LOO-ah) 是一种可嵌入、轻量、快速、功能强大的脚本语言。它支持过程式编程、 面向对象编程、函数式编程、数据驱动编程和数据描述(data description)。

Lua 将简洁的过程式语法和基于关联数组、可扩展语义的数据描述语法结构结合了起来。 Lua 是动态类型的语言,它使用基于寄存器的虚拟机解释和运行字节码(bytecode),并 使用增量垃级回收(incremental garbage collection)机制自动管理内存。这些特点使 得 Lua 很适合用于配置、脚本化和快速构造原型的场景。

Lua 是第一个由第三世界国家(巴西)开发者开发的流行度很高的语言(and the leading scripting language in games)。

Lua 解释器只有 2w+ 多行 ANSI C/C++ 代码, 可执行文件 200+ KB 大小。

下面是几个嵌入了 Lua 解释器,可以使用 Lua 扩展功能的知名应用程序:

Lua 版本

Lua 官方于 2011 年发布的 5.2 和 2015 年发布了 5.3 版本,和用户规模很大的 2006 年发布的 5.1 相比,改动很大,在 Lua 语法和 C API 方面都互不兼容。OpenResty 和 LuaJIT 对于这两个最新版本的支持存在难度:

https://openresty.org/en/faq.html
Lua 5.2+ are incompatible with Lua 5.1 on both the C API land and the Lua
land (including various language semantics)...Lua 5.2+ are essentially
incompatible different languages.
Supporting Lua 5.2+ requires nontrivial architectural changes in ngx_lua's
basic infrastructure. The most troublesome thing is the quite different
"environments" model in Lua 5.2+. At this point, we would hold back ading
suport for Lua 5.2+ to ngx_lua.
https://www.reddit.com/r/lua/comments/2zutj8/mike_pall_luajit_dislikes_lua_53
Mike Pall (LuaJIT) dislikes Lua 5.3

总而言之,Lua 语言像 Python 一样甩开了「向前兼容」的包袱,大跨步向前发展。这对 语言本身来讲,是件好事儿,但是对使用者来讲,短期内是件痛苦的事儿。

由于我们本次学习 Lua 的目的是为 OpenResty 开发做准备,所以,本文概念和示例主要 围绕 Lua 5.1 展示。

Lua 环境

开发工具

软件包管理

分析和调试

更多工具参见:http://lua-users.org/wiki/ProgramAnalysis

基础概念

常量和标识符

and, break, do, else, elseif, end, false, for, function, if, in, local, nil, not, or, repeat, return, then, true, until, while
/ % ^ ##== ~= <= >= < > = ( ) { } [ ] ; : , . .. ... -- ',' is not an operator in Lua, but only a delimiter
\a, \b, \f, \n, \r, \t, \v, \\, \", \ddd, \0
_VERSION
3, 3.0, 3.1416, 314.16e-2, 0xff, 0x56
-- short comment
--[[
    this is a very loooooooooooooong
    comment
]]
--[=[
    Same scheme used with long string.
]=]

变量和数据类型

表达式(expression)

语句(statement)

惯用法

The Lua way.

高级特性

高级语法结构

元表(metatable)

​ Every value in Lua can have a metatable.

Lua 通过 metatable 定义数据(original value)在某些特殊操作下(算术运算、大小比较、连接操作、长度操作和索引操作等)的行为。

我们将 metatable 支持的具体操作称为 event ,操作对应的行为由 metamethod 体现。 metatable 实际上是一个普通的 tableevent 名添加 __ 下 划线前缀后,作为 metatable 的索引(key),索引对应的值(value)就是 metamethod 。 比如,使用非数字类型的值作为算术加 + 的操作数时,Lua 会使用 metatable__add 对应的 metamethod 完成算术加运算。

metatable 提供的主要 event 有:

在 Lua 代码中,可以为每个 tableuserdata 设置不同的 metatable , 而其它 6 种数据类型每种类型的值使用相同的 metatable 。在 Lua 代码中,只能 设置和修改 table 类型值的 metatable,其它类型的 metatable 可以使 用 C API 修改。

userdata 由 C API lua_newuserdata 创建,它和 malloc 创建的内存块有 所不同: userdata 占用的内存会被垃圾回收器回收; 可以为 userdata 设置 metatable ,定制它的行为。 userdata 只支持两种 event__len__gc ,其中, __gcmetamethod 由垃圾回收环节,由垃圾回收器调用。比如,标准库提供的 file 类型的对象,它的 __gc metamethod 负责关闭底层文 件句柄。

另外, metatable 可以使用 __mode eventtable 定义为 weak table

To understand weak tables, you need to understand a common problem with garbage collection. The collector must be conservative as possible, so cannot make any assumptions about data; it is not allowed to be pyshic. As soon as objects are kept in a table, they are considered referenced and will not be collected as long as that table is referenced.
Objects referenced by a weak table will be collected if there is no other reference to them. Putting a value in a table with weak values is effect telling the garbage collector that this is not an important reference and can be safed collected.

A weak table can have weak keys, weak values, or both. A table with weak keys allows the collection of its keys, but prevents the collection of its value. A table with both weak keys and weak values allows the collection of both keys and values. In any case, if either key or the value is collected, the whole pair is removed from the table.

环境(environment)

environment 是除了 metatable 外另外一种可以和 threadfunctionuserdata 类型的值相关联的 tableenvironment 相当于 命名空间,对象通过它查找可以访问的变量。

对象间可以共享同一个 environment

Lua 代码中可以使用 getfenvsetfenv 操作 Lua function 和 正在运行的 threadenvironment , 而 C functionuserdata 和其 它 threadenvironment 只能使用 C API 操作。

闭包(closure)

function count()
    local i = 0
    return function() i = i + 1 return i end
end

local counter = count()
print(counter())
print(counter())

-- partial function
function bind(val, f)
      return function(...)
              return f(val, ...)
      end
end

prt = bind("hello", print)
prt(10, 20)

协程(coroutine)

Lua 原生支持协程,使用 coroutine 类型表示。Lua 协程代表有独立执行流程的线 程。协程由 Lua 解释器调度执行,一个协程显式让出执行权后,其它协程才会被调度执 行。

代码组织

模块(module)

Lua 的模块和其它语言作用类似,用于将一组功能相似的函数和常量存放一起,方便用户 共享代码。

From the user point of view, a module is a library that can be loaded through require and that defines one single global name containing a table. Everything that the module exports, such as functions and constants,it defines inside this table, which works as a namespace. A well-behaved module also arrange for require to return this table.

Lua 语言实现提供了诸如 math, io, string 等等的标准模块,用户可以在 代码中直接使用这些模块提供的功能。同时,Lua 也给用户提供了实现自定义模块的机制 和方法,用户可以使用 Lua 代码或者 C API 开发自定义模块。

定义模块

通常有如下两种使用 Lua 代码定义模块的方法:

使用模块

Lua 提供的内建模块会解释器预加载到全局环境里,在 Lua 代码中可以直接使用或者通 过全局环境引用。

-- main.lua
for line in io.lines "myfile" do
    ...
end
-- or
for line in _G.io.lines "myfile" do
  ...
end

而用户自定义的模块,在使用前需要通过 require 函数加载到代码块可以直接访问的 environment 中。

使用 module 实现的模块在加载时,会将模块放到全局环境 _G 中,加载后的模块 像内建模块一样可以直接调用。

-- main.lua
require "socket"
socket.connect(...)

或者,

-- main.lua
local socklib = require "socket"
socklib.connect(...)

而上面提到的 Lua 5.2 的方法实现的模块,不会在全局环境定义变量。使用这类模块时, 只能通过 require 使用该模块:

-- main.lua
local mymod = require "mymod"
mymod.do_something()

总结下来,模块的使用建议如下:

The require "name" syntax was the one introduced in Lua 5.1; This call does not always return the module, but it was expected a global would be created with the name of the library (so, you now have a _G.name to use the library with). In new code, you should use local name = require "name" syntax; it works in the vast majority of cases, but if you're working with some older modules. They may not support it, and you'll have to just use require "module".

模块查找

require 函数负责查找和加载模块,这个过程大致步骤如下(以 require "testm" 为例):

  1. 根据 package.preload["testm"] 的值,判断 testm 是否己经加载过:如果 该模块已经加载过, requirepackage.preload["testm"] 的值返回;如 果该模块未加载过,继续第 2 步;
  2. 逐个调用 package.loaders 设置的 searcher 函数,选择一个可以用于加载 testmloader 。Lua 默认提供了 4 个 searcher

    • A searcher simply looks for a loader in the package.preload table.
    • A searcher looks for a loader as a Lua library using package.path.
    • A searcher looks for a loader as a C library, using package.cpath.
    • A searcher searches the C path for a library for the root name of the given module.
  3. 调用 loader 加载和执行模块代码。如果 loader 有返回值, require 将这个返回值赋与 package.preload["testm"] ;如果 loader 没有返回 值, requirepackage.loaded["testm"] 赋值为 true

  4. requirepackage.loaded["testm"] 的值返回给调用者;

上述流程的模拟代码如下:

function require(name)
    if not package.loaded[name] then
        local loader = findloader(name)
        if loader == nil then
            error("unable to load module " .. name)
        end
        package.loaded[name] = true
        local res = loader(name)
        if res ~= nil then
            package.loaded[name] = res
        end
    end
    return package.loaded[name]
end

从上面的描述可以看到, require 根据 package.path 中设置的路径查找 Lua 模块,根据 package.cpath中设置的路径模式查找 C 模块。路径模式是包含了 ?; 的字符串,; 用于分隔文件系统的路径, ? 会被 require 替换成模块名。例如:

-- package.path
./?.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua

执行 require("testm") 时, require 会依次使用下面路径查找模块代码:

./testm.lua
/usr/share/lua/5.1/testm.lua
/usr/share/lua/5.1/testm/init.lua

包(package)

Lua 允行将模块按照层级结构组织起来,层级之间使用 . 分隔。例如, 模块 mod.sub 是模块 mod 的子模块。 package 就是这样按此形式组织起来的 模块的集合,同时,它也是 Lua 中用于代码分发的单元。

和模块类似,例如,使用 require 查找和加载子模块 a.b.c 时, require 通过 package.loaded["a.b.c"] 的值判断该子模块是否已经被加载过。和模块不同 的是,如果子模块未被加载过, require 先将 . 转换成操作系统路径分隔符, 比如,类 UNIX 平台上, a.b.c 被转换成 a/b/c ,然后使用 a/b/c 替换 package.pathpackage.cpath 中的 ? 后,查找子模块文件。

module 函数也提供了对子模块的支持,例如,上面的子模块可以使用 module("a.b.c") 的方式定义。同时, module 会定义全局变量 a.b.c 引用 子模块:

module puts the environment table into variable a.b.c, that is , into a field c of a table in field b of a table a. If any of these intermediate tables do not exist, module creates them. Otherwise, it reuses them.

需要注意的一点是,同一个 package 中的子模块之间,除了上面提到的它们的环境可 能嵌套存放以外,并没有显式的关联。比如, 执行require("a") 时,并不会自动加 载它的子模块 a.b ;执行了 require("a.b") 时,也不会自动加载 a 模块;

面向对象

Lua 语言并未提供对面向对象编程模型的原生支持,但是它提供的 table 类型和 metatableenvironment 等机制,可以用来实现类似的面向对象功能。

下面是摘自 PiL 的代码示例:

--- A base class
Account = {balance= 0}
-- Lua hide `self` when using *colon operator*, a syntactic sugar

function Account:new(o)
    -- A hidden `self` refers to table `Account`
    o = o or {}
    setmetable(o, self)
    self.__index = self
    return o
end

function Account:deposit(v)
    self.balance = self.balance + v
end

function Account.withdraw(self, v)
    if v > self.balance then error "insufficient funds" end
    self.balance = self.balance - v
end

-- creates an instance of Account
a = Account:new{balance = 0}
a:deposit(100.00)   -- syntactic sugar of `a.deposit(a, 100.00)`

--- Inheritance
-- `SpecialAccount` is just an instance of `Account` up to now.
SpecialAccount = Account:new()
s = SpecialAccount:new{limit=1000.00}   -- `self` refers to `SpecialAcount`

-- the metatable of `s` is `SpecialAcccount`.
-- `s` is a table and Lua cannot find a `deposit` field in it, so it look
-- into `SpecialAccount`; it cannot find a `deposit` field there, too, so
-- it looks into `Account` and there it finds the original implementation
-- for a `deposit`
s:deposit(100.00)

-- What makes a `SpecialAccount` special is that we can redefine any method
-- inherited from its superclass.
function SpecialAccount:withdraw(v)
    if v - self.balance >= self:getLimit() then
        error"insufficient funds"
    end
    self.balance = self.balance - v
end

function SpecialAccount:getLimit()
    return self.limit or 0
end

-- Lua does not go to `Account`, because it finds the new `withdraw` method
-- in `SpecialAccount` first.
s:withdraw(200.00)

由于语言所限,使用 Lua 实现的面向对象模拟,并不能提供隐私控制机制。

语言互操作

luafaq#T4.4 luafaq#T4.5 luafaq#T7

C API

TODO: To be finished.

FFI

其它

命令行参数

在使用 lua 解释器运行 lua 脚本文件时,Lua 解释器会将所有命令行参数通过全局 table 类型数组 arg 的方式传递给脚本文件:

如下命令行调用,

% lua -la b.lua t1 t2

Lua 会创建有如下元素的 arg 数组:

arg = {
  [-2]= "lua", [-1]= "-la",
  [0]= "b.lua", [1]= "t1", [2]= "t2"
}

其中,索引值为 0 的元素是脚本的文件名,索引值从 1 开始的元素是在命令中出现的脚 本文件名后面的命令行参数,索引值小于 0 的是出现在脚本文件名前面的命令行参数。

在 Lua 代码中,还可以使用 ... varargs 表达式获取索引从 1 开始的命令行参数。

不出意外,Lua 并未提供处理命令行参数的标准方式。但是开发者可以参考其它 Lua 程 序,比如 Luarocks ,使用的处理逻辑,或者使用非标准库lapp

下面的代码摘自 Luarocks ,它使用 Lua 的字符串匹配函数进行命令行参数解析:

--- Extract flags from an argument list.
-- Given string arguments, extract flag arguments into a flags set.
-- For example, given "foo", "--tux=beep", "--bla", "bar", "--baz",
-- it would return the following:
-- {["bla"] = true, ["tux"] = "beep", ["baz"] = True}, "foo", "bar".
function parse_flags(...)
    local args = {...}
    local flags = {}
    for i = #args, 1, -1 do
        local flag = args[i]:match("^%-%-(.*)")
        if flag then
            local var, val = flag:match("([a-z_%-]*)=(.*)")
            if val then
                flags[var] = val
            else
                flags[flag] = true
            end
            table.remove(args, i)
        end
    end
    return flags, unpack(args)
end

装饰器

http://lua-users.org/wiki/DecoratorsAndDocstrings

优化建议

The first question is, do you actually have a problem? Is the program not *fast enough? Remember the three basic requirements of a sytem: Correct, Robust, and Efficient, and the engineering rule of thumb that you may have to pick only two.
Donald Knuth is often quoted about optimisation: "If you optimise everything, you will always be unhappy" and "we should forget about small efficiencies, say about 97% of the time: premature optimisation is the root of all evil."
Assume a program is correct and (hopefully) robust. There is a definite cost in optimising that program, both in programmer time and in code readability. If you don't know what the slow bits are, then you will waste time making your ugly and maybe a little faster (which is why he says unhappy).

Nevertheles, we all know that performance is a key ingredient of programming. It is not by change that problems with exponential time complexity are called intractable. A too late result is a useless result. So every good programmer should always balance the costs from spending resources to optimize a piece of code against the gains of saving resources when running that code.
The first question regarding optimization a good programmer always asks is: "Does the program needs to be optimized?" If the answer is positive (but only then), the second question should be: "Where?"

所以,优化建议的第一条就是不要轻易尝试优化。如果确实到了非优化不可的地步, 也需要先用工具定位需要优化的地方,比如,代码中会被频繁调用并且性能不佳的函数和内循环里的低效操作等等,对这些地方的优化能用较少的工作量换来整体性能的提升。

LuaProfiler 就是一个用于定位代码中低效热点的工具。

我们还可以使用 LuaJIT 替代标准 Lua (Vanilla Lua) 运行代 码,它可能会带来几十倍 的性能提升。

CPU 密集型的操作可以放到使用 C API 实现的模块中。如果实现正确的话,整体可以 达到近似原生 C 程序的性能。同时,因为 Lua 语言语法精练,整体代码也更短小,更易 维护。 另外,通过 LuaJIT 提供的 FFI 等类似接口,甚至可以直接访问外部库提供的 C 语言函数和数据结构,这样就省去了使用 C API 编写模块的繁杂工作。

下面是几条可以提高代码性能的 开发建议

常用类库

参考资料