Lua SPE 论文

转载自软件:实践与经验 26 #6 (1996) 635–652。版权所有 © 1996 John Wiley & Sons, Ltd. [ps · doi]

Lua – 一种可扩展的扩展语言

作者 Roberto Ierusalimschy、Luiz Henrique de Figueiredo、Waldemar Celes Filho

摘要。

本文介绍了 Lua,一种用于扩展应用程序的语言。Lua 通过使用简单但功能强大的机制,将过程特性与强大的数据描述功能相结合。此机制实现了记录、数组和递归数据类型(指针)的概念,并添加了一些面向对象功能,例如具有动态分派的函数。Lua 提供了一种后备机制,允许程序员以一些非常规方式扩展语言的语义。作为一个值得注意的例子,后备允许用户向语言添加不同种类的继承。目前,Lua 已广泛用于生产中,用于多项任务,包括用户配置、通用数据输入、用户界面描述、结构化图形元文件的存储以及有限元网格的通用属性配置。

简介

对可定制应用程序的需求日益增长。随着应用程序变得越来越复杂,使用简单参数进行定制变得不可能:用户现在希望在执行时做出配置决策;用户还希望编写宏和脚本以提高生产力 [1,2,3,4]. 为了满足这些需求,如今有一个重要的趋势是将复杂系统分为两部分:内核配置。内核实现了系统的基本类和对象,通常使用编译的静态类型语言(如 C 或 Modula-2)编写。配置部分通常使用解释的灵活语言编写,将这些类和对象连接起来,为应用程序提供最终形状 [5].

配置语言有多种类型,从用于选择偏好的简单语言(通常作为命令行中的参数列表或从配置文件中读取的变量值对实现,例如 MS-Windows 的 .ini 文件、X11 资源文件)到用于扩展应用程序(基于应用程序提供的基元,使用用户定义函数)的嵌入式语言。嵌入式语言非常强大,有时是 Lisp 和 C 等主流编程语言的简化变体。此类配置语言也称为扩展语言,因为它们允许使用新的用户定义功能扩展基本内核语义。

扩展语言与独立语言的不同之处在于,它们只能嵌入到称为宿主程序的主机客户端中才能工作。此外,宿主程序通常可以提供特定于领域的扩展,以根据自己的目的自定义嵌入式语言,通常是通过提供更高级别的抽象。为此,嵌入式语言既有其自身程序的语法,又有用于与宿主通信的应用程序编程接口 (API)。与用于向宿主提供参数值和动作序列的更简单的配置语言不同,嵌入式语言和宿主程序之间存在双向通信。

必须注意,扩展语言的要求与通用编程语言的要求不同。扩展语言的主要要求是

本文介绍 Lua,这是一种可扩展的过程语言,具有强大的数据描述功能,旨在用作通用扩展语言。Lua 是两种描述性语言融合而成的,用于配置两个特定应用程序:一个用于科学数据输入 [6],另一个用于可视化从地质探测器获得的岩性剖面。当用户开始要求这些语言提供越来越多的功能时,很明显需要真正的编程功能。解决方案不是并行升级和维护两种不同的语言,而是设计一种不仅可用于这两个应用程序,而且可用于任何其他应用程序的单一语言。因此,Lua 结合了大多数过程编程语言共有的功能(控制结构(whileif 等)、赋值、子例程和中缀运算符),但抽象出了特定于任何特定领域的设施。通过这种方式,Lua 不仅可以用作完整的语言,还可以用作语言框架

Lua 很好地满足了上述要求。它的语法和控制结构非常简单,类似于 Pascal。Lua 很小;整个库大约有六千行 ANSI C 代码,其中近两千行由 yacc 生成。最后,Lua 是可扩展的。在它的设计中,许多不同功能的添加已被创建一些元机制所取代,这些元机制允许程序员自己实现这些功能。这些元机制是:动态关联数组反射机制后备

动态关联数组直接实现多种数据类型,如普通数组、记录、集合和包。它们还通过构造函数提升了语言的数据描述能力。

反射机制允许创建高度多态的部分。持久性和多个名称空间是 Lua 中不存在但可以使用反射机制在 Lua 本身中轻松实现的功能示例。

最后,尽管 Lua 具有固定的语法,但后备可以扩展许多语法结构的含义。例如,后备可用于实现不同类型的继承,这是 Lua 中不存在的功能。

Lua 概述

本节简要介绍了 Lua 中的主要概念。包括一些实际代码示例,以展示该语言的风格。可以在其参考手册 [7] 中找到该语言的完整定义。

