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 要差呢?这就要提到给外部函数生成以下的有效签名了:
(ffi:def-c-call-out gethostname
(:arguments (name (ffi:c-ptr (ffi:c-array-max ffi:char 256))
:out :alloca)
(len ffi:int))
;; (:return-type BOOLEAN) could have been used here
;; (Solaris says it's either 0 or -1).
(:return-type ffi:int))
(defun myhostname ()
(multiple-value-bind (success name)
;; :OUT or :IN-OUT parameters are returned via multiple values
(gethostname 256)
(if (zerop success)
(subseq name 0 (position #\null name))
(error ... ; errno may be set
...))))
(defvar hostname (myhostname))
可能是因为 SUBSEQ
和 POSITION
这两个函数是多余的吧,因为 C-ARRAY-MAX
与 C-ARRAY
刚好相反:
(defun myhostname ()
(multiple-value-bind (success name)
;; :out or :in-out parameters are returned via multiple values
(gethostname 256)
(if (zerop success) name
(error ... ; errno may be set
...))))
示例:在 Allegro CL 中调用 gethostname
该示例和上面的示例一样,只不过是在 Allegro Common Lisp 6或更高的版本中实现的。ACL(Allegro Common Lisp)中不会明确的区分 input
和 output
的参数。将参数声明成 output
(也就是说,可以用 C 语言进行修改) 的方法是使用数组,因为数组是通过引用传递的,因此在 C 语言中会接收到指向内存地址的指针(这是它所期望的)。在这种情况下,gethostname() 接收一个字符数组,而简单的字符数组在 Lisp 中与 C 语言中本质上表示相同的东西,这就让事情变得更简单了。因此,外部函数定义如下
(def-foreign-call (c-get-hostname "gethostname")
((name (* :char) (simple-array 'character (*)))
(len :int integer))
:returning :int)
现在开始逐行来分析:首先,第一行中定义了个 C-GET-HOSTNAME
的函数,该函数会调用 C 语言的 gethostname()
函数。这个函数有两个参数:第一个叫做 NAME
,即一个字符指针(*char
C语言中的格式),Lisp 中叫做字符数组;第二个参数叫做 LEN
,是个整型。然后该函数返回一个整型。
然后,以 Lisp 的思路来看是这样的:
(defun get-hostname ()
(let* ((name (make-array 256 :element-type 'character))
(result (c-get-hostname name 256)))
(if (zerop result)
(let ((pos (position #\null name)))
(subseq name 0 pos))
(error "gethostname() failed."))))
函数创建了一个 NAME
数组,将 C-GET-HOSTNAME
返回的结果存入其中,然后在对得到的结果检查。如果返回结果为 0,那么调用成功,然后 NAME
中 0 字符(NULL,C 语言中字符串的终止符)之前的内容,否则,将抛出异常。注意,与上个例子不同的是,这次是在 Lisp 中创建字符串,同时,这次会在函数运行结束后,Lisp 的垃圾回收机制会将该字符串回收掉。以下是示例:
* (get-hostname)
"terminus"
一般来说,处理字符要比上个例子容易。假设你想在 Lisp 中调用 getenv() 来访问环境变量的值。getenv()
接受一个字符串参数(变量名)并返回另一个字符串(变量值)。切确地说,这个参数是个指针,指向调用程序需要分配的字符序列,返回值是个指针,指向(环境中)已存在的字符序列。以下是 C-GETENV
的定义:
(def-foreign-call (c-getenv "getenv")
((var (* :char) string))
:returning :int
:strings-convert t)
在这个示例中,参数仍是个指向字符的 C 指针,但在 Lisp 中可以将其定义为 STRING
。返回值也是个指针,因此,可以将其定义为整型。最后, :STRINGS-CONVERT
参数表明 ACL 需要自动将第一个传递进来的 Lisp 字符串转换成 C 字符串。以下是其用法:
* (c-getenv "SHELL")
-1073742215
如果对返回值不解的话,要记住 C-GETENV
返回的是指针,Lisp 需要知道如何处理指针所指向的内存的内容。在这种情况下我们就知道这是指向一个 C 字符串,因此就可以使用 FF:NATIVE-TO-STRING
函数将它转换成 Lisp 字符串:
* (native-to-string (c-getenv "SHELL"))
"/bin/tcsh"
9
9
(其中第二、三个值是拷贝相应的字符和字节的数量)。注意:如果请求一个不存在的变量的值,C-GETENV
将返回 0,同时 NATIVE-TO-STRING
将会失败。因此,更健全的代码示例是:
* (let ((ptr (c-getenv "NOSUCHVAR")))
(unless (zerop ptr)
(native-to-string ptr)))
NIL