Lua DDJ 文章

重印Dr. Dobb's Journal 21 #12 (1996 年 12 月) 26–33。版权所有 © 1996 Miller Freeman, Inc.

Lua:一种可扩展的嵌入式语言
少数元机制取代大量功能

作者:Luiz Henrique de Figueiredo、Roberto Ierusalimschy 和 Waldemar Celes

近年来,已经提出多种小语言用于扩展和自定义应用程序。一般来说,这些扩展语言应具有以下属性

由于扩展语言不是用于编写大型软件,因此支持大规模编程的机制(如静态类型检查和信息隐藏)不是必需的。

Lua 是我们在此介绍的可扩展嵌入式语言,它满足这些要求。它的语法和控制结构简单且熟悉。Lua 很小——整个实现不到 6000 行 ANSI C。除了大多数过程语言通用的功能外,Lua 还具有使其成为强大的高级可扩展语言的特殊功能

Lua 是一种通用的嵌入式编程语言,旨在支持具有数据描述功能的过程编程。虽然它不在公共领域(TeCGraf 保留版权),但 Lua 可用于学术和商业目的,网址为 https://lua.ac.cn/。该发行版还包括一个数学函数(sincos 等)、I/O 和系统函数以及字符串处理函数的标准库。此可选库向系统添加了大约 1000 行代码。还包括一个调试器和一个单独的编译器,用于生成包含字节码的可移植二进制文件。该代码在大多数 ANSI C 编译器中无需更改即可编译,包括 gcc(在 AIX、IRIX、Linux、Solaris、SunOS 和 ULTRIX 上)、Turbo C(在 DOS 上)、Visual C++(在 Windows 3.1/95/NT 上)、Think C(MacOS)和 CodeWarrior(MacOS)。

所有外部标识符都以 lua 为前缀,以避免在与应用程序链接时发生名称冲突。即使是 yacc 生成的代码也会通过 sed 过滤器来遵守此规则,以便可以将 Lua 与将 yacc 用于其他目的的应用程序链接起来。

Lua 实现

Lua 以一个小型 C 函数库的形式提供,可链接到主机应用程序。例如,最简单的 Lua 客户端是 清单一 中的交互式独立解释器。在此程序中,函数 lua_dostring 通过一段包含在字符串中的代码调用解释器。Lua 代码的每个块可能包含语句和函数定义的混合。

头文件 lua.h 定义了 Lua 的 API,其中包含大约 30 个函数。除了 lua_dostring,还有一个 lua_dofile 函数用于解释文件中包含的 Lua 代码,lua_getgloballua_setglobal 用于操作 Lua 全局变量,lua_call 用于调用 Lua 函数,lua_register 用于使 C 函数可从 Lua 访问,等等。

Lua 的语法有点类似于 Pascal。为了避免悬空的 elseifwhile 等控制结构以显式的 end 结束。注释遵循 Ada 惯例,以 “--” 开头,并一直持续到行尾。Lua 支持多重赋值;例如,x, y = y, x 交换了 xy 的值。同样,函数可以返回多个值。

Lua 是一种动态类型语言。这意味着值具有类型,但变量没有,因此没有类型或变量声明。在内部,每个值都有一个标记来标识其类型;可以在运行时使用内置函数type查询标记。变量是无类型的,可以保存任何类型的值。Lua 的垃圾回收会跟踪哪些值正在被使用,并丢弃那些未被使用 的值。

Lua 提供了nilstringnumberuser datafunctiontable 类型。nil 是值nil 的类型;其主要属性是它与任何其他值都不同。例如,这在用作变量的初始值时很方便。number 类型表示浮点实数。string 具有通常的含义。user data 类型对应于 C 中的通用 void* 指针,并在 Lua 中表示宿主对象。所有这些类型都很有用,但 Lua 的灵活性归功于函数和表,这是从 Lisp 和 Scheme 中吸取的两条关键经验教训的产物

Lua 中的函数值可以存储到变量中,作为参数传递给其他函数,存储在表中,等等。

当你在 Lua 中声明一个函数(参见清单 2)时,函数体会被预编译成字节码,从而创建一个函数值。此值被分配给具有给定名称的全局变量。另一方面,C 函数由宿主程序通过对 API 的适当调用提供。Lua 无法调用其宿主未注册的 C 函数。因此,宿主对 Lua 程序可以做什么拥有完全控制权,包括对操作系统的任何潜在危险访问。