Lua 是一种通用的嵌入式编程语言,旨在支持具有数据描述功能的过程编程。作为一种嵌入式语言,Lua 没有“主”程序的概念;它只能嵌入在宿主客户端中。Lua 以 C 函数库的形式提供,可链接到宿主应用程序。宿主可以调用库中的函数来执行 Lua 中的一段代码、写入和读取 Lua 变量,以及注册由 Lua 代码调用的 C 函数。此外,可以指定后备,以便在 Lua 不知道如何继续时调用后备。通过这种方式,可以扩展 Lua 以应对不同的域,从而创建共享单个语法框架的定制编程语言 [8]。正是在这个意义上,Lua 是一个语言框架。另一方面,为 Lua 编写一个交互式独立解释器非常容易(图 1)。

      #include <stdio.h>
      #include "lua.h"              /* lua header file */
      #include "lualib.h"           /* extra libraries (optional) */

      int main (int argc, char *argv[])
      {
       char line[BUFSIZ];
       iolib_open();               /* opens I/O library (optional) */
       strlib_open();              /* opens string lib (optional) */
       mathlib_open();             /* opens math lib (optional) */
       while (gets(line) != 0)
         lua_dostring(line);
      }

图 1:Lua 的交互式解释器。

Lua 中的所有语句都在全局环境中执行,该环境保留所有全局变量和函数。该环境在宿主程序开始时初始化,并持续到其结束。

Lua 的执行单元称为。块可以包含语句和函数定义。执行块时,首先编译其所有函数和语句,并将函数添加到全局环境中;然后按顺序执行语句。

图 2 展示了如何将 Lua 用作非常简单的配置语言的示例。此代码定义了三个全局变量并为其分配值。Lua 是一种动态类型语言:变量没有类型;只有值才有。所有值都带有自己的类型。因此,Lua 中没有类型定义。

      width = 420
      height = width*3/2     -- ensures 3/2 aspect ratio
      color = "blue"

图 2:一个非常简单的配置文件。

可以使用流控制和函数定义编写更强大的配置。Lua 使用类似 Pascal 的传统语法,带有保留字和明确终止的块;分号是可选的。这种语法很熟悉、很健壮,并且易于解析。图 3 中展示了一个小示例。请注意,函数可以返回多个值,并且可以使用多个赋值来收集这些值。因此,通过引用传递参数(始终是语义上的小困难的来源)可以从语言中丢弃。

      function Bound (w, h)
        if w < 20 then w = 20
        elseif w > 500 then w = 500
        end
        local minH = w*3/2             -- local variable
        if h < minH then h = minH end
        return w, h
      end

      width, height = Bound(420, 500)
      if monochrome then color = "black" else color = "blue" end

图 3:使用函数的配置文件。

Lua 中的函数是一等值。函数定义创建类型为function的值,并将此值分配给全局变量(图 3 中的Bound)。与任何其他值一样,函数值可以存储在变量中,作为参数传递给其他函数,并作为结果返回。此特性极大地简化了面向对象设施的实现,如本节稍后所述。

除了基本类型number(浮点数)和string以及类型function之外,Lua 还提供了三种其他数据类型:niluserdatatable。每当需要显式类型检查时,可以使用原始函数type;它返回一个描述其参数类型的字符串。

类型nil有一个值,也称为nil,其主要属性是与任何其他值不同。在第一次赋值之前,变量的值为nil。因此,未初始化的变量(编程错误的主要来源)在 Lua 中不存在。在需要实际值的情况下使用nil(例如,在算术表达式中)会导致执行错误,提醒程序员该变量未正确初始化。

提供类型userdata以允许将表示为void* C 指针的任意主机数据存储在 Lua 变量中。对这种类型的值的唯一有效操作是赋值和相等性测试。

最后,类型table实现了关联数组,即不仅可以用整数索引,还可以用字符串、实数、表和函数值索引的数组。

关联数组

关联数组是一种强大的语言结构;许多算法简化到微不足道的程度,因为搜索它们所需的数据结构和算法由语言隐式提供 [9]。大多数典型的数据容器,如普通数组、集合、包和符号表,都可以通过表直接实现。表还可以通过简单地使用字段名称作为索引来模拟记录。Lua 通过提供 a.name 作为 a["name"] 的语法糖来支持此表示形式。

与实现关联数组的其他语言(如 AWK [10]、Tcl [11] 和 Perl [12])不同,Lua 中的表不绑定到变量名;相反,它们是动态创建的对象,可以像传统语言中的指针一样进行操作。这种选择的不利之处在于,必须在使用前显式创建表。优点是表可以自由地引用其他表,因此具有表达能力来建模递归数据类型,并创建通用的图形结构,可能带有循环。例如,图 4 展示了如何在 Lua 中构建循环链表。

      list = {}                    -- creates an empty table
      current = list
      i = 0
      while i < 10 do
        current.value = i
        current.next = {}
        current = current.next
        i = i+1
      end
      current.value = i
      current.next = list

图 4:Lua 中的循环链表。

