级别: 中级 M. Tim Jones, 顾问工程师, Emulex Corp.
2009 年 3 月 23 日 Scheme 是一种编程语言,而 Guile(Scheme 解释器和库)将它转换成嵌入式脚本语言,从而很好地将动态性引入到静态应用程序中。现在我们将快速概览 Guile,发现它在构建可扩展应用程序方面的强大特性。
Guile 问世于 1995 年,它是用于 Scheme 语言的解释器,Scheme 语言是简化 Lisp 语言得到的派生物,而 Lisp 语言则是由 John McCarthy 在 1958 年首次提出的。但是 Guile 使 Scheme 变成嵌入式的,因此 Guile 是用于编写嵌入式脚本的理想解释器。Guile 不仅仅是一种扩展语言:它是 GNU 项目的官方扩展语言。您将发现在很多开源应用程序中都使 Guile 来编写脚本 — 从 gEDA CAD 工具到 Scheme Constraints Window Manager(Scwm),后者通过 Scheme 脚本编写提供动态配置性(见 参考资料 小节)。Guile 在通过编写脚本来扩展应用程序方面有过成功的历史,包括在 GNU Emacs、GIMP 和 Apache Web Server 中。
Guile 的特长是可扩展性;如图 1 所示。通过使用 Guile,可以解释 Scheme 脚本,将 Scheme 脚本动态地绑定到编译过的 C 程序,甚至可以将编译过的 C 函数集成到 Scheme 脚本中。这个非常有用的特性使用户可以调整或定制应用程序,增加它们的价值。
图 1. Guile 脚本编写模型
应用程序定制的最好的例子是在视频游戏行业。视频游戏允许通过编写脚本进行大量的定制。很多游戏程序甚至在核心设计中使用脚本,用脚本来实现某些方面(例如非玩家角色行为)。
一个简单的例子
我们来看一个简单的将 Guile 集成到 C 程序中的例子。在这个例子中,我使用一个 C 程序,这个 C 程序调用一个 Scheme 脚本。清单 1 和清单 2 显示了这个例子的源代码。
 |