表对于 Lua 来说就像列表对于 Lisp 一样:强大的数据结构机制。Lua 中的表类似于关联数组。关联数组可以用任何类型的值进行索引,而不仅仅是数字。

当使用关联数组实现时,许多算法变得微不足道,因为搜索它们的语言隐式提供了数据结构和算法。Lua 将关联数组实现为哈希表。

与实现关联数组的其他语言不同,Lua 中的表不绑定到变量名。相反,它们是动态创建的对象,可以像传统语言中的指针一样进行操作。换句话说,表是对象,而不是值。变量不包含表,只包含对它们的引用。赋值、参数传递和函数返回始终操作对表的引用,并且不暗示任何类型的复制。虽然这意味着必须在使用表之前显式创建表,但它也允许表自由地引用其他表。因此,Lua 中的表可用于表示递归数据类型并创建通用图结构,即使是具有循环的图结构。

表通过使用字段名作为索引来模拟记录。Lua 通过提供a.name作为a["name"]的语法糖来简化此操作。还可以通过将元素存储为表的索引来轻松实现集合。请注意,表(因此集合)不必是同质的;它们可以同时存储所有类型的值,包括函数和表。

Lua 提供了一个构造器,一种用于创建表的特殊表达式,它非常适合初始化列表、数组、记录等。参见示例 1

用户定义的构造函数

有时,你需要更精细地控制正在构建的数据结构。遵循仅提供少量通用元机制的理念,Lua 提供了用户定义的构造函数。这些构造函数写为 name{...},这是 name({...}) 的更直观的版本。换句话说,使用这样的构造函数,将创建一个表,对其进行初始化,并将其作为参数传递给函数。此函数可以执行所需的任何初始化,例如动态类型检查、初始化不存在的字段以及辅助数据结构更新,甚至在宿主程序中也是如此。

用户定义的构造函数可用于提供更高级别的抽象。因此,在具有适当定义的环境中,你可以编写 window1=Window{x=200, y=300, color="blue"} 并考虑“窗口”,而不是普通表。此外,因为构造函数是表达式,所以可以嵌套它们以声明式风格描述更复杂结构,如 清单四 中所示。

面向对象编程

因为函数是一级值,所以表字段可以引用函数。这是面向对象编程的一步,并且通过更简单的语法来定义和调用方法,使得这一步变得更容易。

方法定义写为 示例 2(a),它等效于 示例 2(b)。换句话说,定义方法等效于定义函数,其中有一个名为 self 的隐藏第一个参数,并将函数存储在表字段中。

方法调用写为 receiver: method(params),它被翻译为 receiver.method(receiver,params)。方法的接收者作为方法的第一个参数传递,从而为参数 self 提供了预期的含义。

这些构造不提供信息隐藏,因此纯粹主义者可能会(正确地)声称,面向对象的一个重要部分缺失。此外,Lua 不提供类;每个对象都携带自己的方法分派表。然而,这些构造极其轻量,并且可以使用继承来模拟类,这在其他基于原型的语言中很常见,例如 Self。

后备

因为 Lua 是一种无类型语言,所以可能会发生许多异常的运行时事件:将算术运算应用于非数字操作数、对非表值进行索引、调用非函数值。在有类型、独立的语言中,其中一些条件由编译器标记;其他条件导致在运行时中止程序。嵌入式语言中止其宿主程序是不礼貌的,因此嵌入式语言通常提供错误处理挂钩。

在 Lua 中,这些挂钩称为“后备”,也用于处理严格来说不是错误条件的情况,例如访问表中不存在的字段和发出垃圾回收信号。Lua 提供默认后备处理程序,但你可以通过使用内置函数 setfallback 来设置自己的处理程序,该函数有两个参数:一个标识后备条件的字符串(参见 表 1),以及在发生该条件时要调用的函数。setfallback 返回旧的后备函数,因此如果需要,你可以链接后备处理程序

通过后备实现继承

后备最有趣的用途之一是实现 Lua 中的继承。简单继承允许对象在另一个称为其“父级”的对象中查找不存在字段的值;特别是,此字段可以是方法。这种机制是一种对象继承,与 Smalltalk 和 C++ 中采用的更传统的类继承相反。