Lua 提供了许多创建表的方法。最简单的形式是表达式 {},它返回一个新的空表。下面显示了一种更具描述性的方法,它创建了一个表并初始化一些字段;语法在某种程度上受到 BibTeX [13] 数据库格式的启发

      window1 = {x = 200, y = 300, foreground = "blue"}

此命令创建一个表,初始化其字段 xyforeground,并将其分配给变量 window1。请注意,表不必是同质的;它们可以同时存储所有类型的值。

可以使用类似的语法创建列表

      colors = {"blue", "yellow", "red", "green", "black"}

此语句等效于

      colors = {}
      colors[1] = "blue";  colors[2] = "yellow"; colors[3] = "red"
      colors[4] = "green"; colors[5] = "black"

有时,需要更强大的构造设施。Lua 不会尝试提供所有内容,而是提供了一个简单的构造函数机制。构造函数写为 name{...},它只是 name({...}) 的语法糖。因此,使用构造函数,将创建一个表,对其进行初始化,并将其作为参数传递给函数。此函数可以执行任何所需的初始化,例如(动态)类型检查、不存在字段的初始化和辅助数据结构更新,甚至在宿主程序中。通常,构造函数是预先定义的,在 C 或 Lua 中,并且配置用户通常不知道构造函数是一个函数;他们只需编写类似以下内容的内容

      window1 = Window{ x = 200, y = 300, foreground = "blue" }

并考虑“窗口”和其他高级抽象。因此,尽管 Lua 是动态类型的,但它提供了用户控制的类型构造函数

因为构造函数是表达式,所以它们可以嵌套以声明性风格描述更复杂的结构,如下面的代码所示

      d = dialog{
                 hbox{
                      button{ label = "ok" },
                      button{ label = "cancel" }
                 }
          }

反射机制

Lua 的另一个强大机制是它使用内置函数 next 遍历表的能力。此函数有两个参数:要遍历的表和此表的索引。当索引为 nil 时,该函数返回给定表的第一个索引和与此索引关联的值;当索引不为 nil 时,该函数返回下一个索引及其值。索引按任意顺序检索,并返回 nil 索引以表示遍历结束。作为使用 Lua 遍历机制的一个示例,图 5 显示了用于克隆对象的例程。局部变量 i 遍历对象 o 的索引,而 v 接收它们的值。这些值与其对应的索引关联,存储在局部表 new_o 中。

   function clone (o)
     local new_o = {}           -- creates a new object
     local i, v = next(o,nil)   -- get first index of "o" and its value
     while i do
       new_o[i] = v             -- store them in new table
       i, v = next(o,i)         -- get next index and its value
     end
     return new_o
   end

图 5:克隆通用对象的函数。

next 遍历表的方式与之类似,相关函数 nextvar 遍历 Lua 的全局变量。图 6 展示了一个将 Lua 的全局环境保存在表中的函数。与函数 clone 一样,局部变量 n 遍历所有全局变量的名称,而 v 接收它们的值,这些值存储在局部表 env 中。退出时,函数 save 返回此表,稍后可以将其提供给函数 restore 以恢复环境(图 7)。此函数有两个阶段。首先,擦除整个当前环境,包括预定义函数。然后,局部变量 nv 遍历给定表的索引和值,将这些值存储在相应的全局变量中。一个棘手的问题是,restore 调用的函数必须保存在局部变量中,因为所有全局名称都会被擦除。

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

图 6:保存 Lua 环境的函数。

   function restore (env)
     -- save some built-in functions before erasing global environment
     local nextvar, next, setglobal = nextvar, next, setglobal
     -- erase all global variables
     local n, v = nextvar(nil)
     while n do
       setglobal(n, nil)
       n, v = nextvar(n)
     end
     -- restore old values
     n, v = next(env, nil)      -- get first index; v = env[n]
     while n do
      setglobal(n, v)           -- set global variable with name n
      n, v = next(env, n)
     end
   end

图 7:恢复 Lua 环境的函数。

尽管这是一个有趣的示例,但很少需要在 Lua 中操作全局环境,因为用作对象的表提供了维护多个环境的更好方法。

支持面向对象编程

因为函数是一等值,所以表字段可以引用函数。此属性允许实现一些有趣的面向对象机制,通过用于定义和调用方法的语法糖可以简化这些机制。

首先,方法定义可以写成

      function object:method (params)
        ...
      end

它等同于

      function dummy_name (self, params)
        ...
      end
      object.method = dummy_name

也就是说,一个匿名函数被创建并存储在一个表字段中;此外,这个函数有一个名为self的隐藏参数。

其次,一个方法调用可以写成

      receiver:method(params)

它被翻译为

      receiver.method(receiver,params)

换句话说,方法的接收者作为其第一个参数传递,给参数self期望的含义。

