内容


用 Guile 编写脚本

通过扩展语言增强 C 和 Scheme

Comments

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 脚本编写模型
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 使它变得更加强大和有用!


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=377854
ArticleTitle=用 Guile 编写脚本
publish-date=03232009