在 Lua 中实现简单继承的一种方法是将父对象存储在不同字段中,例如 parent ,并设置“索引”后备函数;请参见 清单五。此代码定义了一个函数 Inherit 并将其设置为索引后备。每当 Lua 尝试访问对象中不存在的字段时,后备机制都会调用函数 Inherit。此函数首先检查对象是否有一个包含表值的字段 parent 。如果是,它会尝试在父对象中访问所需字段。如果该字段不存在于父级中,则会自动再次调用后备。此过程会向上重复,直到找到字段的值或父级链结束。当需要更好的性能时,可以使用 Lua 的 API 在 C 中实现相同的继承方案。

反射工具

作为一门解释型语言,Lua 提供了一些反射工具。一个示例是前面提到的函数 type。其他强大的反射函数是遍历表的 next 和遍历所有全局变量的 nextvar。函数 next 获取两个参数,表和表中的索引,并以某种实现相关顺序返回“下一个”索引。(回想一下,表是作为哈希表实现的。)它还返回与表中索引关联的值。(回想一下,Lua 中的函数可以返回多个值。)函数 nextvar 具有类似的行为,但它遍历全局变量而不是表的索引。

使用反射的一个有趣示例是动态类型。如前所述,Lua 没有静态类型。但是,有时检查给定值是否具有正确的类型以防止程序出现奇怪的行为很有用。使用 type 可以轻松检查简单类型。但是对于表,我们必须检查所有字段是否存在且是否正确填充。

使用 Lua 的数据描述工具,您可以使用值来描述类型:单个类型由其名称描述,而表类型由将每个字段映射到其所需类型的表描述(清单六)。给定这样的描述,您可以编写一个单一的、多态函数来检查值是否具有给定的类型;请参见 清单七

自反设施还允许程序操纵其自己的环境。例如,程序可以创建一个“受保护的环境”来运行另一段代码。这种情况在基于代理的应用程序中很常见,当主机运行通过互联网接收到的不受信任的代码时(例如,带有可执行内容的网页,在当今 Java 时代很流行)。一些扩展语言必须提供对安全执行的特定支持,但 Lua 足够灵活,可以使用语言本身来执行此操作。清单八展示了如何将整个全局环境保存在一个表中。类似的功能会恢复已保存的环境。因为所有函数都是分配给变量的一等值,所以从全局环境中删除函数非常简单。清单九展示了一个在受保护环境中运行一段代码的函数。

将 Tk 绑定到 Lua

Lua 的自然用途是 GUI 的描述——你需要设施来描述对象(小部件)的层次结构,并将用户操作绑定到它们。Lua 适用于此类任务,因为它将数据描述机制与简单、强大且可扩展的语义相结合。事实上,我们已经用 Lua 开发了几个 UI 工具包。

尽管 Tk 是一个通用的 GUI 工具包,但 Tcl 并不是每个人都感到舒适的语言类型。由于我们认为 Lua 是 Tcl 的替代品,因此我们决定实现一个 Tk/Lua 绑定,允许从 Lua 访问 Tk 小部件。

在可能的范围内,我们保留了 Tk 的理念,包括小部件名称、属性和命令。每个人都知道尝试改进现有 API 的诱惑有多大,但从长远来看,让它保持原样对 Tk 用户来说更好,因为他们不必学习新概念。(这对我们来说也更好,因为我们不必编写新手册!)

创建 Tk/Lua 小部件

我们已经将所有 Tk 小部件映射到 Lua。你可以使用 Lua 表构造函数描述其属性来创建小部件。例如,示例 3(a)创建一个按钮并将其存储在b中;b现在是一个表示该按钮的对象。在此定义之后,你可以使用普通的 Lua 语法来操作对象。因此,赋值b.label="Hello world from Lua!" 会更改按钮的标签,如果它已显示在屏幕上,则会更新其图像。可以使用e=entry{width=20}创建限制为 20 个字符的文本输入小部件。在此小部件映射到显示的窗口后,e.current包含用户分配给小部件的当前值。(我们使用 current 字段来存储小部件值,而不是像在 Tcl/Tk 中那样使用全局变量。)

小部件不会自动映射到窗口。与 Tk/Tcl 环境不同,Tk/Lua 中没有当前窗口的概念。你必须创建一个窗口(可以是主窗口或顶级小部件)来容纳其他小部件,然后将其显式映射到屏幕上;请参见示例 3(b)