值得注意上述构造的一些特性。首先,它不提供信息隐藏。因此,纯粹主义者可能(正确地)声称对象定向的一个重要部分缺失了。其次,它不提供类;每个对象都携带其操作。然而,这个构造极其轻量(仅为语法糖),并且类可以使用继承来模拟,这在其他基于原型的语言中很常见,例如 Self [14]。但是,在讨论继承之前,有必要讨论回退。

回退

作为一门非类型化语言,Lua 具有语义,其中包含许多运行时异常条件。示例包括应用于非数字操作数的算术运算、尝试索引非表值或尝试调用非函数值。由于在这些情况下停止对于嵌入式语言不合适,因此 Lua 允许程序员设置自己的函数来处理错误条件;此类函数称为回退函数。回退还用于提供钩子来处理严格来说不是错误条件的其他情况,例如访问表中不存在的字段和发出垃圾回收信号。

要设置回退函数,程序员需要调用函数setfallback,并提供两个参数:一个标识回退的字符串以及在发生相应条件时要调用的新函数。函数setfallback返回旧的回退函数,因此程序可以为不同类型的对象链接回退。

Lua 支持以下回退,由给定的字符串标识

"arith", "order", "concat"
当对无效操作数应用操作时,会调用这些回退。它们接收三个参数:两个操作数和一个描述受影响运算符("add""sub"、...)的字符串。它们的返回值是操作的最终结果。这些回退的默认函数会发出错误。
"index"
当 Lua 尝试检索表中不存在的索引的值时,会调用此回退。它接收表和索引作为参数。它的返回值是索引操作的最终结果。默认函数返回nil
"gettable", "settable"
当 Lua 尝试读取或写入非表值中的索引值时调用。默认函数会发出错误。
"function"
当 Lua 尝试调用非函数值时调用。它将非函数值和原始调用中给出的参数作为参数接收。其返回值是调用操作的最终结果。默认函数会发出错误。
"gc"
在垃圾回收期间调用。它将正在回收的表作为参数接收,并将 `nil` 作为垃圾回收结束的信号接收。默认函数不执行任何操作。

在继续之前,请注意,回退通常不是由普通 Lua 程序员设置的。回退主要由专家程序员在将 Lua 绑定到特定应用程序时使用。之后,该工具将作为语言的一个组成部分使用。作为一个典型的示例,大多数实际应用程序都使用回退来实现继承(如下所述),但大多数 Lua 程序员在使用继承时甚至不知道(或不在乎)它是如何实现的。

使用回退

图 8 显示了一个使用回退来允许更面向对象的二元运算符解释样式的示例。设置此回退后,表达式(如 `a+b`,其中 `a` 是一个表)将作为 `a:add(b)` 执行。请注意全局变量 `oldFallback` 的使用,用于链接回退函数。

      function dispatch (receiver, parameter, operator)
        if type(receiver) == "table" then
          return receiver[operator](receiver, parameter)
        else
          return oldFallback(receiver, parameter, operator)
        end
      end

      oldFallback = setfallback("arith", dispatch)

图 8:回退示例。

回退提供的另一个不寻常的工具是重用 Lua 的解析器。许多应用程序将受益于算术表达式解析器,但并未包含一个,因为并非每个人都具备所需的专业知识或倾向来从头编写解析器或使用 yacc 等解析器生成器。图 9 显示了使用回退的表达式解析器的完整实现。此程序读取变量 `a`、...、`z` 上的算术表达式,并输出评估表达式所需的一系列基本操作,使用变量 `t1`、`t2`、... 作为临时变量。例如,为表达式生成的代码

      (a*a+b*b)*(a*a-b*b)/(a*a+b*b+c)+(a*(b*b)*c)

      t1=mul(a,a)      t2=mul(b,b)      t3=add(t1,t2)
      t4=sub(t1,t2)    t5=mul(t3,t4)    t6=add(t3,c)
      t7=div(t5,t6)    t8=mul(a,t2)     t9=mul(t8,c)
      t10=add(t7,t9)

此程序的主要部分是函数 arithfb,它被设置为算术运算的备用函数。函数 create 用于使用表格初始化变量 a、...、z,每个表格都有一个包含变量名称的字段 name。此初始化后,一个循环读取包含算术表达式的行,构建对变量 E 的赋值,并将其传递给 Lua 解释器,调用 dostring。每次解释器尝试执行类似 a*a 的代码时,它都会调用 "arith" 备用函数,因为 a 的值是一个表格,而不是一个数字。备用函数创建一个临时变量来存储每个基本算术运算结果的符号表示。

尽管这段代码很小,但它实际上执行全局公共子表达式识别并生成优化代码。请注意上面的示例中,a*a+b*ba*a-b*b 如何基于 a*ab*b 的单次评估进行评估。还要注意,a*a+b*b 只评估一次。代码优化只需通过将先前计算的值缓存在表格 T 中即可完成,该表格由基本运算的文本表示进行索引,其值是包含结果的临时变量。例如,T["mul(a,a)"] 的值为 t1

