用 Guile 编写脚本
通过扩展语言增强 C 和 Scheme
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 显示了这个例子的源代码。
清单 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 语法的话,会发现上述格式有所不同,但是更易于阅读。然后,我可以像使用任何其他原语一样使用这个新过程,如下所示:
guile> (square 5) 25
条件
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 函数,这些函数实现相同的行为,但是第一个函数有一个参数,第二个函数有一个静态值。
在清单 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 全球网站上的 英文原文。
- GNU 扩展语言主页 提供了最新的 Guile、FAQ 和文档(免费提供内容丰富的 Guile 手册)。在 GNU 网站还有一套 Guile 资源,这是 Guile 入门的最佳起点。
- GNU Electronic Design Automation(gEDU) 包和 Scheme Constraints Window Manager (Scwm) 是将 Guile 用于嵌入式脚本的两个项目。
- UnrealScript 是 Epic Games 为 Unreal Engine 设计的脚本语言。这种脚本语言使游戏社区可以编写新的 in-game 内容。
- 在 Gamasutra 上阅读 用 Python 编写游戏脚本。
- 查看 Scheme Wiki 和 Guile 站点上的 Scheme 资源,其中包括一个 scheme 代码库、Scheme 语言标准和笔记。虽然 Scheme 是一种较老的语言,但是它本身提供的一些特性却非常值得学习。例如,递归在大多数语言中都只是介绍性的专题,但是在 Scheme 中却极为重要。
- Glenn Vanderburg 的站点 归档了展示 Guile 起步的地方的 UseNet 帖子:著名的 “Tcl War” UseNet 论坛。争论始于 Richard Stallman 提出的温和论题 “为什么不应该使用 tcl”,结果却出人意料地引发了长达一个月的激烈辩论。
- 在 developerWorks Linux 专区 寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料,查阅我们 最受欢迎的文章和教程。
- 在 developerWorks 上查阅所有 Linux 技巧 和 Linux 教程。