通过这种方式,用户可以自由描述其对话框,甚至在必要时交叉引用小部件并对其进行映射。我们还消除了显式打包小部件的需要,因为以描述性方式指定布局更加自然。因此,窗口(主窗口和顶级窗口)和小部件框架用作自动打包其内容的容器。例如,要显示带有两个底部按钮的消息,您可以编写示例 3(c)。除了所有常规 Tk 小部件外,我们还实现了两个其他画布,一个使用简化的 API 到 Xlib,另一个使用 OpenGL。

这些库提供的大多数函数都已映射到 Lua。因此,您可以创建使用自定义小部件上的直接操作的复杂图形应用程序——仅在 Lua 中。

访问小部件命令

所有 Tk 小部件命令都作为 Tk/Lua 中的对象方法实现。保留了它们的名称、参数和功能。如果lb表示列表框小部件,则lb:insert("New item")在列表中插入一个新项目,遵循列表框的 Tk insert 命令。另一方面,最常用的 Tk 小部件命令configure不再需要,因为现在可以通过简单赋值获得其效果。

主窗口和小部件继承了窗口管理器的各种方法。如果w表示一个窗口,则w:iconify()具有其通常的效果。

幕后

实现 Tk/Lua 并不难。使用 Tcl/Tk 的 C 接口,我们创建了一个服务提供程序并注册它以从 Lua 访问。实现绑定的 Lua 代码使用面向对象的方法,并带有前面提到的索引后备。每个小部件实例都从一个类对象继承,其中小部件类位于层次结构的顶部。此类提供所有小部件使用的标准方法。清单十显示了此通用类的定义及其将焦点设置为小部件的方法。它还显示了按钮类定义。

如您所知,每个小部件都是使用表构造函数创建的。构造函数设置实例类,创建小部件并将其存储在全局数组中。但是,我们还使用了一个小技巧——构造函数不返回新表,而是将小部件位置作为数字 ID 返回(清单十一)。

因此,当 Lua 尝试对小部件进行索引时,如b.label中所示,它会调用后备,因为数字无法编制索引。此技巧使我们能够完全控制小部件语义。例如,如果b是一个按钮(实际上,b存储一个 ID),并且您设置b.label = "New label,",那么后备负责调用适当的服务命令来更新小部件。

清单十二展示了 Tk/Lua 的“可设置”后备函数。每次尝试索引非表值时,都会调用此后备函数。首先,我们检查第一个参数是否对应于有效的窗口小部件 ID。如果对应,则通过访问全局数组来检索窗口小部件表。否则,我们将事件分派到先前注册的后备函数。

tklua_IDtable 表中的窗口小部件有一个称为 tkname 的内部字段,用于存储相应的 Tk 窗口小部件名称。此名称用于调用 Tk 命令。我们检查是否存在相应的 Tk 窗口小部件,以及索引值是否为有效的 Tk 属性。如果是,我们将要求服务提供商更改窗口小部件属性(调用已注册的 C 函数 tklua_configure)。赋值 h[f]=v 确保我们可以使用窗口小部件表来存储 Tk 属性之外的值。

实现“可获取”后备函数类似。除了这两个后备函数之外,Tk/Lua 还使用索引后备函数来实现继承(清单五)和“函数”后备函数来调用窗口小部件命令或窗口管理器命令。

结论

扩展语言总是以某种方式进行解释。简单的扩展语言可以直接从源代码进行解释。另一方面,嵌入式语言通常是具有复杂语法和语义的强大编程语言。嵌入式语言现在有了一种更有效的实现技术:设计一个适合语言需求的虚拟机,将扩展程序编译成此机器的字节码,然后通过解释字节码来模拟虚拟机。我们为 Lua 的实现选择了这种混合架构,因为词法和句法分析只进行一次,从而实现了更快的执行速度。此外,它允许仅以预编译字节码形式提供扩展程序,从而实现更快的加载速度和更安全的环境。

示例 1:(a)中的表定义等效于(b)。

(a)
     t = {}  -- empty table
     t[1] = i
     t[2] = i*2
     t[3] = i*3
     t[4] = i+j

     s = {}
     s.a = x   -- same as s["a"] = x
     s.b = y

(b)
     t = {i, i*2, i*3, i+j}
     s = {a=x, b=y}

示例 2:(a)中的方法定义等效于(b)。

(a)
     function object:method(params)
       ...
     end