图 9 中的代码可以很容易地修改以处理加法和乘法的交换性以及减法和除法的反对易性。将其更改为输出后缀表示或其他格式也很容易。

在实际应用程序中,变量 a、...、z 将表示应用程序对象,例如复数、矩阵甚至图像,而 "arith" 备用函数将调用应用程序函数对这些对象执行实际计算。因此,Lua 解析器的主要用途是允许程序员使用熟悉的算术表达式来表示对应用程序对象的复杂计算。

      n=0                            -- counter of temporary variables
      T={}                           -- table of temporary variables

      function arithfb(a,b,op)
       local i=op .. "(" .. a.name .. "," .. b.name .. ")"
       if T[i]==nil then             -- expression not seen yet
         n=n+1
         T[i]=create("t"..n)         -- save result in cache
         print(T[i].name ..'='..i)
       end
       return T[i]
      end

      setfallback("arith",arithfb)   -- set arithmetic fallback

      function create(v)             -- create symbolic variable
       local t={name=v}
       setglobal(v,t)
       return t
      end

      create("a") create("b") create("c") ... create("z")

      while 1 do                     -- read expressions
       local s=read()
       if (s==nil) then exit() end
       dostring("E="..s)             -- execute fake assignment
       print(s.."="..E.name.."\n")
      end

图 9:Lua 中的优化算术表达式编译器。

通过备用函数进行继承

当然,回退最有趣的用途之一是在 Lua 中实现继承。简单的继承允许对象在另一个对象(称为其父级)中查找不存在的字段的值;特别是,此字段可以是方法。这种机制是一种对象继承,与 Smalltalk 和 C++ 中采用的更传统的类继承形成对比。在 Lua 中实现简单继承的一种方法是将父对象存储在一个特殊字段中,例如称为parent,并设置一个索引回退函数,如图 10 所示。此代码定义了一个函数 Inherit 并将其设置为"index"回退。每当 Lua 尝试访问对象中不存在的字段时,回退机制都会调用函数 Inherit。此函数首先检查对象是否具有包含表值的字段 parent。如果是,它将尝试访问此父对象中的所需字段。如果此字段不存在于父级中,则会自动再次调用回退;此过程会向上重复,直到找到字段的值或父级链结束。

      function Inherit (object, field)
        if field == "parent" then     -- avoid loops
          return nil
        end
        local p = object.parent       -- access parent object
        if type(p) == "table" then    -- check if parent is a table
          return p[field]             -- (this may call Inherit again)
        else
          return nil
        end
      end

      setfallback("index", Inherit)

图 10:在 Lua 中实现简单继承。

上述方案允许无穷无尽的变化。例如,只能继承方法,或只能继承以下划线开头的字段。还可以实现多种形式的多重继承。其中,一种经常使用形式是双重继承。在此模型中,每当在父级层次结构中找不到字段时,搜索都会通过一个备用父级(通常称为"godparent")继续进行。在大多数情况下,一个额外的父级就足够了。此外,双重继承可以对通用的多重继承进行建模。例如,在下面的代码中,a按此顺序从a1a2a3继承

      a = {parent = a1, godparent = {parent = a2, godparent = a3}}

Lua 在实际应用中的使用

TeCGraf 是里约热内卢教皇天主教大学 (PUC-Rio) 的一个研发实验室,拥有许多工业合作伙伴。在过去的两年中,TeCGraf 的大约四十名程序员使用 Lua 开发了多个实质性产品。本节描述了其中一些用途。

岩性剖面可配置报告生成器

正如引言中提到的,Lua 最初是为了支持两个不同的应用程序而出现的,这两个应用程序有自己的扩展语言,但受到限制。其中一个应用程序是用于可视化从地质探测中获得的岩性剖面的工具。其主要特点是允许用户配置剖面布局,组合对象实例并指定要显示的数据。该程序支持多种类型的对象,例如连续曲线、直方图、岩性表示、比例等。

要构建一个布局,用户可以编写描述这些对象的 Lua 代码(图 11)。应用程序本身也有 Lua 代码,它允许通过图形用户界面创建此类描述。此功能是在 EDG 框架之上构建的,如下所述。

      Grid{
        name = "log",
        log = TRUE,
        h_step = 25,
        v_step = 25,
        v_tick = 5,
        step_line = Line {color = RED, width = SIMPLE},
        tick_line = Line {color = CORAL}
      }

图 11:Lua 中岩性剖面对象的描述。

存储结构化图形元文件

Lua 的另一个重要用途是存储结构化图形元文件。由 TeCGraf 开发的通用绘图编辑器 TeCDraw 保存元文件,其中包含用 Lua 描述构成绘图的图形对象的较高层次的描述。图 12 说明了这些描述。

      line{
         x = { 0.0, 1.0 },
         y = { 5.0, 8.0 },
         color = RED
      }
      text{
         x = 0.8,
         y = 0.5,
         text = 'an example of text',
         color = BLUE
      }
      circle{
         x = 1.0,
         y = 1.0,
         r = 5.0
      }

