null
分类: 嵌入式
2006-01-24 14:09:44
John K. Ousterhout
Computer Science Division
Electrical Engineering and Computer Sciences
University of California at Berkeley
Berkeley, CA 94720
ouster@sprite.berkeley.edu
译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
摘要
Tcl 是用于工具命令语言的一个解释器。它由作为基本命令解释器嵌入到工具(比如编辑器、调试器等)中的一个库包组成。Tcl 提供了(a)用于简单的文本命令语言的分析器,(b)一组内置的实用命令,和(c)一个 C 接口,工具通过它用特定于工具的命令增加内置命令。Tcl 在与窗口组件库整合的时候特别有吸引力:它通过了提供变量、过程、表达式等机制增进了组件的可编程性;它允许用户编程组件的外观和动作二者;并且它在交互式程序之间提供一个简单但强力的通信机制。
本文出现于 1990 Winter USENIX Conference Proceedings
1. 介绍
Tcl 代表了“tool command language - 工具命令语言”。它由一个库包组成,程序可以把它用作自己的命令语言的基础。Tcl 的开发由两项观察所推动。第一项观察是,通用可编程命令语言通过允许用户用命令语言写程序来扩展工具的内置设施,从而扩大了工具的能力。在强力的命令语言之中最众所周知的例子是 UNIX shell[5] 和 Emacs 编辑器[8]。在各自情况下,出现的有着不同寻常能力的计算环境,在很大程度上是因为能获得可编程的命令语言。
第二个促成它的观察是交互式应用正在增长。在 1970 年代晚期和 1980 年代早期的分时环境中,几乎所有的程序都是面向批处理的。典型的使用交互式的命令 shell 来调用它们。除了 shell 之外,只有少数其他的程序是交互式的,比如编辑器和邮件器。正好相反,今天使用的个人工作站,带有它们自己的光栅显示器和鼠标,鼓励了一种不同的系统结构,在这里大量的程序是交互式的,并且最常见的交互方式是直接用鼠标操纵单独的应用。此外,今天能获得的大显示器使很多交互式的应用立即活跃起来成为可能,而对于在十年前很小的屏幕这是不实际的。
不幸的是,很少的今天的交互式程序拥有 shell 或 Emacs 命令语言的能力。在这里好的命令语言是存在着的,它们趋向与特定的程序捆绑在一起。每个新的交互式程序都要求开发一个新的命令语言。在多数情况下,应用程序员没有时间或爱好去实现一个通用设施(特别是在应用自身很简单的时候),所以结果的命令语言趋向于带有不充分的功能和笨拙的语法。
Tcl 是一个独立于应用的命令语言。它作为一个 C 库包存在,可以用于很多不同的程序中。Tcl 库提供了用于简单但完全可编程的命令语言的一个分析器。这个库还实现了提供了通用的编程构造的一组内置命令,比如变量、列表、表达式、条件、循环和过程。单个的应用程序可以用特定于应用的命令来扩展基本的 Tcl 语言。Tcl 库还提供一组实用工具例程来简化特定于工具的命令的实现。
我相信 Tcl 在窗口环境中是特别有用的,它提供了两项优势。首先,它可以用做编制应用的界面的一个通用机制。如果一个工具基于 Tcl,则应当相对容易的去修改应用的用户界面,并使用新命令来扩展这个界面。其次和更重要的是,Tcl 为工具之间通信提供一种统一的框架。如果在所有的工具中统一使用了它,Tcl 将使工具在一起工作得比今天的状况更加优雅。
本文余下部分组织如下:第 2 节描述用户见到的 Tcl 语言。第 3 节讨论在应用程序中如何使用 Tcl,包括在应用程序和 Tcl 库之间的 C 语言接口。第 4 节描述在窗口环境中如何使用 Tcl 来定制界面动作和外观。第 5 节展示如何使用 Tcl 作为进程间通信的媒介,和为什么这很重要。第 6 表述 Tcl 实现的状态和一些初步的性能测量。第 7 节把 Tcl 与 Lisp、Emacs 和 NeWS 做对比,第 8 节总结本文。
2. Tcl 语言
在某种意义上,Tcl 语言的语法是不重要的:任何编程语言,不管它是 C[6]、Forth[4]、Lisp[1] 还是 Postscript[2] 都可以提供同 Tcl 大体相同的可编程性和通信上的优势。这提示了最佳实现途径是借用现存的语言,并集中于为使用这门语言提供一个方便的框架。但是,可嵌入的命令语言的所处环境对语言提出了一组不同寻常的约束,后面将描述它们。我最终决定了从头设计一个新语言更有可能满足这些约束,并比现存的语言带有更少的实现努力。
Tcl 是不寻常的因为它提供两种不同的接口: 给用户发起 Tcl 命令的一个文本接口,和给它所嵌入的应用的一个过程接口。这些接口的每个都必须是简单的、强力的和高效的。在语言设计中有四个主要的因素:
[1] 语言用于命令。几乎所有 Tcl“程序”都是短小的,很多只有一行长。多数程序将是键入的,执行一次或者几次,接着就丢弃了。这提示了这门语言应当有一个简单的语法,以便于键入命令。多数现存的编程语言都有复杂的语法;在写长程序的时候有益,但如果用做命令语言就笨拙了。
[2] 语言必须是可编程的。它应当包含通用编程构造,比如变量、过程、条件和循环,这样用户可以通过写 Tcl 过程来扩展内置的命令。可扩展性也要求简单的语法:这使 Tcl 程序生成其他 Tcl 程序变得容易了。
[3] 语言必须允许一个简单而高效的解释器。由于 Tcl 库要包含到许多小程序中,特别是在没有共享库的机器上,解释器必须不占用太多的内存。用来解释 Tcl 命令的机制必须足够快,可用于每秒发生上百次的事件,比如鼠标移动。
[4] 语言必须允许对 C 应用的一个简单接口。它必须易于让 C 应用调用这个解释器,并易于让它们用特定于应用的命令来扩展内置的命令。这个因素是我决定不使用 Lisp 作为命令语言的原因之一:Lisp 的基本数据类型和存储管理机制与 C 实在是不同,很难在它们之间建立清晰而简单的接口。对 Tcl 我使用了对于 C 最自然的数据类型(字符串)。
2.1. Tcl 语言语法
Tcl 的基本语法类似于 UNIX shell:命令由用空格或 TAB 分隔的一个或多个字段组成。第一个字段是命令的名字,它可以是内置命令、特定于应用的命令、或者是由一系列的 Tcl 命令组成的过程。在第一个后面的字段都作为参数传递给命令。如同在 UNIX shell 中那样,换行字符用做命令分隔符,分号也可用来分隔在同一行上的命令。不同于 UNIX shell,每个 Tcl 命令返回一个字符串结果,或者是空串,如果不适宜返回值的话。
在 Tcl 中有四个补充的语法构造,它们给予语言一种类似 Lisp 的风格。使用花括号来组合复杂的参数;它们充当可嵌套的引用字符。如果参数的第一个字符是左花括号,则这个参数不以空白终结。转而,它终结于相匹配的右花括号。传递给这个命令的参数由在花括号中间的所有东西组成,并剥除围绕的花括号。例如,命令
set a {dog cat {horse cow mule} bear}
将收到两个参数:“a”和“dog cat {horse cow mule} bear”。这个特定命令将把变量 a 设置为等于第二个参数的一个字符串。如果参数包围在花括号中,则不对这个参数做下面描述的其他替换。花括号最常见的用途是把一个 Tcl 子程序指定为到 Tcl 命令的参数。
在 Tcl 中第二个语法构造是是方括号,它用于引发命令替换。如果在参数中出现了左方括号,则从这个左方括号一直到相匹配的右方括号的所有东西都作为一个命令来对待,并由 Tcl 解释器递归的执行。命令的结果接着替换到这个方括号包围的字符串所在的位置上。例如,考虑命令
set a [format {Santa Claus is %s years old} 99]
format 命令做类似 printf 的格式化并返回字符串“Santa Claus is 99 years old”,接着把它传递给 set 并赋值到变量 a。第三个语法构造是美元号,它用于变量替换。如果它出现在参数中,则随后的字符作为变量的名字对待;变量的内容被替换到参数中这个美元符号和名字所在的位置上。例如,命令
set b 99 set a [format {Santa Claus is %s years old} $b]
导致 a 有同前面段落中的简单命令相同的最终值。变量替换不是严格必须的,因为有其他方式来达到相同的效果,但是它减少了键入。
最后一个语法构造是反斜杠字符,可以用它把特殊字符插入到参数中,比如花括号或非打印字符。
2.2. 数据类型
在 Tcl 中只有一种数据类型:字符串。所有命令、到命令的参数、命令返回的结果和变量的值都是 ASCII 字符串。Tcl 始终使用字符串便于在 Tcl 库过程和包围它的应用的 C 代码之间来回传递信息。这使它易于在不同类型的机器之间来回传递有关 Tcl 的信息。
尽管在 Tcl 中所有的东西都是字符串,很多命令都希望它们的字符串参数有特定的格式。这里的字符串有三种特定的通用格式:列表、表达式和命令。列表只是包含用空白分隔的一个或多个字段的字符串,类似于命令。可以使用花括来包围复杂的列表元素;这些复杂的列表元素自身经常也是列表,类似于 Lisp。例如,字符串
dog cat {horse cow mule} bear
是有四个元素的一个列表,其中第三个元素是有三个元素的列表。Tcl 提供一组列表操纵的命令,比如建立列表、提取元素、和计算列表长度。
字符串的第二种常见形式是数值表达式。Tcl 表达式同 C 中的表达式有着同样的操作符合优先级。Tcl 命令 expr 把字符串作为表达式来求值并返回结果(当然是作为字符串)。例如,命令
expr {($a < $b) || ($c != 0)}
在变量 a 小于变量 b 或者变量 c 是零的时候返回“1”,否则返回“0”。一些其他的命令,比如 if 和 for, 期望它们的一个或多个参数是表达式。
字符串的第三种常见解释是命令(或命令的序列)。这种形式的参数用在实现控制结构的 Tcl 命令中。例如,考虑下列命令:
if {$a < $b} { set tmp $a set a $b set b $tmp }
这里的 if 命令接受两个参数,每个都是用花括号界定的。If 是内置命令,它把它的第一个参数作为表达式来求值;如果结果非零,则 if 把它的第二个参数作为 Tcl 命令执行。这个特定命令在变量 a 小于 b 的时候交换 a 和 b 的值。
Tcl 还允许用户定义用 Tcl 语言写的命令过程。我称谓这些过程为 tclproc,为的是区别于用 C 写成的其他过程。使用 proc 内置命令来建立 tclproc。例如,下面定义了一个递归的阶乘过程的 Tcl 命令:
proc fac x { if {$x == 1} {return 1} return [expr {$x * [fac [expr $x-1]]}] }
proc 命令接受三个参数:新 tclproc 的名字、一个变量名字的列表(在这个实例中试只有一个元素 x 的列表),和一个构成 tclproc 的过程体的 Tcl 命令。一旦执行了这个 proc 命令,fac 就可以同其他 Tcl 命令一样调用了。例如
fac 4
将返回字符串“24”。
|
图表 1。内置的 Tcl 命令。这是对使用 Tcl 的任何应用都能获得的一组命令。应用可以定义额外的命令。 |
图表 1 分组列出了所有内置 Tcl 命令。除了已经提及的命令,Tcl 还提供了操纵字符串的命令(比较、匹配和类似 printf/scanf 的操作),操纵文件和文件名字的命令,和 fork 子进程并返回子进程的标准输出作为结果的命令。内置 Tcl 命令提供了简单但完整的编程环境。可以按三种方式扩展内置设施:写 tclproc;把其他程序作为子进程调用;或按下一节描述的那样用 C 过程定义新命令。
3. 在应用中嵌入 Tcl
尽管内置 Tcl 命令可以令人信服的用作独立的编程系统,Tcl 实际上意图被嵌入到应用程序中。我已经建造了使用 Tcl 的几个应用程序,其中之一是针对 X 的叫做 mx 的一个基于鼠标的编辑器。在本文的余下部分,我将使用来自 mx 的例子来展示 Tcl 如何与包围它的应用进行交互。
使用 Tcl 的应用程序用同特定应用有关的一些额外的命令来扩展内置命令。例如,时钟程序可以提供额外的命令来控制时钟如何显示和设置闹钟;mx 编辑器提供额外的命令来从磁盘读取文件,在窗口中显示它,选择和修改一定范围内的字节,和把修改后的文件写回磁盘。应用程序员只需要写特定于应用的命令;内置命令“免费的”提供编程能力和扩展能力。对于用户,特定于应用的命令表现的如同内置命令一样。
|
图表 2. Tcl 库为 Tcl 语言提供一个分析器、一组内置命令、和一些实用工具过程。应用提供特定于应用的命令加上收集要执行的命令的过程。命令由 Tcl 分析,并接着传递给相关的(在 Tcl 中或者在应用中)命令过程去执行。 |
图表 2 展示了在 Tcl 和应用的余下部分之间的联系。Tcl 是连接到应用上的一个 C 库包。Tcl 库包含针对 Tcl 语言的一个分析器、执行内置命令的过程、和做表达式求值和列表管理这种事情的一组实用工具过程。分析器包括可以用来扩展语言的命令集的一个扩展接口。
要使用 Tcl,应用首先要使用下面的库过程建立叫 interpreter 的一个对象:
Tcl_Interp * Tcl_CreateInterp()
解释器由一组命令、一组变量绑定和一个命令执行状态组成。它是多数 Tcl 库过程操纵的基本单位。简单应用将只使用一个单一的解释器,更复杂的应用可以为不同用途而使用多个解释器。例如,mx 为在屏幕上的每个窗口使用一个解释器。
一旦应用建立了一个解释器,它就可以调用 Tcl_CreateCommand 过程来用特定于命令的过程来扩展这个解释器:
typedef int (*Tcl_CmdProc)(ClientData clientData, Tcl_Interp *interp, int argc, char *argv[]); Tcl_CreateCommand(Tcl_Interp *interp, char *name, Tcl_CmdProc proc, ClientData clientData)
每次对 Tcl_CreateCommand 的调用都把一个特定的命令名字(name)、同一个实现这个命令的过程(proc)、和一个任意的传递到这个过程的一个单字值(clientData)关联起来。
在建立了特定于应用的命令之后,应用进入一个主循环,收集命令并把它们传递给 Tcl_Eval 过程去执行:
int Tcl_Eval(Tcl_Interp *interp, char *cmd)
在最简单的形式中,应用可以简单的从终端或屏幕读取命令。在 mx 编辑器中为事件如击键、鼠标按钮或菜单激活都关联上 Tcl 命令;每次事件发生的时候,就把对应的 Tcl 命令传递给 Tcl_Eval。
Tcl_Eval 过程把它的 cmd 参数分析成字段,在与这个解释器相关联的表格中查找命令的名字,并调用与这个命令相关联的命令过程。所有的命令过程,不管内置的还是特定于应用的,都按照上面 Tcl_CmdProc 中 typedef 描述的方式,以同样的方式来调用。向命令过程传递描述命令参数的一个字符串的数组(argc 和 argv),加上在建立的时候关联到命令上的 clientData 值。ClientData 典型的是到包含执行这个命令所需信息的特定于应用的结构一个指针。例如,在 mx 中 clientData 参数指向一个每窗口数据结构,描述了编辑的文件和在其中显示的窗口。
控制机制如 if 和 for 是使用对 Tcl_Eval 的递归调用实现的。例如,if 命令的命令过程把它的第一个参数作为表达式求值;如果结果是非零,则它递归的调用 Tcl_Eval 来把它的第二个参数作为 Tcl 命令来执行。在这个命令执行期间,可以再次递归调用 Tcl_Eval,以此类推。Tcl_Eval 还递归的调用自身来执行出现在参数中的方括号包围的命令。
甚至 tclproc 如 fac 都使用了同样的基本机制。在调用了 proc 命令建立 fac 的时候,proc 命令过程通过调用 Tcl_CreateCommand 建立一个新命令,这在图表 3 中展示。新命令拥有名字 fac。它的命令过程 (对 Tcl_CreateCommand 调用中的 proc)是叫做 InterpProc 的一个特殊的 Tcl 库过程,而它的 clientData 是到描述这个 tclproc 的结构的一个指针。这个结构包含了,同其他东西在一起的,这个 tclproc 的过程体的一个复件(给 proc 命令的第三个参数)。在调用 fac 命令的时候,Tcl_Eval 调用 InterpProc,它依次调用 Tcl_Eval 来执行 tclproc 的过程体。需要有一些额外的代码来把 fac 命令的参数(它们在传递给 InterpProc 的 argv 数组中)关联到在 fac 过程体内使用的 x 变量上,和支持带有局部作用域的变量,但是用于 tclprocs 的机制多数与用于任何其他 Tcl 命令的机制相同。
|
图表 3. tclproc (用 Tcl 写的过程)的建立和执行: (a) 调用了 proc 命令,就是说,要建立 fac 过程;(b) Tcl 分析器调用与 proc 相关联的命令过程;(c) proc 命令过程建立一个数据结构来持有是 fac 过程体的 Tcl 命令;(d) fac 注册为一个新的 Tcl 命令,带有 InterpProc 作为它的命令过程;(e) fac 作为 Tcl 命令而被调用;(f) Tcl 分析器把 InterpProc 作为 fac 的命令过程来调用;(g) InterpProc 从数据结构中取回 fac 的过程体;(h) 把在 fac 过程体中的 Tcl 命令传递回到 Tcl 分析器去执行。 |
Tcl 命令过程向 Tcl_Eval 返回两个结果: 一个整数返回代码和一个字符串。返回代码是作为过程的结果返回的,而这个字符串存储在解释器中,以后可以从中取回它。Tcl_Eval 向它的调用者返回相同的代码和字符串。表格 I 总结了返回代码和字符串。正常的返回代码是 TCL_OK 和包含命令结果的字符串。如果 在执行命令中发生一个错误,则返回代码将是 TCL_ERROR 而字符串将描述错误状况。在返回的 TCL_ERROR 的时候(或者任何不是 TCL_OK 的值),对于嵌套命令的通常的动作是向它们的调用者返回相同的代码和字符串,回退(unwind)所有未决的命令执行,直到返回代码和字符串最终从对 Tcl_Eval 的顶层调用返回。在这个时刻应用通常向用户显示错误消息,通常在终端上打印它或在一个通知窗口中显示它。
返回代码 | 意义 | 字符串 |
TCL_OK | 命令正常完成 | 结果 |
TCL_ERROR | 在命令中出现了错误 | 错误消息 |
TCL_BREAK | 应当从最内层循环中退出 | 没有 |
TCL_CONTINUE | 应当跳过最内层迭代 | 没有 |
TCL_RETURN | 应当从最内层过程中返回 | 过程结果 |
表格 I. 每个 Tcl 命令都返回描述发生了什么的一个代码和提供补充信息的一个字符串。如果返回代码不是 TCL_OK,则回退(unwind)嵌套的命令并返回相同的代码,直到达到顶层或预备来处理异常返回代码的某个命令。 |
不是 TCL_OK 或 TCL_ERROR 的返回代码导致部分回退。例如,break 命令返回一个 TCL_BREAK 代码。它导致回退嵌套的命令执行,直到达到一个嵌套的 for 或 foreach 命令。在 for 或 foreach 命令递归的调用 Tcl_Eval 的时候,它特别的检查 TCL_BREAK 结果。当这个代码出现的时候,for 或 foreach 命令终止这个循环,但是它不向它的调用者返回 TCL_BREAK。它转而返回 TCL_OK。这样就不会有更高层次的执行被中止(abort)。TCL_CONTINUE 返回代码也由 for 和 foreach 命令处理(它们继续做下一个循环迭代),而TCL_RETURN 由 InterpProc 过程处理。只有一些命令过程,如 break 和 for,知道关于特殊返回代码如 TCL_BREAK 的事情;其他命令过程在见到不是 TCL_OK 的返回代码的时候就是简单的中止。
可以使用 catch 命令来防止对 TCL_ERROR 返回的完全回退。catch 接受是要执行的 Tcl 命令的一个参数。它把这个命令传递到 Tcl_Eval 去执行,但总是返回 TCL_OK。如果在这个命令中发生一个错误,catch 的命令过程检测来自 Tcl_Eval 的 TCL_ERROR 返回值,在 Tcl 变量中保存有关错误的信息,并接着向它的调用者返回 TCL_OK。在几乎所有的情况下,我认为对一个错误最好的响应就是退出所有的命令调用并通知用户;catch 是为那些少见的场合提供的,这里期望着一个错误并做出处理而不用中止。
4. Tcl 和窗口应用
可嵌入的命令语言如 Tcl 在窗口环境中提供了特别的好处。部分原因是在窗口环境中有很多交互式程序(所以有很多地方要使用命令语言),部分的原因是在今天的窗口环境中可配置性是重要的,并且语言如 Tcl 提供了做重新配置的灵活性。Tcl 在窗口应用中可以用于两个目的: 配置应用的界面动作,配置应用的界面外观。在下面的段落中讨论这两个用途。
Tcl 的第一个用法是用于界面动作。理想的,对应用重要的每个事件都应当绑定上 Tcl 命令。每次击键、每次鼠标移动或鼠标按钮按下(或释放)、和每个菜单条目都应当关联上 Tcl 命令。
当事件发生时,首先把它映射到它的 Tcl 命令上,接着通过把这个命令传递到 Tcl_Eval 来执行它。应用不应当直接接收任何动作;所有动作都应当首先通过 Tcl 来传递。进一步,应用应当提供 Tcl 命令允许用户改变与任何事件相关联的 Tcl 命令。
在交互式的窗口应用中,Tcl 的使用可能对于初级用户是不可见的: 他们将使用按钮、菜单和其他界面构件来操纵应用。但是,如果使用 Tcl 作为所有界面动作的中间媒介,则会产生两个好处。首先,使得写 Tcl 程序来重新配置界面成为可能。例如,用户将能够重新绑定击键、改变鼠标按钮、或把一个现存的操作替代为指定为一组 Tcl 命令或 tclproc 的更加复杂的操作。第二个好处是这种方式强制所有的应用的功能都可通过 Tcl 来访问: 任何可以使用鼠标或键盘调用的东西都可以使用 Tcl 程序调用。这使得有可能写模拟程序动作的 tclproc,或把程序的基本动作组合到更加强力的动作中。这还允许交互式会话作为一序列 Tcl 命令而被记录和重演(参见章节 5)。
在窗口应用中 Tcl 的第二个用法是配置应用的外观。所有应用的界面构件(X 术语是“组件-widgets”),比如标签、按钮、文本录入框、菜单和滚动条应当可以使用 Tcl 命令来配置。例如,在按钮的情况下,应用(或按钮组件代码)应当提供 Tcl 命令来改变按钮的大小和位置、它的文本、它的颜色、和动作(当然是一个 Tcl 命令),在这个按钮活跃的时候调用。这使得有可能让用户写 Tcl 程序来个人化他们所使用的应用的格局和外观。这种重新配置能力的最常见使用、可能就是用在程序开始执行时自动读取的 Tcl 命令文件中。但是,也可以用 Tcl 命令在应用运行的时候改变它的外观,如果能证明有用的话。
如果按上面描述的那样使用 Tcl,则它将充当一种用户界面的说明语言。可以写用户界面编辑器来显示组件,并让用户重新安排它们并配置属性,比如颜色和相关联的 Tcl 命令。界面编辑器可以接着把关于界面的信息输出为 Tcl 命令文件,在应用启动的时候读取。一些当前的界面编辑器输出 C 代码,需要接着编译到应用中[7];不幸的是,这种方式要求重新编译一个应用来改变它的界面(或者作为可供选择的一种方式,要求动态代码装载设施)。如果把 Tcl 用作界面说明语言,则不需要重新编译,而且一个简单的应用二进制程序可以支持多种不同的界面。
5. 应用间通信
如果在一个环境中的所有工具都基于相同的可嵌入命令语言如 Tcl,则好处就更大的。首先,用户只需要学习一门基本的命令语言;要从一个应用移动到另一个只需要为这个新应用学习(很少的?)特定于应用的命令。其次,通用的界面编辑器成为可能,如前面章节描述的那样。第三,按我的观点最重要的是,Tcl 可以提供在应用之间通信的一种方式。
我已经用叫做 send 的一个额外的 Tcl 命令、为 X11 实现了一种通信机制。为了让 send 工作,与 X11 应用相关联的每个 Tcl 解释器都要给出一个文本的名字,比如称呼 X 邮件处理器为 xmh,称呼 mx 在其中显示叫做 foo.c 文件的那个窗口为 mx.foo.c。send 命令接受两个参数: 解释器的名字和要在这个解释器中执行的 Tcl 命令。send 安排把这个命令传递到包含指名解释器的进程;命令由那个解释器执行并把结果(返回代码和字符串)返回给发起 send 命令的应用。
send 的 X11 实现使用了与根窗口连结的一个特殊属性。这个属性存储所有解释器的命令加上给每个解释器的窗口标识符。通过把命令添加到在解释器相关联的窗口中的特定属性上来发送它。拥有这个解释器的进程检测到这个属性的变更。它读取这个属性,执行命令,并把结果信息添加到与发送应用相关联的属性上。最后,发送应用检测到这个属性的变更,读取结果信息,并把它返回为 send 命令的结果。
send 命令为一个应用控制另一个应用提供了一种强力方式。例如,调试器在单步跟踪一个程序的时候可以发送命令到编辑器来高亮当前源代码行。还有,用户界面编辑器可以使用 send 来直接操纵一个应用的窗口: 不用修改界面编辑器显示的应用界面的伪装(dummy)版本,界面编辑器可以使用 send 来修改一个“活着的”应用的界面,此时还能保存配置到配置文件中。这将允许界面设计者试验新界面的观感并对界面做出改变逐步增加上去。
使用 send 的另一个例子是改变用户偏好。如果一个用户来到了已经为其他用户配置好的显示器面前,这个新用户可以运行一个程序来找出几乎所有在屏幕上现存的应用程序(通过查询包含它们名字的属性),对每个应用读取这个新用户的配置文件,并向这些应用发送命令来按这个新用户的偏好来重新配置它们。当老用户回来的时候,他可以调用同一个程序恢复最初的偏好。
send 还可以用来记录涉及多个应用的交互式会话,并在以后重演这个会话(就是说,用于演示目的)。这需要叫做 trace 的一个额外的 Tcl 命令;trace 将接受一个单一的参数(一个 Tcl 命令字符串),并导致在这个解释器中在每个其他命令执行之前执行这个命令字符串。在一个单一的应用中,将使用 trace 在每个 Tcl 命令执行之前记录它,这样这些命令就可以在以后重演。在多应用环境中,可是使用 send 建造记录器程序。记录器发送 trace 命令到需要记录的每个应用。trace 命令安排把同在这个应用中执行的每个命令有关的信息发送回到记录器。记录器接着把关于哪个应用执行了哪个命令的信息记入日志。记录器可以把命令再次发送回到那个应用来重新执行它们。trace 命令在 Tcl 还不存在,但它是很容易增加的。
send 为进程间通信提供了比今天能获得的机制更加强力的机制。今天的应用能用到的唯一易用的通信形式是选择或剪切缓冲区: 它是一个单一的字符串,可以由一个应用设置而由另一个应用读取。send 提供了类似于远程过程调用[3]的一种更一般的通信形式,如果所有应用的功能都可以通过 Tcl 来获得,如在第 4 节描述的那样,则 send 能让每个应用的功能都对其他应用也是可能获得的。
如果 Tcl (和 send)在窗口应用中得到广泛使用,我相信会出现一种更好的交互环境,它是由大量小而专门的应用而不是一些单体(monolithic)应用组成。今天的应用不能很好的相互通信,所以每个应用都必须结合它需要的所有功能。例如,一些基于窗口的调试器包含了内置的文本编辑器,这样他们才可以高亮当前的执行点。使用了 Tcl 和 send,调试器和编辑器就可以是单独的程序,在需要的时候相互发送命令。理想的, 可以用以一种令人激动的新方式一起工作的大量小应用替代单体应用,就象 UNIX shell 允许把大量小的文本处理应用组合在一起。我认为 Tcl,或类似它的某种其他语言,将提供把 1990 年代的窗口应用绑定在一起的胶水。
6. 状态和性能
Tcl 语言是在 1987 年秋天设计并在 1988 年冬天实现的。在 1988 年春天我把 Tcl 结合到 mx 编辑器中(它已经存在了,但是带有低级的命令语言),还结合到叫 Tx 的一个作为同伴的终端模拟器中。在这一年半中,这些程序都已经在 Berkeley 的一个小型用户社区中使用了。上面描述的所有的 Tcl 语言设施都是存在的,除了 send 命令仍是原型形式和 trace 仍未实现之外。在第 4 节中描述的某些特征,比如菜单和击键绑定在 mx 中实现了,但是以一种特别的样式: Tcl 仍未与组件集相整合。我正在建造一个新的工具箱、和完全基于 Tcl 的组件集。当它完成的时候,我希望它能提供在第 4 节描述的所有特征。在本文写作的时候,实现才刚刚起步。
Tcl 命令 | Sun-3 时间 (微秒) | DS3100 时间(微秒) |
set a 1 |
225 | 57 |
list abc def ghi jkl |
460 | 138 |
if {4 > 3} {set a 1} |
700 | 220 |
proc fac x { if { $x == 1} {return 1} return [expr {$x*[fac [expr $x-1]]}] } |
1280 | 380 |
fac 5 |
11250 | 3630 |
表格 II. 各种 Tcl 命令的花费,在 Sun-3/75 工作站和 DECstation3100 上进行的测量。命令 fac 5 执行了总计 23 个 Tcl 命令,平均命令时间大约是在 Sun-3 上 500 微秒在 DECstation3100 上 160 微秒。 |
表格 II 展示了 Tcl 在两种不同的工作站上执行各种命令花费的时间。在 Sun-3 工作站上,简单命令的平均时间是大约 500 微秒,而在 DECstation 3100 上命令的平均时间是大约 160 微秒。尽管 mx 目前不对每次鼠标移动都使用一个 Tcl 命令,在表格 II 中的时间表明了,即使是在 Sun-3 工作站上,没有响应的明显减低是可能的。例如,如果鼠标移动事件每秒发生 100 次,Tcl 为每个事件分派一个命令的花费将只消耗 Sun-3 处理器的 1-2%。在目前使用 Tcl 的方式下(击键和菜单绑定由一些命令组成),没有明显与 Tcl 有关的延迟,对于特定于应用的命令比如 mx 编辑器的那些命令,执行这些命令的时间比 Tcl 分析它并调用命令过程需要的时间要大很多。
Tcl 库足够小以便用在各种程序中,甚至是没有共享库机制的系统上。Tcl 代码由大约 7000 行 C 代码组成(其中一半是注释)。在 Motorola 68000 上编译的时候,它生成大约 27000 字节的目标代码。
7. 比较
Tcl 语言在表面上相当类似于 Lisp,除了 Tcl 使用花括号或方括号替代了圆括号,和在命令的最外层不需要包围圆括号之外。在 Tcl 和 Lisp 之间最大的区别是 Lisp 缺省的求值参数,而在 Tcl 中参数不被求值除非包围着方括号。这意味着在 Tcl 中如果一个参数需要被求值则需要做更多的键入努力,而在 Lisp 中如果一个参数要被引用(不被求值)则需要做更多的键入努力。就我而言对于命令语言不求值通常是更需要的结果,所以我在 Tcl 中做了这种缺省。Tcl 还有比 Lisp 更少的数据类型;这么做是为了简化 Tcl 库和围绕它的 C 应用之间的接口。
在提供可以用来控制很多不同的应用程序的一个框架方面,Emacs 编辑器类似于 Tcl。例如,子进程可以在 Emacs 窗口中运行,并且用户可以写 Emacs 命令脚本来 (a) 生成输入到应用的命令序列和 (b) 重新格式化应用的输出。这允许用户去修饰应用的基本设施,编辑它们的输出,诸如此类。在 Emacs 和 Tcl 之间的不同是编程能力集中在 Emacs 中:应用不能相互交流除非 Emacs 充当中间媒介(就是说在两个应用之间架设通信机制,要在应用之间来回传递信息、代码必须写在 Emacs 中)。Tcl 的方式是分散的:每个应用都有它自己的命令解释器并直接相互通信。
最后,比较 Tcl 和 NeWS [9] 是很有趣的,它是基于 Postscript 语言的一个窗口系统。NeWS 允许应用下载 Postscript 程序到窗口服务器中来改变用户界面和修改系统的其他方面。在某种意义上,这类似于 Tcl 中的 send 命令,在这种系统中,应用可以把程序发送到服务器上面去执行。但是,NeWS 的机制比 Tcl 更少一般性:NeWS 应用生成 Postscript 程序作为输出,但是它们不是必须响应作为输入的 Postscript 程序。换句话说,NeWS 应用可以通过控制服务器来影响其他应用的界面,但是它们不能像在 Tcl 中那样,直接的调用其他应用的特定于应用的操作。
作为总结,Tcl 方式比 Emacs 或 NeWS 方式更少集中性。对于有着大量独立的工具的窗口环境,我认为分散的方式更有意义。出于对 Emacs 的公平,指出 Emacs 不是为这种环境而设计是很重要的,Emacs 在它设计用于的环境中工作的非常令人满意(ASCII 终端和批处理方式的应用)。值得一提的还有应用之间的直接通信不是 NeWS 系统设计明确的目标。
8. 结论
我认为 Tcl 能以三种一般的方式来改善我们的交互环境。首先,Tcl 可以用来通过向单独的工具提供可编程的命令解释器来增进它们;这允许用户去定制工具和扩展它们的功能。其次,Tcl 可以提供跨越各类工具的一个统一的命令语言;这使它便利了用户编程这些工具,并允许建造独立于工具的设施,比如界面编辑器。第三,Tcl 为工具相互控制提供一种机制;这鼓励了为窗口应用提供一种更模块化的方式,并使以新方式重新使用旧应用成为可能。按照我的观点第三个好处是潜在的最重要的。
迄今为止我对 Tcl 经验是积极而有限的。在被彻底的评估之前,Tcl 需要更大的用户社区和更完全的整合到一个窗口工具箱中。Tcl 库的源代码目前对于公众是免费的、不授加许可的形式,我希望不久就能生产出基于 Tcl 的工具箱。
9. 致谢
Sprite 计划的成员为基于 Tcl 的编辑器和终端模拟器充当了廉价劳工;没有他们的帮助这门语言是不能发展到今天的状态的。Fred Douglis、John Hartman、Ken Shirriff 和 Brent Welch 提供了有益的评论来增进本文的表述。
10. 引用
[1] Abelson, H. and Sussman, G.J. Structure and Interpretation of Computer Programs, MIT Press, Cambridge, MA, 1985.
[2] Adobe Systems, Inc. Postscript Language Tutorial and Cookbook, Addison-Wesley, Reading, MA, 1985.
[3] Birrell, A. and Nelson, B. ‘‘Implementing Remote Procedure Calls.’’ ACM Transactions on Computer Systems, Vol. 2, No. 1, February 1986, pp. 39-59.
[4] Brodie, L. Starting FORTH: An Introduction to the FORTH Language and Operating System for Beginners and Professionals, Prentice Hall, Englewood Cliffs, NJ, 1981.
[5] Kernighan, B.W. and Pike, R. The UNIX Programming Environment, Prentice Hall, Englewood Cliffs, NJ, 1984.
[6] Kernighan, B.W. and Ritchie, D.M. The C Programming Language, Second Edition, Prentice Hall, Englewood Cliffs, NJ, 1988.
[7] Mackey, K., Downs, M., Duffy, J., and Leege, J. ‘‘An Interactive Interface Builder for Use with Ada Programs,’’ Xhibition Conference Proceedings, 1989.
[8] Stallman, R. GNU Emacs Manual, Fourth Edition, Version 17, February 1986.
[9] Sun Microsystems, Inc. NeWS Technical Overview, Sun Microsystems, Inc. PN 800-1498-05, 1987.