ANSI Common Lisp 标准中也没有这个内容。因此,与上章中介绍的与操作系统的交互一样,外部函数接口也依赖于运行的操作系统和具体实现方式。

示例:在CLISP 中调用 gethostname

注: 在查看示例前,推荐阅读 CLISP 实现的注解中的 相关章节

int gethostname(char *name, int len) 遵循标准的 C 语言的显示参数约定 —— 接受一个指针,该指针指向需要写入的buffer。因此,必须将这个参数看作是 :OUT:IN-OUT
不仅如此,还需要知道这个 buffer 的大小。这这个函数中, len 就是个 :IN 参数。 有时也会是个 :IN-OUT 参数,,返回 buffer 的实际大小。

所以, name 实际上是指向长度为 len 的字符数组,不管 char * 在 C 中的原型是什么,其作用类似于一个 C 字符串(以 0 为结尾)。那么这个数组中有多少个元素呢?幸运的是,在以下的示例中,不需要去计算一个 C 语言结构体的大小(sizeof())。 这将返回其主机名. Solaris 2.x 手册上写着 “主机名被限制在 MAXHOSTNAME(256)个字符之内“。

当然,在下面的例子中,也可以像 C 语言中申请临时的数据堆一样,使用 :ALLOCA 来申请堆。但是为什么 Lisp 表现的会比 C 要差呢?这就要提到给外部函数生成以下的有效签名了:

  1. (ffi:def-c-call-out gethostname
  2. (:arguments (name (ffi:c-ptr (ffi:c-array-max ffi:char 256))
  3. :out :alloca)
  4. (len ffi:int))
  5. ;; (:return-type BOOLEAN) could have been used here
  6. ;; (Solaris says it's either 0 or -1).
  7. (:return-type ffi:int))
  8. (defun myhostname ()
  9. (multiple-value-bind (success name)
  10. ;; :OUT or :IN-OUT parameters are returned via multiple values
  11. (gethostname 256)
  12. (if (zerop success)
  13. (subseq name 0 (position #\null name))
  14. (error ... ; errno may be set
  15. ...))))
  16. (defvar hostname (myhostname))

可能是因为 SUBSEQPOSITION 这两个函数是多余的吧,因为 C-ARRAY-MAXC-ARRAY 刚好相反:

  1. (defun myhostname ()
  2. (multiple-value-bind (success name)
  3. ;; :out or :in-out parameters are returned via multiple values
  4. (gethostname 256)
  5. (if (zerop success) name
  6. (error ... ; errno may be set
  7. ...))))

示例:在 Allegro CL 中调用 gethostname

该示例和上面的示例一样,只不过是在 Allegro Common Lisp 6或更高的版本中实现的。ACL(Allegro Common Lisp)中不会明确的区分 inputoutput 的参数。将参数声明成 output (也就是说,可以用 C 语言进行修改) 的方法是使用数组,因为数组是通过引用传递的,因此在 C 语言中会接收到指向内存地址的指针(这是它所期望的)。在这种情况下,gethostname() 接收一个字符数组,而简单的字符数组在 Lisp 中与 C 语言中本质上表示相同的东西,这就让事情变得更简单了。因此,外部函数定义如下

  1. (def-foreign-call (c-get-hostname "gethostname")
  2. ((name (* :char) (simple-array 'character (*)))
  3. (len :int integer))
  4. :returning :int)

现在开始逐行来分析:首先,第一行中定义了个 C-GET-HOSTNAME 的函数,该函数会调用 C 语言的 gethostname() 函数。这个函数有两个参数:第一个叫做 NAME,即一个字符指针(*char C语言中的格式),Lisp 中叫做字符数组;第二个参数叫做 LEN,是个整型。然后该函数返回一个整型。

然后,以 Lisp 的思路来看是这样的:

  1. (defun get-hostname ()
  2. (let* ((name (make-array 256 :element-type 'character))
  3. (result (c-get-hostname name 256)))
  4. (if (zerop result)
  5. (let ((pos (position #\null name)))
  6. (subseq name 0 pos))
  7. (error "gethostname() failed."))))

函数创建了一个 NAME 数组,将 C-GET-HOSTNAME 返回的结果存入其中,然后在对得到的结果检查。如果返回结果为 0,那么调用成功,然后 NAME 中 0 字符(NULL,C 语言中字符串的终止符)之前的内容,否则,将抛出异常。注意,与上个例子不同的是,这次是在 Lisp 中创建字符串,同时,这次会在函数运行结束后,Lisp 的垃圾回收机制会将该字符串回收掉。以下是示例:

  1. * (get-hostname)
  2. "terminus"

一般来说,处理字符要比上个例子容易。假设你想在 Lisp 中调用 getenv() 来访问环境变量的值。getenv() 接受一个字符串参数(变量名)并返回另一个字符串(变量值)。切确地说,这个参数是个指针,指向调用程序需要分配的字符序列,返回值是个指针,指向(环境中)已存在的字符序列。以下是 C-GETENV 的定义:

  1. (def-foreign-call (c-getenv "getenv")
  2. ((var (* :char) string))
  3. :returning :int
  4. :strings-convert t)

在这个示例中,参数仍是个指向字符的 C 指针,但在 Lisp 中可以将其定义为 STRING。返回值也是个指针,因此,可以将其定义为整型。最后, :STRINGS-CONVERT 参数表明 ACL 需要自动将第一个传递进来的 Lisp 字符串转换成 C 字符串。以下是其用法:

  1. * (c-getenv "SHELL")
  2. -1073742215

如果对返回值不解的话,要记住 C-GETENV 返回的是指针,Lisp 需要知道如何处理指针所指向的内存的内容。在这种情况下我们就知道这是指向一个 C 字符串,因此就可以使用 FF:NATIVE-TO-STRING 函数将它转换成 Lisp 字符串:

  1. * (native-to-string (c-getenv "SHELL"))
  2. "/bin/tcsh"
  3. 9
  4. 9

(其中第二、三个值是拷贝相应的字符和字节的数量)。注意:如果请求一个不存在的变量的值,C-GETENV 将返回 0,同时 NATIVE-TO-STRING 将会失败。因此,更健全的代码示例是:

  1. * (let ((ptr (c-getenv "NOSUCHVAR")))
  2. (unless (zerop ptr)
  3. (native-to-string ptr)))
  4. NIL