游戏中的脚本
现代游戏常常使用脚本语言,包括从 Python 和 Ruby 等传统的解释语言到专用脚本语言,例如 UnrealScript (见 参考资料 小节)。在游戏系统中,这些脚本语言可以用来实现非玩家角色的行为,甚至是出现在游戏中的物体的行为。通过使用脚本,开发游戏更加方便,因为不需要很长的编译周期就可以引入新的行为。如果深入探究基于 PC 的游戏的子目录,就很可能发现脚本。
|
|
清单 1 展示了调用 Scheme 脚本的 C 应用程序。首先要注意的是,其中包括了 libguile.h 头文件,这使程序中可以使用必要的 Guile 符号。接下来,注意程序中定义了一个新的类型:SCM。这个类型是一种抽象的 C 类型,它表示 Guile 中包含的所有 Scheme 对象。在此,我用它表示后面调用的 Scheme 函数。
对于任何要使用 Guile 的线程,首先要做的是调用 scm_init_guile。该函数初始化 Guile 的全局状态,它必须在任何其他 Scheme 函数之前调用。接下来,在调用 Scheme 函数之前,必须装载包含该函数的文件。这可以通过 scm_c_primitive_load 函数来完成。注意这里的命令:函数中的 _c_ 表明给它传递一个 C 变量(而不是 Scheme 变量)。
接下来,我使用 scm_c_lookup 发现并返回符号所绑定的变量(模型中的 Scheme 函数),再用 scm_variable_ref 取消对其的引用,然后存储到 Scheme 变量 func 中。最后,我使用 scm_call_0 调用 Scheme 函数。这个 Guile 函数调用之前定义的不带参数的 Scheme 函数。
清单 1. 调用 Scheme 脚本的 C 程序
#include <stdio.h>
#include <libguile.h>
int main( int argc, char **arg )
{
SCM func;
scm_init_guile();
scm_c_primitive_load( "script.scm" );
func = scm_variable_ref( scm_c_lookup( "simple-script" ) );
scm_call_0( func );
return 0;
}
|
清单 2 显示了从 C 程序中调用的 Scheme 函数。该函数使用 display 过程将一个字符串打印到屏幕上。在这个函数之后,调用 newline 过程,结果会输出一个回车。
清单 2. 从 C 调用的 Scheme 脚本(script.scm)
(define simple-script
(lambda ()
(display "script called") (newline)))
|
有趣的是,脚本不是静态地绑定到 C 程序的;它是动态 绑定的。Scheme 脚本可以更改,当执行前面编译过的 C 程序时,将执行脚本中实现的新行为。这就是嵌入式脚本编写的威力:既获得已编译应用程序的速度,又具有动态脚本的可扩展性。
现在,您已经有了一个简单的例子,接下来让我们更深入一点,探索 C 语言中 Scheme 脚本编写的其他元素。
Scheme 简介
可能有些人对 Scheme 还比较陌生,我们来看一些演示这种语言的威力的例子。这些例子演示 Scheme 的一些关键特性,此外还演示了变量、条件和循环。对 Scheme 的全面论述超出了本文的范围,但是可以在 参考资料 小节找到链接。
在这些例子中,我使用了 Guile 解释器,通过它可以实时地使用 Scheme,提供 Scheme 代码并立即看到结果。
变量
Scheme 是一种动态类型语言;因此,变量的类型通常只有到了运行时才知道。所以,Scheme 变量实际上就是容器,它们的类型可以在以后定义。
变量是使用 define 原语(primitive)创建的,并且可以使用 set! 原语更改。在此,我只需做以下事情:
guile> (define my-var 3)
guile> (begin (display my-var) (newline))
guile> (set! my-var (* my-var my-var))
|
过程
毫不奇怪,在 Scheme 中还可以创建过程 — 也是用 define 原语来完成。过程可以是匿名的(lambda 过程),也可以是命名的。对于命名的过程,它们被存储在一个变量中,如下所示:
(define (square val) (* val val))
|
如果您刚好熟悉传统的 Lisp 语法的话,会发现上述格式有所不同,但是更易于阅读。然后,我可以像使用任何其他原语一样使用这个新过程,如下所示:
条件
Scheme 有几种方式可实现条件。最基本的是简单的 if 条件。它定义一个测试条件,一个 true 表达式,以及一个可选的 false 表达式。在下面的例子中,可以看到 Scheme 的列表处理透视图。该列表以 if 开始,以 (display "less") 结束。记住,Scheme 是 Lisp 的派生物,因此它是基于列表的。Scheme 将代码和数据都表示为列表,这使得该语言可以模糊代码与数据之间的界限(代码可以是数据,数据也可以是代码)。
guile> (define my-var 3)
guile> (if (> my-var 20) (display "more") (display "less"))
less
|
循环
Scheme 通过递归实现循环,这导致实现循环时需要一种特殊的思维模式。但是,它是迭代的一种自然方式。下面的例子演示一个 Scheme 脚本,该脚本从 0 迭代到 9,然后打印 done。这个例子使用 Scheme 中所谓的尾递归(tail recursion)。注意,在循环的结尾处,我用一个比上次大 1 的参数递归地调用同一个函数,以实现循环的迭代。在传统的语言中,这样的递归需要连续不断的入栈操作,以维护调用的历史;而在 Scheme 中,却不是这样。最后的调用(尾(tail))只是调用函数,没有任何过程调用或栈维护开销。
(let countup ((i 0))
(if (= i 10) (begin (display "done") (newline))
(begin
(display i)
(newline)
(countup (+ i 1)))))
|
另一种在 Scheme 中实现循环的有趣方式是使用 map 过程。这个概念只是将一个过程应用(或映射)到一个列表,如下面的例子所示。这种方法既简单,又具有可读性。
guile> (define my-list '(1 2 3 4 5))
guile> (define (square val) (* val val))
guile> (map square my-list)
(1 4 9 16 25)
|
用 Scheme 脚本扩展 C 程序
正如 清单 1 所示,用 Scheme 扩展 C 程序非常容易。接下来再看一个例子,这个例子探索其他一些可用于建立 C 与 Scheme 之间桥梁的应用程序编程接口(API)。在大多数应用程序中,不仅需要调用 Scheme,还需要传递参数给 Scheme 函数,接收返回值,并在两个环境之间共享变量。Guile 提供了一套丰富的函数来支持各种功能。
Guile 试图跨越两个环境之间的界线,将 Scheme 的威力扩展到 C。这体现在通过 Guile API 扩展到 C 的 Scheme 概念,包括动态类型、持续(continuation)和垃圾收集等。
将 Scheme 概念扩展到 C 的一个例子是从 C 环境动态创建新的 Scheme 变量。用于创建 Scheme 变量的 C 函数是 scm_c_define。还记得吗,_c_ 表明提供一个 C 类型作为参数。如果已经有 Scheme 变量(例如由 scm_c_lookup 函数提供),则可以使用 scm_define。除了在 C 中创建 Scheme 变量外,还可以取消引用 Scheme 变量,在两个环境之间转换值。我在清单 3 中演示了这些方面的例子。
清单 3 和清单 4 提供了 C 与 Scheme 之间交互的两个例子。第一个例子演示如何从 C 调用一个 Scheme 函数,传入一个参数,并捕获返回值。第二个例子创建一个 Scheme 变量,以便传入参数。清单 4 展示了 Scheme 函数,这些函数实现相同的行为,但是第一个函数有一个参数,第二个函数有一个静态值。
 |
scm_call 的限制
Guile 提供了 scm_call 的 5 种变体。可以不带参数调用 Scheme 函数(scm_call_0),或者最多带 4 个参数调用它(scm_call_4),于是通过 Guile 传递的 Scheme 变量的个数就限制为 4。另外,不支持可变参数函数。如果需要传递 4 个以上的参数,或者传递数量不定的参数,那么可以构造一个 Scheme 列表对象,其中包含所需数量的参数。
|
|
在清单 3 中的第一个例子中,我使用 scm_call_1 函数调用 Scheme 函数,调用时带有一个参数。注意,这里必须将 Scheme 值传递给函数:scm_int2num 函数用于将 C 整数转换成 Scheme 数值数据类型。可以使用相反的 scm_num2int 函数将 Scheme 变量 ret_val 转换成 C 整数值。
清单 3 中的第二个例子首先用 scm_c_define 创建一个新的 Scheme 变量,并以一个 C 字符串变量(sc_arg)标识它。使用类型转换函数 scm_int2num 自动初始化这个变量。现在已经创建了 Scheme 变量,接下来就可以调用 Scheme 函数 square2(这一次没有参数),并用相同的方式捕捉和取消引用返回值。
清单 3. 用 C 探索 Scheme 函数和变量
#include <stdio.h>
#include <libguile.h>
int main( int argc, char *argv[] )
{
SCM func;
SCM ret_val;
int sqr_result;
scm_init_guile();
/* Calling the square script with a passed argument */
scm_c_primitive_load( "script.scm" );
func = scm_variable_ref( scm_c_lookup( "square" ) );
ret_val = scm_call_1( func, scm_int2num(7) );
sqr_result = scm_num2int( ret_val, 0, NULL );
printf( "result of square is %d\n", sqr_result );
/* Calling the square2 script using a Scheme variable */
scm_c_define( "sc_arg", scm_int2num(9) );
func = scm_variable_ref( scm_c_lookup( "square2" ) );
ret_val = scm_call_0( func );
sqr_result = scm_num2int( ret_val, 0, NULL );
printf( "result of square2 is %d\n", sqr_result );
return 0;
}
|
清单 4 显示了清单 3 中的 C 程序所使用的两个 Scheme 过程。第一个过程 square 是一个传统的 Scheme 函数,它接受一个参数,然后返回一个结果。第二个过程 square2 不接受参数,但是对一个 Scheme 变量(sc_arg)进行操作。和前一个过程一样,该过程也返回结果。
清单 4. 从清单 3 中调用的 Scheme 脚本(script.scm)
(define square
(lambda (x) (* x x)))
(define square2
(lambda () (* sc_arg sc_arg)))
|
用 C 函数扩展 Scheme 脚本
在这最后一个例子中,我探索从 Scheme 脚本中调用 C 函数的过程。首先,从清单 5 中的 Scheme 可调用函数开始。首先需要注意的是,虽然这是一个 C 函数,但是它接受一个 Scheme 对象,并返回一个 Scheme 对象(SCM 类型)。首先,我创建一个 C 变量,并在 scm_num2int 函数(将 Scheme 数值类型转换成 C int)中使用它来捕捉 SCM 参数。然后,我平方参数,并通过另一个 scm_from_int 调用返回它。
清单 5 中程序剩下的部分用于设置环境,以引导到 Scheme。初始化 Guile 环境之后,我用一个 scm_c_define_gsubr 调用将 C 函数导出到 Scheme,调用时以函数在 Scheme 中的名称、参数的数量(必需的参数,可选的参数,以及剩下的参数)和要导出的实际 C 函数作为参数。其余部分和之前看到的一样。我将装载 Scheme 脚本,获得对特定 Scheme 函数的引用,然后不带参数调用它。
清单 5. 为 Scheme 设置环境的 C 程序
#include <stdio.h>
#include <libguile.h>
SCM c_square( SCM arg)
{
int c_arg = scm_num2int( arg, 0, NULL );
return scm_from_int( c_arg * c_arg );
}
int main( int argc, char *argv[] )
{
SCM func;
scm_init_guile();
scm_c_define_gsubr( "c_square", 1, 0, 0, c_square );
scm_c_primitive_load( "script.scm" );
func = scm_variable_ref( scm_c_lookup("main-script") );
scm_call_0( func );
return 0;
}
|
清单 6 提供 Scheme 脚本。该脚本显示 c_square 函数调用的响应,该函数是清单 5 中的 C 程序导出的函数。
清单 6. 调用 C 函数的 Scheme 脚本(script.scm)
(define main-script
(lambda ()
(begin (display (c_square 8))
(newline))))
|
这是一个很普通的例子,但是它展示了在两种语言环境之间共享代码和变量有多么容易!
结束语
构建和交付静态软件和产品的时代已经结束。当今,用户期望他们的产品是动态的,并且可轻松定制。虽然这种变革会带来新的复杂性,但是它最终让用户通过我们的应用程序创造新的价值。本文希望帮助您了解 Guile 的威力。Scheme 也许是至今仍在使用的最古老的编程语言之一,但它也是最强大的编程语言之一。Guile 使它变得更加强大和有用!
参考资料 学习
获得产品和技术
- 用可直接从 developerWorks 下载的 IBM 试用软件 构建您的下一个 Linux 开发项目。
讨论
关于作者  | |  | M. Tim Jones 是一名嵌入式软件工程师,他是 Artificial
Intelligence: A Systems Approach, GNU/Linux Application Programming(现在已经是第 2 版)、AI Application Programming(第 2 版)和 BSD Sockets Programming from a Multilanguage Perspective 等书的作者。他的工程背景非常广泛,从同步宇宙飞船的内核开发到嵌入式系统架构设计,再到网络协议的开发。Tim 是位于科罗拉多州 Longmont 的 Emulex Corp. 的一名顾问工程师。 |
对本文的评价
|