图 12:结构化图形元文件中的摘录。

此类通用结构化元文件为开发带来了若干好处

高级通用图形数据输入

Lua 功能也在 EDG 的实现中得到广泛利用,EDG 是一个用于支持数据输入程序开发的系统,具有较高的抽象级别。该系统提供界面对象(如按钮、菜单、列表)和图形对象(如线、圆和基元组)的处理。因此,程序员可以在较高的抽象编程级别构建复杂界面对话框。程序员还可以将回调操作与图形对象关联,从而创建对用户输入进行过程化反应的活动对象。

如上所述,EDG 系统使用 Lua 回退功能来实现双重继承。因此,可以构建新的界面和图形对象,继承原始对象的行为。EDG 中存在的继承的另一个有趣用法是跨语言继承。EDG 建立在可移植用户界面工具包 IUP [15] 之上。为了避免在 Lua 中复制驻留在主机中的 IUP 数据,EDG 使用回退来获取“可获取的”和“可设置的”以直接从 Lua 访问工具包中的字段。因此,可以使用直观的记录语法直接访问主机数据,而无需为主机中的每个导出数据项创建访问函数。

EDG 系统已用于开发多个数据输入程序。在许多工程系统中,完整分析分为三个步骤:数据输入,称为预处理;分析本身,称为处理或仿真;结果报告和验证,称为后处理。通过绘制必须指定为分析输入的数据的图形表示,可以使数据输入任务变得更容易。对于此类应用程序,EDG 系统非常有用,并为定制数据输入提供了一个快速开发工具。这些图形数据输入工具为批处理仿真程序的遗留代码注入了新的活力。

有限元网格的通用属性配置

Lua 被用于的另一个工程领域是有限元网格的生成。有限元网格由节点和单元组成,它们分解了分析域。为了完成模型,必须将物理属性(属性)与节点和单元相关联,例如材料类型、支撑条件和载荷工况。必须指定的一组属性根据要执行的分析而有很大差异。因此,为了实现通用的有限元网格生成器,建议用户保持属性的可配置性,而不是在程序中硬编码。

ESAM [16] 是一个通用系统,它使用 Lua 为属性配置提供支持。与 EDG 一样,ESAM 采用面向对象的方法:用户创建从预定义核心类派生的特定属性。图 13 显示了如何创建一种称为“各向同性”的新材料的示例。

      ISO_MAT = ctrclass{ parent = MATERIAL,
                           name = "Isotropic",
                           vars = {"e", "nu"}
                }

      function ISO_MAT:CrtDlg ()
        ...  -- creates a dialog to specify this material
      end

图 13:在 ESAM 中创建新材料。

相关工作

本节讨论了一些其他扩展语言,并将它们与 Lua 进行比较。无意面面俱到;相反,选择了一些当前扩展语言趋势的代表:Scheme、Tcl 和 Python。可以在互联网上找到嵌入式语言的综合列表 [17]。本节还将回退机制与一些其他语言机制进行比较。

Lisp 方言,尤其是 Scheme,一直是扩展语言的热门选择,因为它们简单、易于解析的语法和内置的可扩展性 [8,18,19]。例如,文本编辑器 Emacs 的主要部分实际上是用它自己的 Lisp 变体编写的;其他一些文本编辑器也遵循了同样的路径。目前有许多以库形式实现的 Scheme,专门设计为嵌入式语言(例如,libscheme [18]、OScheme [20] 和 Elk [3])。但是,在定制方面,Lisp 无法称之为用户友好。对于非程序员来说,它的语法相当粗糙。此外,很少有 Lisp 或 Scheme 的实现真正具有可移植性。

另一种现今非常流行的扩展语言是 Tcl [11]。毫无疑问,它成功的理由之一是 Tk 的存在,Tk 是一个用于构建图形用户界面的强大 Tcl 工具包。Tcl 具有非常原始的语法,这极大地简化了它的解释器,但也使编写稍微复杂的结构变得复杂。例如,Tcl 代码将变量 A 的值加倍为 set A [expr $A*2]。Tcl 支持一个原始类型,即字符串。这一事实,加上没有预编译,使 Tcl 即使作为扩展语言也相当低效。正如 TC [21] 所示,纠正这些问题可以将 Tcl 的效率提高 5 到 10 倍。Lua 具有更合适的数据类型和预编译,比 Tcl 运行速度快 10 到 20 倍。一个简单的测试表明,在 Sparcstation 1 中运行的 Tcl 7.3 中不带参数的程序调用大约花费 44 微秒,而全局变量的增量则需要 76 微秒。在 Lua v. 2.1 中,相同的操作分别花费 6 微秒和 4 微秒。另一方面,Lua 比 C 慢大约 20 倍。这似乎是解释型语言的典型值 [22]。

