脚本语言是当前编程语言领域中的一个重要元素。脚本语言的一个关键特性是它与系统语言集成的能力。7这种整合主要有两种形式:扩展而且嵌入.在第一种形式中,使用用系统语言编写的库和函数扩展脚本语言,并用脚本语言编写主程序。在第二种形式中,您将脚本语言嵌入到宿主程序(用系统语言编写)中,以便宿主可以运行脚本并调用脚本中定义的函数;主程序就是主程序。在这种设置中,系统语言通常称为宿主语言。
许多语言(不一定是脚本语言)支持通过外国函数接口(FFI)。FFI还不足以让系统语言中的函数做脚本中函数能做的所有事情。然而,在实践中FFI满足了大多数常见的扩展需求,例如对外部库的访问和系统调用。另一方面,嵌入则更难支持,因为它通常要求宿主程序和脚本之间更紧密地集成,而仅靠FFI是不够的。
在本文中,我们将讨论可嵌入性如何影响语言的设计,特别是它如何从一开始就影响Lua的设计。Lua3.,4是一种特别强调可嵌入性的脚本语言。它被嵌入到广泛的应用程序中,是脚本游戏的主要语言。2
乍一看,脚本语言的可嵌入性似乎是其解释器实现的一个特性。对于任何解释器,我们都可以将API附加到它上面,以允许宿主程序和脚本进行交互。然而,语言本身的设计对它的嵌入方式有很大的影响。相反,如果您在设计语言时考虑到了可嵌入性,那么这种思维模式将对最终的语言产生很大的影响。
大多数脚本语言的典型宿主语言是C,因此这些语言的api大多由函数加上一些类型和常量组成。这对脚本语言的API设计施加了一个自然但狭窄的限制:它必须通过针眼提供对语言特性的访问。句法结构尤其难以理解。例如,在脚本语言中,方法必须在类中按词法编写,宿主语言不能向类中添加方法,除非API提供合适的机制。同样,通过API传递词法作用域也很困难,因为宿主函数不能在脚本函数中使用词法。
可嵌入语言的API中的一个关键成分是eval
函数,该函数执行一段代码。特别是,当嵌入脚本语言时,所有脚本都由主机调用运行eval
.一个eval
函数还允许以极简的方法设计API。用一个适当的eval
函数,主机实际上可以在脚本环境中做任何事情:它可以赋值给变量(eval = 20)
、查询变量(eval“返回”)
调用函数(eval“foo(32岁的“统计”)”)
,等等。可以通过计算适当的代码来构造和分解数组等数据结构。例如,再次假设一个假设eval
函数所示的C代码图1会将一个C数组的整数复制到脚本中。
尽管它的简单性和完整性令人满意,但一个由单个eval
函数有两个缺点:由于每次交互时解析和解释数据块的成本,它的效率太低,无法集中使用;而且它使用起来太麻烦,因为在C中创建命令需要进行字符串操作,并且需要序列化所有通过API的数据。然而,在实际应用程序中经常使用这种方法。Python称之为“非常高级的内嵌”。8
对于一个更高效、更容易使用的API,我们需要更多的复杂性。除了一个eval
函数用于执行脚本,我们需要直接的方法来调用脚本定义的函数,处理脚本中的错误,在宿主程序和脚本环境之间传输数据,等等。我们将讨论可嵌入语言的API的这些不同方面,以及它们如何影响Lua的设计,但首先我们将讨论这种API的简单存在如何影响语言。
给定一个具有API的可嵌入语言,用宿主语言编写一个将API导出回脚本语言的库并不困难。因此,我们有了一种有趣的反射形式,宿主语言就像一面镜子。Lua中的一些机制使用了这种技术。例如,Lua提供了一个称为类型
查询给定值的类型。这个函数是在解释器外部通过一个外部库用C实现的。这个库只是向Lua导出一个C函数(称为luaB _类型)
它调用Lua API来获取参数的类型。
一方面,这种技术简化了解释器的实现;一旦一种机制对API可用,就可以很容易地对语言可用。另一方面,它也迫使语言特征通过针眼。在讨论异常处理时,我们将看到这种折衷的具体示例。
每个脚本语言必须解决的第一个与控制相关的问题是“谁拥有主函数”问题。当我们使用嵌入到主机中的脚本语言时,我们希望该语言是一个库,主函数在主机中。然而,对于许多应用程序,我们希望该语言作为具有自己内部主函数的独立程序。
Lua通过使用单独的独立程序解决了这个问题。Lua本身完全是作为库实现的,其目标是嵌入到其他应用程序中。的lua
命令行程序只是一个小型应用程序,它使用Lua库作为任何其他主机来运行Lua代码片段。中的代码图2是此应用程序的基本版本。当然,真正的应用程序要比这长,因为它必须处理选项、错误、信号和其他现实生活中的细节,但它仍然只有不到500行C代码。
尽管函数调用构成了Lua和C之间的大部分控件通信,但通过API还公开了其他形式的控件:迭代器、错误处理和协程。Lua中的迭代器允许如下结构,它遍历文件的所有行:
尽管迭代器提供了一种新的语法,但它们是构建在第一级函数之上的。在我们的例子中,调用io.lines(文件)
返回一个迭代函数,它在每次调用时从文件中返回一个新行。因此,API不需要任何特殊的东西来处理迭代器。Lua代码很容易使用用C编写的迭代器(就像io.lines
)以及C代码使用Lua编写的迭代器进行迭代。在这种情况下,没有语法支持;C代码必须显式地完成所有这些为
construct在Lua中隐式执行。
错误处理是Lua受到API强烈影响的另一个领域。Lua中的所有错误处理都是基于longjump
它是一个从API导出到语言的特性的例子。
该API支持两种调用Lua函数的机制:unprotected而且受保护的.无保护调用不处理错误:调用期间的任何错误长跳
通过此代码到达调用堆栈更下方的受保护调用。受保护调用使用setjmp
,以便捕获调用期间的任何错误;调用总是返回一个正确的错误代码。这种受保护的调用在嵌入式场景中非常重要,因为在嵌入式场景中,宿主程序不能因为脚本中偶尔出现的错误而中止。刚刚介绍的基本应用程序的用途lua _ pcall
(protected call)以保护模式调用每个编译行。
标准Lua库只是将受保护的调用API函数以的名称导出到Luapcall
.与pcall
,相当于atry - catch在Lua中是这样的:
这当然比语言中内置的try-catch原语机制更麻烦,但它与C API非常匹配,实现也非常简单。
Lua中协同程序的设计是API产生巨大影响的另一个领域。协程有两种形式:对称的而且不对称.1对称协程提供单一的控制传输原语,通常称为转移,这就像转到:它可以将控制从任何协程转移到任何其他协程。非对称协程提供两个控制传输原语,通常称为重新开始而且收益率,就像一对调用返回:一个重新开始
可以将控制转移到任何其他协程;一个收益率
停止当前的协程并返回到恢复屈服的协程的那个。
我们很容易把协程想象成一个调用堆栈(a延续),它编码了一个程序必须进行哪些计算才能完成该协程。的转移对称协程的基元对应于用转移目标的调用堆栈替换正在运行的协程的整个调用堆栈。另一方面,重新开始Primitive将目标堆栈添加到当前堆栈之上。
对称协程比非对称协程更简单,但对于像Lua这样的可嵌入语言来说是个大问题。脚本中任何活动的C函数都必须在C堆栈中有相应的激活寄存器。在脚本执行的任何时候,调用堆栈可能混合有C函数和Lua函数。(特别是,调用堆栈的底部总是有一个C函数,它是初始化脚本的宿主程序。)但是,程序不能从调用堆栈中删除这些C项,因为C没有提供任何机制来操作它的调用堆栈。因此,程序不能进行任何传输。
非对称协程没有这个问题,因为重新开始原始元素不影响当前堆栈。仍然有一个程序不能满足的限制在函数之间的栈中不能有C函数重新开始
和收益率.对于在Lua中允许可移植协程来说,这个限制是一个很小的代价。
极简主义者的主要问题之一eval对于API来说,最重要的方法是需要将所有数据序列化为字符串或重新构建数据的代码段。因此,一个实用的API应该提供其他更有效的机制来在宿主程序和脚本环境之间传输数据。
当主机调用脚本时,数据作为参数从主机程序流向脚本环境,作为结果反向流动。当脚本调用宿主函数时,情况正好相反。在这两种情况下,数据必须能够双向流动。因此,与数据传输相关的大多数问题都与嵌入和扩展相关。
为了讨论LuaC API如何处理这一数据流,让我们从一个如何扩展Lua的示例开始。图3显示了函数的实现io.getenv
,用于访问主机程序的环境变量。
为了让脚本能够调用这个函数,我们必须注册它进入脚本环境。我们一会儿就会看到如何做到这一点;现在,让我们假设它已经注册为一个全局变量采用
,可以这样使用:
在这段代码中首先要注意的是操作系统_ getenv
.该函数的唯一参数是Lua状态。解释器通过此状态内的数据结构将实际参数传递给函数(在本例中为环境变量的名称)。该数据结构是Lua值的堆栈;鉴于其重要性,我们称其为堆栈.
当Lua脚本调用时采用
, Lua解释器调用操作系统_ getenv
使用只包含给定参数的堆栈采用
,第一个参数位于堆栈中的位置1。的第一件事操作系统_ getenv
是打电话luaL _ checkstring
,它检查位置1的Lua值是否真的是一个字符串,并返回一个指向相应C字符串的指针。(如果值不是字符串,luaL _ checkstring
标记一个错误longjump
,使它不回操作系统_ getenv
)。
接下来,函数调用采用
从C库,它做了真正的工作。然后它调用lua _ pushstring
,它将C字符串值转换为Lua字符串并将该字符串压入堆栈。最后,操作系统_ getenv
返回1。这个返回告诉Lua解释器堆栈顶部有多少值应该被认为是函数结果。(Lua中的函数可能返回多个结果。)
现在让我们回到如何注册的问题上操作系统_ getenv
作为采用
在脚本环境中。一个简单的方法是改变我们之前的独立Lua程序的例子,如下所示:
添加的第一行是我们用宿主函数扩展Lua所需要的所有魔力。函数lua _ pushcfunction
接收一个指向C函数的指针,并在堆栈上推入一个(Lua)函数,该函数在被调用时调用其对应的C函数。因为Lua中的函数是一级值,所以API不需要额外的工具来注册全局函数、局部函数、方法等等。API只需要一个注入函数lua _ pushcfunction
.一旦创建为Lua函数,就可以像操作任何其他Lua值一样操作这个新值。在新代码中添加的第二行调用lua _ setglobal
将堆栈顶部的值(新函数)设置为全局变量的值采用
.
除了是一流的值,Lua中的函数总是匿名的。声明,例如
是赋值的语法糖:
我们用来注册函数的API代码采用
与Lua中的声明完全相同:它创建一个匿名函数并将其赋值给一个全局变量。
同样,API不需要不同的工具来调用不同类型的Lua函数,如全局函数、局部函数和方法。要调用任何函数,主机首先使用API的常规数据操作工具将函数推入堆栈,然后推入参数。一旦函数(作为一级值)和参数都在堆栈中,主机就可以使用单个API原语调用它,而不管函数来自何处。
Lua最显著的特性之一是表的广泛使用。表本质上是一个关联数组。表是Lua中唯一的数据结构机制,因此它们比具有类似结构的其他语言发挥更大的作用。Lua不仅将表用于所有数据结构(记录和数组等),还将表用于其他语言机制,如模块、对象和环境。
中的例子图4说明了通过API对表的操作。函数操作系统_环境
创建并返回一个表,其中包含进程可用的所有环境变量。函数假定访问环境
数组,它是在POSIX系统中预定义的;该数组中的每个条目都是该形式的字符串名称=值
,描述环境变量。
的第一步操作系统_环境
是通过调用lua _ newtable
.然后函数遍历数组环境
在Lua中构建一个表,反映该数组的内容。对于每一项环境
,函数将变量名推入堆栈,推入变量值,然后调用lua _可设置的
将pair存储在新表中。(不像lua _ pushstring
,它假设一个以零结尾的字符串,lua _ pushlstring
接收显式长度。)
函数lua _可设置的
假设新条目的键和值在堆栈的顶部;调用中的参数3告诉表在堆栈中的位置。(负数从顶部开始索引,所以3表示从顶部开始的三个槽。)
函数lua _可设置的
弹出键和值,但保留表在堆栈中的位置。因此,在每次迭代之后,表又回到了顶部。最后一个return1
告诉Lua这个表是的唯一结果操作系统_环境
.
Lua API的一个关键属性是,它不为C代码提供直接引用Lua对象的方法;C代码要操作的任何值都必须在堆栈上。在上一个例子中,函数操作系统_环境
创建一个Lua表,用一些条目填充它,并将它返回给解释器。表始终保持在堆栈上。
我们可以将这种方法与使用某种C类型来引用语言的值进行对比。例如,Python有类型PyObject
;JNI (Java本机接口)有jobject
.Lua的早期版本也提供了类似的功能lua _对象
类型。然而,经过一段时间后,我们决定更改API。6
一个主要的问题lua _对象
类型是与垃圾收集器的交互。在Python中,程序员负责调用宏,例如Py _ INCREF
而且DECREF
增加或减少API操纵的对象的引用计数。这种显式计数既复杂又容易出错。在JNI(以及Lua的早期版本)中,对对象的引用在创建它的函数返回之前都是有效的。这种方法比手工计数引用更简单和安全,但是程序员失去了对对象生命周期的控制。只有当函数返回时,在函数中创建的任何对象才能被释放。相反,栈允许程序员以一种安全的方式控制任何对象的生命周期。当一个对象在堆栈中时,它不能被收集;一旦出栈,它就不能被操纵。此外,堆栈提供了一种传递参数和结果的自然方式。
Lua中表的广泛使用对C API有明显的影响。Lua中表示为表的任何内容都可以使用完全相同的操作进行操作。例如,Lua中的模块被实现为表。Lua模块不过是一个包含模块函数和偶尔数据的表。(记住,函数在Lua中是一等值。)当你写这样的时候sin (x)
,你可以把它看作是罪
函数的数学
模块,但实际上调用的是存储在全局变量中的表中的字段“sin”的内容数学
.因此,主机很容易创建模块,向现有模块添加函数,“导入”Lua编写的模块,等等。
Lua中的对象遵循类似的模式。Lua为面向对象编程使用基于原型的风格,其中对象由表表示。方法被实现为存储在原型中的函数。与模块类似,主机很容易创建对象、调用方法等等。在基于类的系统中,类及其子类的实例必须共享某种结构。基于原型的系统没有这个要求,所以宿主对象可以从脚本对象继承行为,反之亦然。
动态语言的一个主要特征是eval构造,它允许执行在运行时构建的代码。正如我们讨论过的,一个evalfunction也是脚本语言API中的基本元素。特别是,eval是主机运行脚本的基本方法。
Lua没有直接提供eval函数。相反,它提供了一个负载函数。(代码图2使用luaL _ loadstring
函数,它是负载)。这个函数不执行一段代码;相反,它生成一个Lua函数,当调用该函数时,执行给定的代码段。
当然,转换起来很容易eval成负载反之亦然。尽管如此,我们认为负载有一些优势eval
.从概念上讲,负载将程序文本映射到语言中的值,而不是将其映射到操作。一个eval函数通常是API中最复杂的函数。通过将“编译”与执行分离,它变得简单了一些;特别是,与eval、负载从来没有副作用。
编译和执行之间的分离也避免了组合问题。Lua有三个不同的加载函数,取决于源:一个用于加载字符串,一个用于加载文件,一个用于加载由给定阅读器函数读取的数据。(前两个函数是在后一个函数之上实现的。)
因为调用函数有两种方法(受保护的和未受保护的),我们需要六种不同的方法eval
函数覆盖所有可能性。
错误处理也更简单,因为静态错误和动态错误是分开发生的。最后,负载确保所有Lua代码总是在某个函数中,这为语言提供了更多的规律性。
与eval
功能是环境的概念。每一种图灵完备语言都能自我解释;这是图灵机的标志。是什么让eval
特殊之处在于它在与使用它的程序相同的环境中执行动态代码。换句话说,aneval
建筑提供了某种程度的反思。例如,用C语言编写一个C解释器并不太难,但遇到这样的语句时x=1,此解释器无法访问变量x在程序里,如果有的话。(一些非ansi工具,例如那些与动态链接库相关的工具,允许C程序找到给定全局符号的地址,但程序仍然无法找到关于其类型的任何信息。)
Lua中的环境就是一个表。Lua只提供两种变量:局部变量和表字段。从语法上说,Lua还提供了全局变量:任何没有绑定到局部声明的名称都被认为是全局的。从语义上讲,这些未绑定的名称引用与封闭函数相关的特定表中的字段;这个表叫做环境的函数。在典型的程序中,大多数(或所有)函数共享单个环境表,然后该表扮演全局环境的角色。
通过API可以很容易地访问全局变量。因为它们是表字段,所以可以通过常规API访问它们来操作表。例如,函数lua _ setglobal
,它实际上是在表操作原语之上编写的一个简单宏。
另一方面,局部变量遵循严格的词汇作用域规则,因此它们根本不参与API。因为C代码不能在Lua代码中进行词法嵌套,所以C代码不能访问Lua中的局部变量(除非通过一些调试工具)。这实际上是Lua中唯一不能通过API模拟的机制。
这种例外有几个原因。词汇作用域是一个古老而强大的概念,应该遵循标准行为。此外,由于局部变量不能从它们的作用域之外访问,词汇作用域为程序员提供了访问控制和封装的基础。例如,任何Lua代码文件都可以声明只在文件内部可见的局部变量。最后,局部变量的静态特性允许编译器将所有局部变量放在Lua基于寄存器的虚拟机的寄存器中。5
我们已经讨论过,向外界提供API不是脚本语言实现中的细节,而是可能影响整个语言的决策。我们已经展示了Lua的设计是如何受到API的影响的,反之亦然。
任何编程语言的设计都涉及许多这样的权衡。一些语言属性,比如简单性,有利于可嵌入性,而其他的,比如静态验证,则不然。Lua的设计涉及到围绕可嵌入性的几项权衡。对模块的支持就是一个典型的例子。Lua用最少的额外机制支持模块,以牺牲某些功能(如不合格的导入)为代价支持简单性和可嵌入性。另一个例子是对词法作用域的支持。在这里,我们选择了更好的静态验证,但损害了其可嵌入性。我们对Lua的平衡很满意,但这对我们来说是一个学习的过程。
相关文章
在queue.acm.org
专用语言
迈克·夏皮罗
http://queue.acm.org/detail.cfm?id=1508217
与威尔·哈维的对话
克里斯Dibona
http://queue.acm.org/detail.cfm?id=971586
我们软件里的人
约翰·理查兹,吉姆·克里斯滕森
http://queue.acm.org/detail.cfm?id=971596
1.de Moura, A., Ierusalimschy, R.重新审视协同程序。ACM反式。程序设计语言与系统, 2(2009), 6.16.31。
2.发动机调查:一般结果。Gamasutra;http://www.gamasutra.com/blogs/MarkDeLoura/20090302/581/The_Engine_Survey_General_results.php.
3.Ierusalimschy, R。在Lua中编程,2nd艾德.Lua.org,巴西,里约热内卢,2006年。
4.Ierusalimschy, R., de Figueiredo, L. H, Celes, W. lua一种可扩展的扩展语言。软件:实践与经验, 6(1996), 635652。
5.Ierusalimschy, R., de Figueiredo, l.h., Celes, W. Lua 5.0的实现。通用计算机科学学报11地球物理学报,7(2005):11591176。
6.Ierusalimschy, R., de Figueiredo, L. H., Celes, W. Lua的进化。在会议记录理查德·道金斯ACM SIGPLAN编程语言历史会议(圣地亚哥,加州,2007年6月)。
7.脚本:21世纪的高级编程。IEEE计算机313(1998), 2330。
8.Python软件基金会。扩展和嵌入Python解释器,版本2.7(2011年4月);http://docs.python.org/extending/.
©2011 acm 0001-0782/11/0700 $10.00
允许为个人或课堂使用部分或全部作品制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。除ACM外,本作品的其他组件的版权必须受到尊重。允许有信用的文摘。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,都需要事先获得特定的许可和/或费用。请求发布的权限permissions@acm.org传真(212)869-0481。
数字图书馆是由计算机协会出版的。版权所有©2011 ACM, Inc.
没有发现记录