(b)
     function object.method(self, params)
       ...
     end

示例 3:(a)创建和存储按钮;(b)将窗口显式映射到屏幕上;(c)显示带有两个底部按钮的消息。

(a)
     b = button{label = "Hello world!"
                command = "exit(0)"
               }


(b)
     w = toplevel{b}
     w:show()

(c)
     b1 = button{label="Yes", command="yes=1"}
     b2 = button{label="No", command="yes=0"}
     w  = toplevel{message{text="Overwrite file?"},
                   frame{b1, b2; side="left"};
                   side="top"
                  }

表 1:后备条件。

字符串 条件
"arith" 对无效操作数进行算术运算。
"order" 无效操作数上的顺序比较。
"concat" 无效操作数上的字符串连接。
"getglobal" 读取未定义的全局变量的值。
"index" 检索表中不存在的索引的值。
"gettable" 读取非表值中索引的值。
"settable" 在非表值中写入索引的值。
"function" 调用非函数值。
"gc" 在垃圾回收期间为每个被回收的表调用。
"error" 在发生致命错误时调用。

清单一

#include <stdio.h>
#include "lua.h"

int main()
{
 char line[BUFSIZ];
 while (fgets(line,sizeof(line),stdin)!=0)
   lua_dostring(line);
 return 0;
}

清单二

function map(list, func)
  local newlist = {}
  local i = 1
  while list[i] do
    newlist[i] = func(list[i])
    i = i+1
  end
  return newlist
end

清单三

list = {}
i = 4
while i >= 1 do
   list = {head=i,tail=list}
   i = i-1
end

清单四

S = Separator{
     drawStyle = DrawStyle{style = FILLED},
     material =  Material{
       ambientColor  = {0.377, 0.377, 0.377},
       diffuseColor  = {0.800, 0.771, 0.093},
       emissiveColor = {0.102, 0.102, 0.102},
       specularColor = {0.0, 0.0, 0.0}
     },

     transform = Transform{
       translation = {64.293, 20.206, 0.0},
       rotation    = {0.0, 0.0, 0.0, 0.0}
     },
     shape = Sphere{radius = 10.0}
   }

清单五

function Inherit(t,f)
  if f == "parent" then  -- avoid loops
    return nil
  end
  local p = t.parent
  if type(p) == "table" then
    return p[f]
  else
    return nil
  end
end

setfallback("index", Inherit)

清单六

TNumber="number"
TPoint={x=TNumber, y=TNumber}
TColor={red=TNumber, blue=TNumber, green=TNumber}
TRectangle={topleft=TPoint, botright=TPoint}
TWindow={title="string", bounds=TRectangle, color=TColor}

清单七

function checkType(d, t)
  if type(t) == "string" then
    -- t is the name of a type
    return (type(d) == t)
  else
    -- t is a table, so d must also be a table
    if type(d) ~= "table" then
      return nil
    else
      -- d is also a table; check its fields
      local i,v = next(t,nil)
      while i do
        if not checkType(d[i],v) then
          return nil
        end
        i,v = next(t,i)
      end
    end
  end
  return 1
end

清单八

function save()
  -- create table to hold environment

  local env = {}
  -- get first global var and its value
  local n, v = nextvar(nil)
  while n do
    -- save global variable in table
    env[n] = v
    -- get next global var and its value
    n, v = nextvar(n)
  end
  return env
end

清单九

function runProtected(code)
  -- save current environment
  local oldenv = save()
  -- erase "dangerous" functions
  readfrom,writeto,execute = nil,nil,nil
  -- run untrusted code
  dostring(code)
  -- restore original environment
  restore(oldenv)
end

清单十

widgetClass = {}
function widgetClass:focus()
 if self.tkname then
  tklua_setFocus(self.tkname)
 end
end
buttonClass = {
 parent = widgetClass,
 tkwidget = "button"
}

清单十一

function button(self)
 self.parent = classButton
 tklua_ID = tklua_ID + 1
 tklua_IDtable[tklua_ID] = self
 return tklua_ID
end

清单十二

function setFB(id, f, v)
 local h = tklua_IDtable[id]
 if h == nil then
  old_setFB(id,f,v)
  return
 end
 if h.tkname and h:isAttrib(f) then
  tklua_configure(h.tkname,f,v)
 end
 h[f] = v

end
old_setFB = setfallback("settable",setFB)