Tcl 没有内置控制结构,例如whileif。相反,控制结构可以通过延迟评估进行编程,就像 Smalltalk 中一样。虽然功能强大且优雅,但可编程控制结构可能导致非常晦涩的程序,并且在实践中很少使用。此外,它们通常会带来很高的性能损失。

Python [23] 是一种有趣的新语言,也已被提议作为扩展语言。然而,根据其作者自己的说法,仍然需要“改进对 Python 在其他应用程序中的嵌入支持,例如,通过将大多数全局符号重命名为带有 `Py' 前缀” [24]。Python 不是一种微型语言,并且具有扩展语言中不必要的许多特性,例如模块和异常处理。这些特性增加了使用该语言的应用程序的额外成本。

Lua 被设计为结合现有语言的优点,以实现其作为可扩展扩展语言的目标。与 Tcl 一样,Lua 是一个小型库,具有一个简单的 C 接口;此接口是一个包含 100 行的单一头文件。然而,与 Tcl 不同的是,Lua 被预编译为标准字节码中间形式。与 Python 一样,Lua 具有简洁但熟悉的语法,以及内置的对象概念。与 Lisp 一样,Lua 具有一个单一的数据结构机制(表),功能强大到足以高效地实现大多数数据结构。表使用哈希实现。冲突通过线性探测处理,当表变得超过 70% 满时自动重新分配和重新哈希。哈希值被缓存以提高访问性能。

Lua 中提供的后备机制可以看作是一种带有恢复功能的异常处理机制 [25]。但是,Lua 的动态特性允许在许多情况下使用它,而在这些情况下,静态类型语言会在编译时发出错误;上面提供的两个示例就是这种情况。三个特定的后备 "arith""order""concat" 主要用于实现重载。特别是,图 9 中的示例可以很容易地翻译成其他具有重载功能的语言,例如 Ada 或 C++。但是,由于其动态特性,后备比异常处理或重载机制更加灵活。另一方面,一些作者 [26] 认为使用这些机制的程序往往难以验证、理解和调试;使用后备时,这些困难会变得更糟。后备应该谨慎适度地编写,并且只能由专家程序员编写。

结论

对配置应用程序的需求不断增长正在改变程序的结构。如今,许多程序是用两种不同的语言编写的:一种用于编写功能强大的“虚拟机”,另一种用于为此机器编写单个程序。Lua 是一种专门为后者任务设计的语言。它很小:如前所述,整个库大约有六千行 ANSI C 代码。它具有可移植性:Lua 正用于从 PC-DOS 到 CRAY 的各种平台中。它具有简单的语法和简单的语义。而且它很灵活。

这种灵活性是通过一些不寻常的机制实现的,这些机制使语言具有很强的可扩展性。在这些机制中,我们强调以下内容

关联数组是一种强大的统一数据构造器。此外,它允许比其他统一构造器(如字符串或列表)更有效的算法。与实现关联数组的其他语言 [10,11,12] 不同,Lua 中的表是具有标识的动态创建的对象。这极大地简化了将表用作对象以及添加面向对象功能。

后备允许程序员扩展大多数内置操作的含义。特别是,通过索引操作的后备,可以向语言添加不同类型的继承,而 "arith" 和其他运算符的后备可以实现动态重载。

用于数据结构遍历的反射设施有助于生成高度多态的代码。在其他系统中必须作为基本类型提供或针对每种新类型单独编码的许多操作,可以在 Lua 中以单一通用形式进行编程。例如,克隆对象和操作全局环境。

除了在多个工业应用中使用 Lua,我们目前正在多个研究项目中对 Lua 进行试验,从使用分布式对象(它们相互发送包含 Lua 代码的消息)进行计算 [27](Tcl 中之前提出的一个想法 [4]),到使用客户端 Lua 代码透明地扩展 WWW 浏览器。由于所有 Lua 与操作系统交互的函数都在外部库中提供,因此很容易限制解释器的功能以提供足够的安全性。

我们还计划改进 Lua 的调试设施;目前,仅提供简单的堆栈回溯。遵循提供强大元机制的理念,该机制允许程序员构建自己的扩展,我们计划向运行时系统添加简单的挂钩,以允许用户程序在发生重要事件时(例如进入或退出函数、执行一行用户代码等)收到通知。可以在这些基本挂钩之上构建不同的调试接口。此外,这些挂钩对于构建其他工具(例如用于性能分析的分析器)也很有用。

本文中描述的 Lua 实现可在以下网址的互联网上获得

      https://lua.ac.cn/ftp/lua-2.1.tar.gz

致谢

我们要感谢 ICAD 和 TeCGraf 的工作人员使用和测试 Lua,以及 John Roll,感谢他通过邮件就 Lua 以前版本中的回退提出了宝贵的建议。文中提到的工业应用正在与巴西石油公司 (PETROBRAS) 和巴西电力公司 (ELETROBRAS) 的研究中心合作开发。作者部分得到巴西政府(CNPq 和 CAPES)的研究和开发补助金的支持。Lua 在葡萄牙语中意为月亮

参考文献

[1] B. Ryan,“不受限制的脚本”,Byte15(8),235–240 (1990)。

[2] N. Franks,“向您的软件添加扩展语言”,Dr. Dobb's Journal16(9),34–43 (1991)。

[3] O. Laumann 和 C. Bormann。Elk:扩展语言工具包。 ftp://ftp.cs.indiana.edu:/pub/scheme-repository/imp/elk-2.2.tar.gz,德国柏林工业大学。

[4] J. Ousterhout,“Tcl:一种可嵌入的命令语言”,1990 年冬季 USENIX 会议论文集。USENIX 协会,1990 年。

[5] D. Cowan、R. Ierusalimschy 和 T. Stepien,“面向最终用户的编程环境”,第 12 届世界计算机大会。IFIP,1992 年 9 月,第 54-60 页 A-14 卷。

[6] L. H. Figueiredo、C. S. Souza、M. Gattass 和 L. C. Coelho,“用于捕获设计数据的界面生成”,第五届 SIBGRAPI,1992 年,第 169-175 页。

[7] R. Ierusalimschy、L. H. Figueiredo 和 W. Celes,“Lua 编程语言 2.1 版参考手册”,计算机科学专著 08/95,巴西里约热内卢天主教大学,巴西里约热内卢,1995 年。(可通过 ftp 在 ftp.inf.puc-rio.br/pub/docs/techreports 获取)。

[8] B. Beckman,“交互式图形中小型语言的方案”,软件、实践与经验21,187-207(1991)。

[9] J. Bentley,更多编程精华,艾迪生-韦斯利,1988 年。

[10] A. V. Aho、B. W. Kerninghan 和 P. J. Weinberger,AWK 编程语言,艾迪生-韦斯利,1988 年。

[11] J. K. Ousterhout,Tcl 和 Tk 工具包,艾迪生-韦斯利,1994 年。

[12] L. Wall 和 R. L. Schwartz,Perl 编程,O'Reilly & Associates,Inc.,1991 年。

[13] L. Lamport,LaTeX:文档准备系统,艾迪生-韦斯利,1986 年。

[14] D. Ungar 等,“Self:简单的力量”,Sigplan 公告22(12),227-242(1987)(OOPSLA'87)。

[15] C. H. Levy、L. H. de Figueiredo、C. J. Lucena 和 D. D. Cowan。“IUP/LED:一种可移植的用户界面开发工具”,软件:实践与经验 26 #7(1996)737-762。

[16] M. T. de Carvalho 和 L. F. Martha,“几何建模器配置的架构:应用于计算力学”,PANEL95 - 第二十一届拉丁美洲计算机会议,1995 年,第 123-134 页。

[17] C. Nahaboo。嵌入式语言目录。 ftp://koala.inria.fr:/pub/EmbeddedInterpretersCatalog.txt

[18] B. W. Benson Jr.,“libscheme:Scheme 作为 C 库”,1994 年 USENIX 超高级语言研讨会论文集。USENIX,1994 年 10 月,第 7-19 页。

[19] A. Sah 和 J. Blow,“脚本语言实现的新架构”,USENIX 非常高级语言研讨会论文集,1994 年。

[20] A. Baird-Smith。“OScheme 手册”。http://www.inria.fr/koala/abaird/oscheme/manual.html,1995 年。

[21] A. Sah,“TC:Tcl 语言的高效实现”,硕士论文,加州大学伯克利分校,计算机科学系,伯克利,加利福尼亚州,1994 年。

[22] Sun Microsystems,Java 语言,1995 年。http://java.sun.com/people/avh/talk.ps

[23] G. van Rossum,“为 UNIX/C 程序员介绍 Python”,UUG najaarsconferentie 论文集。荷兰 UNIX 用户组,1993 年。(ftp://ftp.cwi.nl/pub/python/nluug-paper.ps)。

[24] G. van Rossum。Python 常见问题,版本 1.20++。ftp://ftp.cwi.nl/pub/python/python-FAQ,1995 年 3 月。

[25] S. Yemini 和 D. Berry,“模块化可验证异常处理机制”,ACM 编程语言和系统事务7(2)(1985 年)。

[26] A. Black,“异常处理:反对意见”,博士论文,牛津大学,1982 年。

[27] R. Cerqueira、N. Rodriguez 和 R. Ierusalimschy,“面向事件的分布式编程体验”,PANEL95 - 第二十一届拉丁美洲信息学会议,1995 年,第 225-236 页。