4 外部接口(Foreign Interface)

Chez Scheme 提供两种与“外部代码”(即其他语言写的代码)交互的方式。第一种是通过子进程创建和通信,在4.1节进行讨论。第二种是通过静态或者动态加载的方式,在Scheme中调用C编写的过程,或者在C中调用Scheme编写的过程。这些机制在4.2节到4.4节中讨论。

静态加载C目标代码的方法取决于你所运行的机器。详见与Chez Scheme一起发布的安装指示。

4.1 子进程通信(Subprocess Communication)

system和process两个过程用来创建子进程。这两个过程都接受一个string类型参数,创建一个子进程来执行string中的shell命令。system过程在返回前等待进程退出,而process过程立即返回,并不等待进程退出。由system创建的子进程的标准输入和输出文件可以被用于与用户console的通信。由process创建的子进程的标准输入和输出文件可以被用于同scheme进程的通信。

过程: (system command) 返回: 见下文 库: (chezscheme)

command 必须是string类型

system过程创建一个子进程来执行由command中指定的指令。子进程可通过同一console的输入输出文件(由Scheme procss使用)与用户通信。在创建完子进程后,system在其返回前一直等待子进程退出。

当子进程退出时,system返回子进程的退出码,除非signal导致子进程终止(如在类Unix系统中)。在该情况下,system返回引起进程终止的信号的负数,如SIGHUP为-1。

过程: (open-process-ports command) 过程: (open-process-ports command b-mode) 过程: (open-process-ports command b-mode ?transcoder) 返回: 见下文 库: (chezscheme)

command 必须是string类型。如果?transcoder存在且不是#f,且其必须是transcoder,该过程创建了文本端口,每个端口的transcoder为?transcoder。否则,该过程返回二进制端口。b-mode指定该过程返回的每个端口的buffer模式,默认为block。buffer模式在《The Scheme Programming Language, 4th Edition》的7.2节介绍。

open-process-ports创建子进程执行由command指定的指令。与system和process不同处是其在创建子进程后立即返回,即不等待子进程结束。其返回四个值:

  1. to-stdin 是输出端口,Scheme能够使用该端口通过子进程的标准输入文件发送输出内容到子进程。
  2. from-stdout 是输入端口,Scheme能够使用该端口通过子进程的标准输出文件读取子进程的输出内容。
  3. from-stderr 是输入端口,Scheme能够使用该端口通过子进程的标准错误文件读取子进程的输出内容。
  4. process-id 是个整数,由操作系统分配,用于识别创建的子进程。

如果子进程退出或者关闭标准输出文件描述符,任何从from-stdout读取内容的过程将会返回end-of-file对象。类似的,如果子进程退出或者关闭标准错误文件描述符,任何从from-stderr读取内容的过程会返回end-of-file对象。(译者注:原文为process进程,而非子进程)

判断input-port-ready?可被用于检测是否有来自子进程的输出内容到达Scheme。

有时需要立即强制输出内容到子进程可在to-stdin上调用flush-output-port,Chez Scheme由于效率原因缓冲输出。

在Unix系统中,process-id是执行command的shell的进程描述符。如果command用于调用可执行文件而非shell命令,需要在command之前加上"exec",这样shell会直接加载和执行可执行文件,而不会fork一个新进程--尾递归的shell等效。这会减少一个创建的子进程的数量,从而使得process-id反应执行文件的进程描述符,一旦shell完成转移控制。

过程: (process command) 返回: 见解释 库: (chezscheme)

command 必须是string类型。

process与open-process-ports类似,但不那么一般化。它不返回能够读取子进程标准错误输出的端口,而且它总是创建文本端口。它返回三个值的list,而不是像open-process-ports那样四个独立的值。返回的list依次包含:from-stdout, to-stdin, process-id。分别对应open-process-ports返回值得第二,第一,第四个返回值。

4.2 导入Scheme (Calling out of Scheme)

Chez Scheme的foreign-procedure接口允许Scheme程序调用由C或者由遵循C调用规则的语言编写的过程。在外部过程能够被Scheme调用之前需要完成两个步骤。第一,外部过程必须被编译和加载,动态或者静态的加载形式,如在4.6节所述。然后,外部过程的入口必须在Scheme中建立,如本节所述。一旦外部过程的入口建立,就能够像普通Scheme过程那样被调用。

由于外部过程的操作独立于Scheme的内存管理和异常处理系统,在使用它们的时候需要多加小心。尽管,foreign-procedure接口提供类型检查(在小于三级优化下)和类型转换,程序员必须确保Scheme与外部过程之间的共享数据被安全地指定了合适的参数和结果类型。

外部过程的Scheme可调用封装(Scheme-callable wrappers)能够通过ftype-ref和function ftypes创建(见4.5节)。

语法: (foreign-procedure conv ... entry-exp (param-type ...) res-type) 返回: 一个过程 库: (chezscheme)

entry-exp必须是代表有效外部过程入口的string类型,或代表外部过程地址的integer类型。param-types和res-type必须是symbols或如下所述的结构化形式。当foreign-procedure表达式被求值,一个能调用以entry-exp指定的外部过程的Scheme过程被创建。外部过程的调用结果通过转换后被指定为res-type类型。可以对同一个外部入口创建多个Scheme过程。

每个conv用来指定调用规则。conv可以是#f,代表目标机器上的默认调用规则(所以#f没有效果)。在Windows下,另外三个目前支持的调用规则是stdcall, cdecl,和com (只支持32位)。因为cdecl是默认的,指定cdecl与指定#f或者不指定是一样的。最后,conv也可以是collect_safe,用来指定在外部过程运行时,并发允许垃圾回收。

使用stdcall能够使用大多数Windows API过程。对于Windows API变参数过程,C语言库过程,和大多数其他过程,可以使用cdecl。使用com可调用COM接口方法;COM使用stdcall规则,但是额外增加了必须的间接操作来获取COM实例的正确方法。COM实例的地址必须以第一个参数传递,必须被声明为iptr。只有__com接口,entry-exp必须被指定为COM vtable(COM虚表)中的字节偏移。例如,

(foreign-procedure __com 12 (iptr double-float) integer-32)

创建一个在COM实例的vtable中偏移12字节的COM方法的接口,COM实例在第一个参数中传递,第二个参数是double float,返回值类型是integer。

使用collect_safe声明允许在外部函数调用时,并发允许垃圾回收。collect_safe声明允许并发回收。通过在外部过程调用时抑制当前线程(见fork-thread),当外部函数返回时线程再次激活。collect_safe声明是有用的,例如,当调用包含I/O阻塞的调用时,允许其他Scheme线程正常运行。避免传递可回收的内存到collect_safe的外部过程,或者使用lock-object来锁定内存;(参考Sdeactivate_thread)。__collect_safe声明对于非线程版本的系统是没有效果的。

例如,以默认规则调用C sleep函数会阻塞其他Scheme线程执行垃圾回收,但是加入__collect_safe声明后可以避免该问题:

(define c-sleep
  (foreign-procedure __collect_safe "sleep" (unsigned) unsigned))
(c-sleep 10) ; 睡眠10秒钟而不阻塞其他线程

如果以collect_safe方式调用的外部过程能够调用过程,那么被调用的过程也必须是collect_safe方式,这样被调用的过程才能重新激活线程。

对外部过程的参数进行完整的类型检查和转换。在使用scheme-object, string, wstring, u8, u16, u32*, utf-8, utf-16le, utf-16be, utf-32le, and utf-32be这些类型时需要特别小心,因为这些分配出来的Scheme对象允许在Scheme内存管理系统控制不能控制的地方被使用。只要这些对象或者数据结构不在Scheme代码运行时保存在外部变量中,且只要他们不是被传递到__collect_safe过程中,就不会发生问题,因为垃圾回收只有在Scheme代码运行时或者当并发垃圾回收被开启的情况下才执行。其他参数类型被等价转换成外部表示方式,他们能够无期限地被保存在外部变量和数据结构中。

对于string, wstring, utf-8, utf-16le, utf-16be, utf-32le, and utf-32be参数类型,一个参数会被转换成一个新的对象后再传给外部过程。因为新的对象在调用前不能被锁定,它不会被collect_safe外部过程正确处理,所以这些类型不允许作为collect_safe外部过程的参数类型。基于类似的理由,这些类型也不允许作为__collect_safe外部调用的返回值类型。

以下是合法的参数类型:

integer-8: 从-27 到 28 - 1 的整数有效。 27 到 28 - 1 范围内的整数被认为是负数的补充表示,如 #xff 是 -1。参数作为一个合适大小的整数类型传递给C(通常是signed char)。

unsigned-8: 从 -27 到 28 - 1 的整数有效。 -27 到 -1范围内的整数被认为是整数的补充表示,如 -1 是 #xff。参数作为一个合适大小的整数类型传递给C(通常是unsigned char)。

integer-16: 从 -215 到 216 - 1 的整数有效。215 到 216 - 1 范围内的整数被认为是负数的补充表示,如 #xffff 是 -1。参数作为一个合适大小的整数类型传递给C (通常是short)。

unsigned-16: 从 -215 到 216 - 1 的整数有效。-215 到 -1范围内的整数被认为是整数的补充表示,如 -1 是 #xffff。参数作为一个合适大小的整数类型传递给C (通常是 unsigned short)。

integer-32: 从 -231 到 232 - 1 的整数有效。231 到 232 - 1 范围内的整数被认为是负数的补充表示,如 #xffffffff 是 -1。参数作为一个合适大小的整数类型传递给C(通常是int)。

unsigned-32: 从 -231 到 232 - 1 的整数有效。-231 到 -1范围内的整数被认为是整数的补充表示,如 -1 是 #xffffffff. 参数作为一个合适大小的整数类型传递给C(通常是unsigned int)。

integer-64: 从 -263 到 264 - 1 的整数有效。263 到 264 - 1 范围内的整数被认为是负数的补充表示。参数作为一个合适大小的整数类型传递给C(通常是long long,或者在许多64位机器上long)。

unsigned-64: 从 -263 到 264 - 1 的整数有效。-263 到 -1范围内的整数被认为是整数的补充表示。参数作为一个合适大小的整数类型传递给C(通常是unsigned long long,或者在许多64位机器上unsigned long)(译者注:原文为long)。

double-float: 只有Scheme flonums有效--其他Scheme数值类型不会自动转换。参数作为双精度double类型传递给C。

single-float: 只有Scheme flonums有效--其他Scheme数值类型不会自动转换。参数作为单精度float类型传递给C。由于Chez Scheme以double-float类型表示flonums,参数首先会转换到single-float格式。

short: 该类型是以上合适固定大小类型的别名,取决于C中short的大小。

unsigned-short: 该类型是以上合适固定大小类型的别名,取决于C中unsigned short的大小。

int: 该类型是以上合适固定大小类型的别名,取决于C中int的大小。

unsigned: 该类型是以上合适固定大小类型的别名,取决于C中unsigned的大小。

unsigned-int: 该类型是unsinged类型的别名。固定大小类型,取决于C中unsigned的大小。

long: 该类型是以上合适固定大小类型的别名,取决于C中long的大小。

unsigned-long: 该类型是以上合适固定大小类型的别名,取决于C中unsigned long的大小。

long-long: 该类型是以上合适固定大小类型的别名,取决于非标准C中long long的大小。

unsigned-long-long: 该类型是以上合适固定大小类型的别名,取决于非标准C中unsigned long long的大小。

ptrdiff_t: 该类型是以上合适固定大小类型的别名,取决于在宿主机器stddef.h头文件中的定义。

size_t: 该类型是以上合适固定大小无符号类型的别名,取决于在宿主机器stddef.h头文件中的定义。

ssize_t: 该类型是以上合适固定大小有符号类型的别名,取决于在宿主机器stddef.h头文件中的定义。

iptr: 该类型是以上合适固定大小类型的别名,取决于C中指针类型的大小。

uptr: 该类型是以上合适固定大小(无符号)类型的别名,取决于C中指针类型的大小。

void*: 该类型是uptr的别名。

fixnum: 该类型等效于iptr,除了在fixnum范围内的数值有效。传输fixnums比传输iptr值稍微快一点,但是fixnum的范围更小,所以有效iptr值没有fixnum的表示。

boolean: 任何Scheme对象可以被当做boolean传递。#f会被转换为0。所有其他对象被转换为1。参数以int类型传递给C。

char: 只有Scheme字符和在0~255范围的Unicode标量值是有效char参数。字符通过char->integer被转换为Unicode标量值,以unsigned char类型传递给C。

wchar_t: 只有Scheme的字符是有效wchar_t参数。在Windows和其他任何wchar_t只有16bit值而不是所有Unicode标量值的系统中,只有16bit Unicode标量值的字符有效。在wchar_t时32bit值得系统中,任何Scheme字符是有效的。字符通过char->integer被转换成Unicode标量值,以wchar_t类型传递给C。

wchar: 该类型是wchar_t的别名。

double: 该类型是double-float的别名。

float: 该类型是single-float的别名。

scheme-object: 该参数直接传递给外部过程;没有转换,没有类型检查。这种形式的参数传递应该被谨慎使用。Scheme objects不该被保存在外部变量或数据结构中,因为内存管理系统会在外部函数调用的间隙重定位它们。

ptr: 该类型是scheme-object的别名。

u8*: 该参数必须是Scheme bytevector或者#f类型。对于#f,null pointer (0)被传递给外部过程。对于bytevector,指向bytevector数据的第一个字节的指针被传递给外部过程。如果被传递数据的C程序要求输入以'\0'结尾,那么 null (0)字节必须被显示包含在bytevector中。bytevector不应该被保存在外部变量或数据结构中,因为内存管理系统会在外部函数调用间隙重定位或丢去它们,把它们的储存空间另做他用。

u16: 可把该类型的参数与u8类型的参数同样看待。如果被传递数据的C程序要求输入以'\0'结尾,那么两个null (0)字节必须被显示包含在bytevector中,以16bit边界对齐。

u32: 可把该类型的参数与u8类型的参数同样看待。如果被传递数据的C程序要求输入以'\0'结尾,那么四个null (0)字节必须被显示包含在bytevector中,以32bit边界对齐。

utf-8: 该参数必须是Scheme string类型或者#f。对于#f,null pointer (0)被传递给外部过程。string被转换成bytevector,好像通过string-utf8一样,增加了null字节,第一个bytevector第一个字节的地址被传递给C。bytevector不应该被保存在外部变量或数据结构中,因为内存管理系统会在外部函数调用间隙重定位或丢去它们,把它们的储存空间另做他用。__collect_safe指定的外部过程不允许utf-8参数类型。

utf-16le: 可把该类型的参数与utf-8类型的参数同样看待,除了他们好像通过string->utf16转换一样,以endianness little方式,后面追加了两个null字节而不是一个。

utf-16be: 可把该类型的参数与utf-8类型的参数同样看待,除了他们好像通过string->utf16转换一样,以endianness big方式,后面追加了两个null字节而不是一个。

utf-32le: 可把该类型的参数与utf-8类型的参数同样看待,除了他们好像通过string->utf32转换一样,以endianness little方式,后面追加了四个null字节而不是一个。

utf-32be: 可把该类型的参数与utf-8类型的参数同样看待,除了他们好像通过string->utf32转换一样,以endianness big方式,后面追加了四个null字节而不是一个。

string: 该类型是utf-8的别名。

wstring: 该类型是utf-16le,utf-16be,utf-32le,或utf-32be的别名,取决于C中wchar_t的大小和目标机器的endianness。例如,在Intel处理器的Windows系统中wstring等效于utf-16le。

(* ftype): 该类型允许外部类型ftype的指针被传递。参数必须是类型ftype的ftype指针,真实的参数是ftype指针所封装的地址。见4.5节外部类型描述。

(& ftype): 该类型允许外部类型ftype以值得形式传递,但是在Scheme方面是以外部类型数据的指针方式。也就是说, (& ftype) 参数在Scheme方面与 (* ftype) 参数一样,但是 (& ftype) 参数是以外部指针地址中的内容而非地址传递给外部过程。例如,如果ftype是struct类型,那么(& ftype)传递struct参数而不是struct-pointer参数。ftype不能引用array类型。

返回值类型和参数类型情况类似,增加了一个void类型。一般来说,类型转换是参数类型转换的逆过程。在返回时灭有错误检查,应为系统不能决定一个外部结果是否真的是其所指示的类型。在使用过程中需要特别注意scheme-object, double-float, double, single-float, float和会创建bytevectors或strings的类型,因为无效的返回值可能会导致不正确的内存引用和不正确的计算。以下是有效的返回值类型:

void: 外部函数过程的返回值被忽略,未指定的Scheme对象被返回。void 用于只是产生效果的外部过程的调用。

integer-8: 结果被解译为signed 8-bit integer,并转换为Scheme exact integer。

unsigned-8: 结果被解译为unsigned 8-bit integer,并转换为Scheme非负exact integer。

integer-16: 结果被解译为signed 16-bit integer,并转换为Scheme exact integer。

unsigned-16: 结果被解译为unsigned 16-bit integer,并转换为Scheme非负exact integer。

integer-32: 结果被解译为signed 32-bit integer,并转换为Scheme exact integer。

unsigned-32: 结果被解译为unsigned 32-bit integer,并转换为Scheme非负exact integer。

integer-64: 结果被解译为signed 64-bit integer,并转换为Scheme exact integer。

unsigned-64: 结果被解译为unsigned 64-bit integer,并转换为Scheme非负exact integer。

double-float: 结果被解译为双精度浮点型double float,并被转换为Chez Scheme flonum。

single-float: 结果被解译为双精度浮点型single float,并被转换为Chez Scheme flonum。因为Chez Scheme用double-float格式表示flonums,结果首先被转换成double-float格式。

short: 该类型是以上合适固定大小类型的别名,取决于C short的大小。

unsigned-short: 该类型是以上合适固定大小类型的别名,取决于C unsigned short大小。

int: 该类型是以上合适固定大小类型的别名,取决于C int的大小。

unsigned: 该类型是以上合适固定大小类型的别名,取决于C unsigned的大小。

unsigned-int: 该类型是以上合适固定大小类型unsigned的别名,取决于C unsigned的大小。

long: 该类型是以上合适固定大小类型的别名,取决于C long的大小。

unsigned-long: 该类型是以上合适固定大小类型的别名,取决于C unsigned long的大小。

long-long: 该类型是以上合适固定大小类型的别名,取决于非标准C long long的大小。

unsigned-long-long: 该类型是以上合适固定大小类型的别名,取决于非标准C unsigned long long的大小。

ptrdiff_t: 该类型是以上合适固定大小类型的别名,取决于宿主机器stddef.h头文件中的定义。

size_t: 该类型是以上合适固定大小unsigned类型的别名,取决于宿主机器stddef.h头文件中的定义。

ssize_t: 该类型是以上合适固定大小signed类型的别名,取决于宿主机器stddef.h头文件中的定义。

iptr: 该类型是以上合适固定大小类型的别名,取决于C中指针的大小。

uptr: 该类型是以上合适固定大小(unsigned)类型的别名,取决于C中指针的大小。

void*: 该类型是uptr的别名。

boolean: 该类型把C int返回值转换为Scheme boolean。0为#f,其他值为#t。

char: 该类型把C unsigned char类型转换为Scheme character,就像通过integer->char。

wchar_t: 该类型把C wchar_t返回值转化为Scheme character,就像通过integer->char。wchar_t值必须是一个有效的Unicode标量值。

wchar: 该类型是wchar_t的别名。

double: 该类型是double-float的别名。

float: 该类型是single-float的别名。

scheme-object: 结果被假设为一个有效的Scheme object,不进行转换。该类型具有危险性,因为无效的Scheme object会导致内存管理系统不确定的错误结果(通常是不不好的)。由于Scheme object实际上是typed指针,甚至integers也不能安全地被返回为scheme-object,除非他们是由Scheme系统创建的。

ptr: 该类型是scheme-object的别名。

u8*: 结果被解译为一个'\0'结尾的8-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,字节序列被储存在新分配的合适长度的bytevector中,bytevector被返回给Scheme。

u16*: 结果被解译为一个'\0'结尾的16-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,16-bit integers序列被储存在新分配的合适长度的bytevector中,bytevector被返回给Scheme。null 结尾符为16 bit对齐,即两个字节的16-bit对齐边界。

u32*: 结果被解译为一个'\0'结尾的32-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,32-bit integers序列被储存在新分配的合适长度的bytevector中,bytevector被返回给Scheme。null 结尾符为32-bit对齐,即四个字节的32-bit对齐边界。(译者注:原文为the sequence of 16-bit integers)

utf-8: 结果被解译为一个'\0'结尾的8-bit unsigned character序列的指针。如果结果是空指针,返回为#f。否则,字节序列被转换为Scheme string类型,就像通过utf8->string,string返回给Scheme。

utf-16le: 结果被解译为一个'\0'结尾的16-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,字节序列被转换为Scheme string,就像通过utf16->string,以endianness little方式,string返回给Scheme。在integers序列中的字节序标记被认为是普通字符值,不影响字节序。

utf-16be: 结果被解译为一个'\0'结尾的16-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,字节序列被转换为Scheme string,就像通过utf16->string,以endianness big方式,string返回给Scheme。在integers序列中的字节序标记被认为是普通字符值,不影响字节序。

utf-32le: 结果被解译为一个'\0'结尾的32-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,字节序列被转换为Scheme string,就像通过utf32->string,以endianness little方式,string返回给Scheme。在integers序列中的字节序标记被认为是普通字符值,不影响字节序。

utf-32be: 结果被解译为一个'\0'结尾的32-bit unsigned integers序列的指针。如果结果是空指针,返回为#f。否则,字节序列被转换为Scheme string,就像通过utf32->string,以endianness big方式,string返回给Scheme。在integers序列中的字节序标记被认为是普通字符值,不影响字节序。

string: 该类型是utf-8的别名。

wstring: 该类型是utf-16le, utf-16be, utf-32le, utf-32be中合适类型的别名,取决于C中wchar_t的大小和目标机器的endianness。例如,wstring在Intel硬件的Windows系统上等效于utf-16le。

(* ftype): 结果被解译为外部对象的地址,其由ftype描述,新分配的封装该地址的ftype指针被返回。见4.5节对外部类型的描述。

(& ftype): 结果被解译为外部对象,其由ftype描述,外部函数返回一个ftype结果,但是,调用者必须在其他调用参数前提供一个额外的(* ftype)参数来接收结果。外部过程调用后一个未指定的Scheme对象被返回,因为结果被写入由额外参数引用的储存空间中。ftype不能引用array类型。

我们看下C identity过程:

int id(x) int x; { return x; }

当包含该过程的文件被编译加载(见4.6),它能够由以下方式访问:

(foreign-procedure "id"
  (int) int)
=> #<procedure>
((foreign-procedure "id"
   (int) int)
 1)
=> 1
(define int-id
  (foreign-procedure "id"
    (int) int))
(int-id 1)
=> 1

“id”入口能够被解译为接收和返回boolean类型:

(define bool-id
  (foreign-procedure "id"
    (boolean) boolean))
(bool-id #f)
=> #f
(bool-id #t)
=> #t
(bool-id 1)
=> #t

如上例所揭示的,bool-id实际上是转换过程。当Scheme对象以类型boolean传递时,其被转换为0或1,当其返回时被转换为#f或#t。结果被转换成规范化的boolean值。通过变换类型指定,“id”入口能够被用来创建其他转换过程。

(define int->bool
  (foreign-procedure "id"
    (int) boolean))
(int->bool 0)
=> #f
(int->bool 5)
=> #t
(map (foreign-procedure "id"
       (boolean) int)
     '(#t #f))
=> (1 0)

(define void
  (foreign-procedure "id"
    (int) void))
(void 10)
=> unspecified

当然在Scheme中有更加简单、高效的直接完成转换的方式。

当foreign-procedure表达式被求值时外部入口被解析,而不是代码被加载或者每次调用时。因此,以下定义总是有效地,因为foreign-procedure表达式不是立即被求值:

(define doit
  (lambda ()
    ((foreign-procedure "doit" () void))))

在“doit”的入口未被提供前,doit不应该被调用。类似地,“doit”的入口必须在以下代码求值前存在。

(define doit
  (foreign-procedure "doit" () void))

尽管第二种定义更加限制外部文件的加载顺序,但是更加有效率,因为入口解析只需要做一次。

定义一个模板用于创建几个具有类似参数类型和返回值的外部过程,这通常很有用。例如,以下代码从一个外部过程表达式创建两个外部过程,通过抽象出外部过程名字:

(define double->double
  (lambda (proc-name)
    (foreign-procedure proc-name
      (double)
      double)))

(define log10 (double->double "log10"))
(define gamma (double->double "gamma"))

在对应的定义之前,“log10”和“gamma”都必须存在外部入口(见4.6节)。使用外部过程模板能够简化编码过程,减少需要生成大量外部过程所使用的代码量,例如,当整个库的外部过程被导入到Scheme中。

4.3 从Scheme导出(Calling into Scheme)

4.2节描述了foreign-procedure形式,其允许Scheme代码调用C或C兼容外部过程。该节描述foreign-callable形式,其允许C或C兼容代码调用Scheme过程。在C中调用Scheme过程的更原始的机理在4.8节描述。

当在外部调用Scheme过程时,Scheme和调用Scheme的外部代码之间共享数据要小心,防止破坏Scheme的内存管理系统。(译者注:原文为As when calling foreign procedures from Scheme)。

通过传递过程给make-ftype-pointer,并使用合适的函数ftype(见4.5节),能够创建Scheme过程的foreign-callable封装。

语法: (foreign-callable conv ... proc-exp (param-type ...) res-type) 返回: 代码对象 libraries: (chezscheme)

proc-exp必须是过程,是一个被用于外部代码调用的Scheme过程。参数类型和返回值类型如4.2节的foreign-procedure中介绍的,除了要求和转换是相反的,即对于foreign-procedure参数的描述适用于foreign-callable的返回值描述。callable的(& ftype)参数引用了一个只有在callback调用的动态范围中有效的地址。callable的(& ftype)返回值类型需要Scheme过程在所有其他参数前面额外接收一个(& ftype)参数。Scheme过程应该将结果写入这个额外的参数,Scheme过程的直接结果被抛弃。类型检查针对返回值而不是参数类型,因为参数值由外部代码提供,必须假设其是正确的。

每一个conv调整其使用的调用规则。foreign-callable支持与foreign-procedure一样的调用规则,除了com。callable的collect_safe调用规则会将没有激活的调用线程激活,当callable返回时,线程的激活状态将恢复。如果调用线程当前没有在Scheme系统中注册,那么恢复线程激活状态指的是销毁该线程的注册(见Sdestroy_thrread)。

由foreign-callable产生的值是一个Scheme code对象,其包含一些头信息和中包装的Scheme可执行过程。code对象可通过foreign-callable-entry-point转换为foreign-callable的地址,返回一个代表code对象入口地址的整数。(C-callcalbe库函数Sforeign_callable_entry_point,在4.8节介绍,也能够被用来获取入口。)这是一个Scheme对象的隐式指针,在许多情况下,在转换成入口之前必须锁住code对象(使用lock-object)以防止Scheme内存管理系统重新定位或者销毁code对象,即当入口被注册为callback,可以在C中保持永久使用。

以下代码创建得了foreign-callable code对象,锁住code对象,返回入口。

(let ([x (foreign-callable
           (lambda (x y) (pretty-print (cons x (* y 2))))
           (string integer-32)
           void)])
  (lock-object x)
  (foreign-callable-entry-point x))

由foreign-callable返回的code对象的指针可以再不需要的时候解锁,除非入口需要永久保留。

foreign-callable和foreign-procedure的混合使用会导致外部代码和Scheme调用的嵌套,当涉及到延续(continuations)时会产生有意思的思考,无论这是直接还是间接的(由于通过默认异常处理)。见4.4节中关于外部调用和延续(continuations)的交互。

以下例子展示了在Scheme中使用foreign-callable定义许多窗口系统需要的回调函数。假设以下C代码已经被编译和加载(见4.6节)。

#include <stdio.h>

typedef void (*CB)(char);

CB callbacks[256];

void cb_init(void) {
   int i;

   for (i = 0; i < 256; i += 1)
       callbacks[i] = (CB)0;
}

void register_callback(char c, CB cb) {
    callbacks[c] = cb;
}

void event_loop(void) {
    CB f; char c;

    for (;;) {
        c = getchar();
        if (c == EOF) break;
        f = callbacks[c];
        if (f != (CB)0) f(c);
    }
}

这些函数的接口可以在Scheme中如下定义。

(define cb-init
  (foreign-procedure "cb_init" () void))
(define register-callback
  (foreign-procedure "register_callback" (char void*) void))
(define event-loop
  (foreign-procedure __collect_safe "event_loop" () void))

选择字符的回调函数能在后面定义。

(define callback
  (lambda (p)
    (let ([code (foreign-callable __collect_safe p (char) void)])
      (lock-object code)
      (foreign-callable-entry-point code))))
(define ouch
  (callback
    (lambda (c)
      (printf "Ouch! Hit by '~c'~%" c))))
(define rats
  (callback
    (lambda (c)
      (printf "Rats! Received '~c'~%" c))))

(cb-init)
(register-callback #\a ouch)
(register-callback #\c rats)
(register-callback #\e ouch)

这样就建立了如下的交互。

> (event-loop)
a
Ouch! Hit by 'a'
b
c
Rats! Received 'c'
d
e
Ouch! Hit by 'e'

该例子中的__collect_saft声明确保当event-loop等待输入阻塞时其他线程能够继续工作。一个更好的该例子的版本应该保存每个由foreign-callable返回的code对象,当回调函数不再注册时对它们解除锁定。

过程: (foreign-callable-entry-point code) 返回: code对象中foreign-callable的地址 库: (chezscheme)

code是foreign-callable产生的code对象。

过程: (foreign-callable-code-object address) 返回: foreign-callable的入口地址对应的code对象 库: (chezscheme)

地址必须是exact integer且必须是由foreign-callable产生的code对象的入口的地址。

4.4 延续和外部调用(Continuations and Foreign Calls)

foreign-callable和foreign-procedure 允许任意地嵌套外部和Scheme调用。因为其他语言不完全支持Scheme的通用头等延续(general first-class continuations),延续(continuations)间的交互和Scheme与外部过程的嵌套调用是易发生问题的。Chez Scheme使用通用的陷入方式(trapping)尝试返回到旧外部上下文(stale foreign context)来处理交互,而不是直接限制延续(continuations)的使用。外部上下文是一个外部帧(foreign frame)和来自外部函数特定调用的返回点,例如,从C进入Scheme。在一个平常的返回到上下文或者返回到控制栈上的在其下面的其他外部上下文后,外部上下文将变旧。

这样处理的结果是,Scheme延续逻辑上能被用来释放控制权,无论是向上或者向下,通过任意混合Scheme和外部帧的方式。更进一步讲,直到返回外部上下文被真正执行,所有的返回点都是有效地。特别的,这表示使用延续的程序,除了非局部退出的情况,绝不会尝试返回到一个旧外部上下文。(非局部退出本身并没有问题,是由C函数库longjmp及其等效方式实现的)。使用延续的程序只要它们绝不真正返回到一个旧外部上下文一般都能够正常地工作,即使通过延续的调用,在逻辑上控制越过了旧上下文。

一种该机制的结果会是:当Scheme端使用了延续时,非局部退出情况下,C栈指针不会自动恢复其基值(base value)。如果程序在非局部退出后继续运行的话,后面建立的C栈会被增加到已经存在的栈上,可能会导致C栈溢出。为了避免该情况的发生,程序可以在获取延续之前安排建立一个单独的C调用帧,在非局部退出后返回到该C帧。下面的with-exit_-proc过程就是安排做这个,并且不涉及到C代码。

(define with-exit-proc
  (lambda (p)
    (define th (lambda () (call/cc p)))
    (define-ftype ->ptr (function () ptr))
    (let ([fptr (make-ftype-pointer ->ptr th)])
      (let ([v ((ftype-ref ->ptr () fptr))])
        (unlock-object
          (foreign-callable-code-object
            (ftype-pointer-address fptr)))
        v))))

除了当延续被调用时会重置C的栈,with-exit-proc类似call/cc。为了实现这个,其创建了th的一个ftype-pointer来表示foreign-callable入口,并创建了一个Scheme可调用过程的入口。这为th建立了一个涉及C调用的封装。当该封装返回时,无论是通过显示的延续调用传递p,还是一般地从p返回,C栈都会被重置为原始值。

4.5 外部数据(Foreign Data)

本节介绍的过程直接创建和操作外部数据,即存在于Scheme堆外的数据。由于foreign-alloc和foreign-sizeof的异常,就它们不(不能)检查传给它们的地址的有效性而言,这些过程本质上是不安全的。不恰当的使用这些过程会导致无效的内存引用,毁坏数据,或系统崩溃。

该节也介绍一个更高阶的操作外部数据的语法机制,包含外部结果体,联合体,数组,位域。语法接口比过程接口更加安全,但是仍必须假设对给出的被操作的对象类型的地址是正确的。

过程: (foreign-alloc n) 返回: 新分配的外部数据块的地址,具有n字节长度 库: (chezscheme)

n必须是一个正的fixnum。返回值是一个exact integer,确保按底层硬件的要求对任意类型合理对齐。如果外部数据块不能被分配,会抛出一个&assertion条件类型异常。

过程: (foreign-free address) 返回: 未指定 libraries: (chezscheme)

该过程释放地址点处的储存块。地址必须是一个在-2w-1到2w - 1范围的exact integer,其中w是指针的字节宽度,即对于64位机器是64。它必须是由之前foreign-alloc返回的地址,而不是传给foreign-free后的。

过程: (foreign-ref type address offset) 返回: 见下文 库: (chezscheme)

foreign-ref 提取type类型的值,该值的地址在外部数据地址address偏移offset字节处。

type必须是个识别要提取的值类型的符号。下面的类型具有依赖于机器的大小和类似于C中的类型名称:

  • short,
  • unsigned-short,
  • int,
  • unsigned,
  • unsigned-int,
  • long,
  • unsigned-long,
  • long-long,
  • unsigned-long-long,
  • ptrdiff_t,
  • size_t,
  • ssize_t,
  • char,
  • wchar_t,
  • float,
  • double,
  • void*

long-long和unsigned-long-long类型对应于C类型中的long long和unsigned long long。char类型的值以单字节引用,并转换为Scheme character(类似通过integer->char)。wchar_t类型的值被转换为Scheme character(类似通过integer->char)。其值必须是一个有效地Unicode标量值。

wchar是wchar_t的别名。

另外一些依赖机器的类型被记为:

  • iptr,
  • uptr,
  • fixnum, and
  • boolean.

uptr等效于void*;两者都被当作unsigned integers,即指针的大小。itpr被当作signed integer,即指针的大小。fixnum被当作一个iptr,但是值范围被限定在fixnum的值范围。boolean被当作一个int,0被转换为Scheme中的#f,其他值被转换为#t。

最后是一些支持的固定大小类型:

  • integer-8,
  • unsigned-8,
  • integer-16,
  • unsigned-16,
  • integer-32,
  • unsigned-32,
  • integer-64,
  • unsigned-64,
  • single-float, and
  • double-float.

地址必须是一个在-2w-1到2w - 1范围的exact integer,其中w是指针的字节宽度,即对于64位机器是64。offset必须是一个exact fixnum。address与offset之和能够寻址一个足够容纳type类型的值大小的可读内存块,在一个由foreign-alloc返回的存储块中,且并没有被foreign-free释放,或者在由其他机制获取的一个存储块中,例如,外部调用。对于多字节的值,假设使用机器本身的endianness。

过程: (foreign-set! type address offset value) 返回: 见下文 库: (chezscheme)

foreign-set! 存储了一个表示value的type类型的值,其通过外部数据块地址address偏移offset字节寻址。

type必须是一个识别储存值类型的符号,为列在上面foreign-ref描述中的其中一个类型。Scheme characters被转换为char 或者 wchar_t,类似通过char->integer。对于boolean类型,Scheme #f转换为0,任何其他Scheme对象转换为1。

地址必须是一个在-2w-1到2w - 1范围的exact integer,其中w是指针的字节宽度,即对于64位机器是64。offset必须是一个exact fixnum。address与offset之和能够寻址一个足够容纳type类型的值大小的可读内存块,在一个由foreign-alloc返回的存储块中,且并没有被foreign-free释放,或者在由其他机制获取的一个存储块中,例如,外部调用。对于多字节的值,假设使用机器本身的endianness。

过程: (foreign-sizeof type) 返回: type的字节大小 库: (chezscheme)

type 为列在上面foreign-ref描述中的其中一个类型。

语法: (define-ftype ftype-name ftype) 语法: (define-ftype (ftype-name ftype) ...) 返回: 未指定 库: (chezscheme)

define-ftype 形式是一个定义,能出现于任何其他定义能够出现的地方。它建立了一个或多个foreign-type(ftype)的绑定,从标识ftype-name或者标识ftype-name ...到外部类型ftype或ftype ...的绑定。每个ftype-name能被用于获取声明类型的外部对象(译者注:原文为declared shape,是否是declared type?),每个能被用于构成其他ftype。

一个ftype必须是以下形式中的一种:

  • ftype-name
  • (struct (field-name ftype) ...)
  • (union (field-name ftype) ...)
  • (array length ftype)
  • (* ftype)
  • (bits (field-name signedness bits) ...)
  • (function conv ... (ftype ...) ftype)
  • (packed ftype)
  • (unpacked ftype)
  • (endian endianness ftype)

其中length是一个非负exact integer,bits是一个正的exact integer,field-name是一个标识符,conv是#f或如4.2所述的有效调用规则名字的string,signedness为signed或者unsigned,endianness是native, big, little中的其中一个。

以上并没有反应出一个限制,即function ftypes不能被用于field names 或 array 中值的类型。就是说,function types只在一个ftype的最顶层有效,如:

(define-ftype bvcopy_t (function (u8* u8* size_t) void))

或者直接作为指针ftype(*)类型的子类型,如以下定义,其等效于上面的bvcopy_t的定义。

(define-ftype A
  (struct
    [x int]
    [f (* (function (u8* u8* size_t) void))]))

(define-ftype A
  (struct
    [x int]
    [f (* bvcopy_t)]))

也就是说,function不能直接嵌入struct,union,array中,但是function的指针能够被嵌入。

如下定义建立了F,A,E的ftype绑定。

(define-ftype F (function (wchar_t int) int))

(define-ftype A (array 10 wchar_t))

(define-ftype E
  (struct
    [a int]
    [b double]
    [c (array 25
         (struct
           [a short]
           [_ long]
           [b A]))]
    [d (endian big
         (union
           [v1 unsigned-32]
           [v2 (bits
                 [hi unsigned 12]
                 [lo unsigned 20])]))]
    [e (* A)]
    [f (* F)]))

ftype F 描述了接收两个参数的外部函数的类型,一个宽字符和一个integer,返回一个integer。ftype A是一个简单的含有10个wchar_t的数组,其大小为10倍的单个wchar_t。ftype E是一个有五个域的结构体,一个integer类型a,一个double类型b,一个数组c,一个联合d,一个指针e。数组c是包含25个结构体的数组,每个结构体包含一个short integer类型,一个long integer类型,一个A类型数组。c数组的大小是25倍的A类型数组,加上25倍的能够存储一个short和一个long integer类型的空间。联合d可以是一个32bit的unsigned integer类型或者一个32bit unsigned integer分为高位(12bits)和低位(20btis)分量。联合的域是重合的,以至于往一个写入会有效地影响到另一个。因此,可以使用d联合类型分开一个unsigned integer,通过往v1中写入,而从hi和lo读取。e是一个指向A数组的指针,它自己不是数组,它的大小只是单个指针的大小。类似的,f指向一个函数,它的大小也是单个指针的大小。

一个下划线(_)可用于表示struct, union, bits ftype的一个或多个域名字。在内存布局中包含了这些域,但是被认为未命名,不能通过以上描述的ftype的操作获取。因此,上面的例子,c数组中的long域是不能被获取的。

不是下划线的名字被符号化处理,即他们被认为是符号而不是标识符。相对于同一个struct,union,bits ftype中的其他域名字,每个符号必须是唯一的(作为一个符号),但是在同一个ftype中对于不同的struct,union,bits ftype中的域名字不需要是唯一的。

一个ftype中的每个ftype-name必须满足(a)之前已经被define-ftype定义过了,(b)被当前的define-ftype定义,或者(c)是一个基类名,即被foreign-ref和foreign-set!所支持的类名中的一个。在情况(b)中,一个ftype中的一个之前绑定的ftype-name的任何引用是被允许的,但是当前的ftype-name的引用或者之后的绑定,只能以指针域的形式出现。

例如:

(define-ftype
  [Qlist (struct
           [head int]
           [tail (* Qlist)])])

对于Qlist的引用是被允许的,因为它是以指针域的形式出现的。类似的:

(define-ftype
  [Qfrob (struct
           [head int]
           [tail (* Qsnark)])]
  [Qsnark (struct
            [head int]
            [xtra Qfrob]
            [tail (* Qfrob)])])

Qsnark和Qfrob的相互递归引用也是允许的。但是,在下面的例子中:

(define-ftype
  [Qfrob (struct
           [head int]
           [xtra Qfrob]
           [tail (* Qsnark)])]
  [Qsnark (struct
            [head int]
            [tail (* Qfrob)])])

Qfrob ftype中的Qfrob引用是无效的,再如:

(define-ftype
  [Qfrob (struct
           [head int]
           [xtra Qsnark]
           [tail (* Qsnark)])]
  [Qsnark (struct
            [head int]
            [tail (* Qfrob)])])

类似地,Qsnark的引用也是无效。

默认情况下,为了维持多字节标量合适的对齐,需要在合适的地方插入填充字节,使得与目标机器的C结构体布局规则相对应,这些布局已被充分文档化。对于packed ftypes(在packed形式中封装的ftypes,且没有在最近的unpacked形式中被包含),填充字节不插入。(译者注:原文ftypes wrapped in a packed form with no closer enclosing upacked form)

内存中存储的多字节标量值使用目标机器本地"endianness",比如,在基于X86或者X86_64的平台上是little的,在基于Sparc的平台上是big的。Big-endian和little-endian表示能够通过endian ftype指定big 或者 little endianness指示符而强制改变。native指示符被用来强制返回本地表示。每个endian形式在语法上只影响嵌套在其中的ftypes且没有嵌套在最近的endian形式中。(译者注:原文not nested within a closer endian form)

bits形式的ftype中的各个域的总大小n必须满足8,16,24,32,40,48,56 或 64。如果需要字节填充必须人为添加。在little-endian表示下,第一个域占用低位bits,8,16,24,32,40,48,56,或64bit字,接下来的域在该域之上。在big-endian表示时,第一个域占用高位bits,接下来的域在该域之下。

只有当两个ftypes绑定相同的ftype时,它们被认为是等价的。如果两个ftype的定义看起来是一样的,但是出现在同一个程序的两个部分,那这两个ftypes被认为是不同的,尝试通过下面介绍的操作符,使用一个ftype的名字来访问另外一个ftype会导致失败,并产生运行时异常。

数组Array的大小必须是常数。如果只有到运行时才能确定一个数组的长度,那么数组需要被放在ftype的末尾(和任何包含的ftype),然后将其声明为0大小,如下例所示。

(define-ftype Vec
  (struct
    [len int]
    [data (array 0 double)]))
(define make-Vec
  (lambda (n)
    (let ([fptr (make-ftype-pointer Vec
                  (foreign-alloc
                    (+ (ftype-sizeof Vec)
                       (* (ftype-sizeof double) n))))])
      (ftype-set! Vec (len) fptr n)
      fptr)))
(define x (make-Vec 100))
(/ (- (ftype-pointer-address (ftype-&ref Vec (data 10) x))
      (ftype-pointer-address x)
      (ftype-sizeof int))
   (ftype-sizeof double)) =>  10
(foreign-free (ftype-pointer-address x))

(译者注:在笔者64位电脑中结果为21/2,由于内存对齐会填充额外字节的原因吧,因为(ftype-sizeof Vec)为8字节)

对于0长度数组不执行数组边界检查。在一个外部对象中只能出现一个变长度数组,但是可以通过将该对象认为是多个独立的对象来绕过该问题。(译者注:原文but one can work around this by treating the object as multiple individual objects)

为了避免在多个位置处指定一个数组的常数长度,可以使用一个绑定大小的变量和ftype名字的宏。例如:

(define-syntax define-array
  (syntax-rules ()
    [(_ array-name type size-name size)
     (begin
       (define size-name size)
       (define-ftype array-name
         (array size type)))]))
(define-array A int A-size 100)
A-size => 100
(ftype-pointer-ftype
  (make-ftype-pointer A
    (foreign-alloc (ftype-sizeof A)))) => (array 100 int)

该技术能被用于定义任何含有任意大小的数组域的ftypes。

一个struct ftype是struct的第一个域类型的隐式子类型。类似的,一个array ftype是它的元素类型的隐式子类型。因此,struct或者array以更多的域或者元素来扩展第一个域或者元素的类型。这允许struct或者array的实例被当作第一个域或者元素类型的实例,不需要使用ftype-&ref来分配一个指向域或元素的新指针。

语法: (ftype-sizeof ftype-name) 返回: ftype-name标识的ftype类型的字节大小 库: (chezscheme)

大小包含任何在标识ftype中直接嵌入的任何ftype的大小,但不包含由pointer ftype间接嵌入的大小。在后面的情况中,pointer的大小是被包括的。

ftype-name 必须不是一个function ftype,因为函数的大小一般不能被确定。

(define-ftype B
  (struct
    [b1 integer-32]
    [b2 (array 10 integer-32)]))
(ftype-sizeof B) => 44

(define-ftype C (* B))
(ftype-sizeof C) => 4  ; on 32-bit machines
(ftype-sizeof C) => 8  ; on 64-bit machines

(define-ftype BB
  (struct
    [bb1 B]
    [bb2 (* B)]))
(- (ftype-sizeof BB) (ftype-sizeof void* )) => 44

(译者注:(- (ftype-sizeof BB) (ftype-sizeof void* )) => 48,由于字节对齐的原因)

语法: (make-ftype-pointer ftype-name expr) 返回: 一个ftype-pointer对象 库: (chezscheme)

如果ftype-name不是function ftype,expr必须是exact integer表示的地址,其值在目标机器合适的范围。

该过程返回的ftype-pointer对象封装了该地址,以ftype-name为其表示的类型名,通过使用下面描述的过程使得各种形式的检查能够进行。

(make-ftype-pointer E #x80000000) => #<ftype-pointer #x80000000>

地址通常不会是如上所示的常量。相反,可能会是来自foreign-alloc调用的返回值,如:

(make-ftype-pointer E (foreign-alloc (ftype-sizeof E)))

也可能来自Scheme外部的来源,比如来自Scheme通过foreign-procedure接口调用的一个C过程。

如果ftype-name为函数类型,expr必须是个地址,procedure或者string。如果是地址,那么该调用与其他任何使用地址作为参数的make-ftype-pointer调用一样。

如果是procedure,一个针对该过程的foreign-callable code对象被创建,类似于通过foreign-callable(4.3节)。地址被封装在返回的ftype-pointer对象中,是该过程的入口地址。

(define fact
  (lambda (n)
    (if (= n 0) 1 (fact (- n 1)))))
(define-ftype fact_t (function (int) int))
(define fact-fptr (make-ftype-pointer fact_t fact))

返回的ftype指针能被传递给C过程,如果参数被声明为指向相同ftype的指针,C过程能像调用其他函数指针一样调用其收到的该函数指针。因此,ftype-name为function ftype的make-ftype-pointer是一种创建Scheme过程的C可调用封装foreign-callable的另外一种方式。

因为所有的Scheme对象,包括code对象,能够被重新定位甚至被垃圾回收器回收,所以在嵌入ftype指针前foreign-callable code对象被自动锁定,就像通过lock-object。code对象应该在C中使用后被解锁定。因为锁定对象占用空间,导致内存碎片,增加回收代价。由于系统自动确定何时是最后一次C中的使用,程序必须显示解锁code对象,通过从ftype-pointer中提取地址,将地址转换回code对象,传入unlock-object:

(unlock-object
  (foreign-callable-code-object
    (ftype-pointer-address fact-fptr)))

一旦解锁,ftype pointer不能再被使用,除非再次被锁定,例如通过:

(lock-object
  (foreign-callable-code-object
    (ftype-pointer-address fact-fptr)))

程序能通过lock-object?判断来确定一个对象是否已经被锁定。

function ftype 也能在make-ftype-pointer中被用于创建一个C函数的ftype-pointer,无论是提供C函数的地址或者它的名字,以string表示。例如使用如下bvcopy_t的定义:

(define-ftype bvcopy_t (function (u8* u8* size_t) void))

下面两种定义方式是等效的。

(define bvcopy-fptr (make-ftype-pointer bvcopy_t "memcpy"))
(define bvcopy-fptr (make-ftype-pointer bvcopy_t (foreign-entry "memcpy")))

包含memcpy的库必须首先通过load-shared-object加载,或者memcpy必须通过4.6节中描述的其中一种方法注册。

语法: (ftype-pointer? obj) 返回: 如果obj是ftype pointer返回#t,否则返回#f 语法: (ftype-pointer? ftype-name obj) 返回: 如果obj是ftype-name类型则返回#t,否则返回#f 库: (chezscheme)

(define-ftype Widget1 (struct [x int] [y int]))
(define-ftype Widget2 (struct [w Widget1] [b boolean]))

(define x1 (make-ftype-pointer Widget1 #x80000000))
(define x2 (make-ftype-pointer Widget2 #x80000000))

(ftype-pointer? x1) => #t
(ftype-pointer? x2) => #t

(ftype-pointer? Widget1 x1) => #t
(ftype-pointer? Widget1 x2) => #t

(ftype-pointer? Widget2 x1) => #f
(ftype-pointer? Widget2 x2) => #t

(ftype-pointer? #x80000000) => #f
(ftype-pointer? Widget1 #x80000000) => #f

过程: (ftype-pointer-address fptr) 返回: 返回fptr中封装的地中 库: (chezscheme)

fptr必须是一个ftype-pointer对象。

(define x (make-ftype-pointer E #x80000000))
(ftype-pointer-address x) => #x80000000 (译者注:一般输出为十进制数,这里可能是为了表述更清楚)

语法: (ftype-pointer=? fptr1 fptr2) 返回: 如果ftpr1和fptr2有相同的地址返回#t,否则返回#f 库: (chezscheme)

fptr1和fptr2必须是ftype-pointer对象。

ftype-pointer=? 可能被定义为:

(define ftype-pointer=?
  (lambda (fptr1 fptr2)
    (= (ftype-pointer-address fptr1) (ftype-pointer-address fptr2))))

保证不要为地址分配bignums类型,即使地址不能在fixnum范围内表示。

语法: (ftype-pointer-null? fptr) 返回: 如果fptr的地址为0返回#t,否则返回#f 库: (chezscheme)

fptr必须是一个ftype-pointer对象。

ftype-pointer-null?可能被定义为:

(define ftype-pointer-null?
  (lambda (fptr)
    (= (ftype-pointer-address fptr) 0)))

保证不要为地址分配bignums类型,即使地址不能在fixnum范围内表示。

语法: (ftype-&ref ftype-name (a ...) fptr-expr) 语法: (ftype-&ref ftype-name (a ...) fptr-expr index) 返回: 一个ftype-pointer对象 库: (chezscheme)

由ftype-&ref返回的ftype-pointer对象封装外部对象中直接或间接嵌入对象的地址,由fptr-expr的值所指向,如果index存在,index为偏移。fptr-expr的值必须是ftype-name类型的一个ftype指针(fptr),index必须是 或者是fixnum,可能是负的。index值会被自动乘以ftype-name类型的ftype大小,允许fptr被认为是ftype-name对象的数组,index作为该数组的索引。index为 或者0 与没有指定index是相同的。

访问器序列a...必须指定通过标识符ftype的有效路径。对于struct,union,bits ftypes,访问器必须是有效的ftype域名,对于pointer和array ftypes,访问器必须是 * 或者fixnum索引。对于array ftypes,索引必须是非负的,对于0长度的array ftypes,索引必须小于其长度。

以下例子中假设B和BB的定义为上面ftype-sizeof描述中的定义。固定地址只起到说明的作用,其被假设为有效的,尽管地址是在运行时通过foreign-alloc或者其他的机制定义的。

(define x (make-ftype-pointer B #x80000000))
(ftype-&ref B () x)                   => #<ftype-pointer #x80000000>
(let ([idx 1])                        => #<ftype-pointer #x8000002C>
  (ftype-&ref B () x idx))
(let ([idx -1])                       => #<ftype-pointer #x7FFFFFD4>
  (ftype-&ref B () x idx))
(ftype-&ref B (b1) x)                 => #<ftype-pointer #x80000000>
(ftype-&ref B (b2) x)                 => #<ftype-pointer #x80000004>
(ftype-&ref B (b2 5) x)               => #<ftype-pointer #x80000018>
(let ([n 5]) (ftype-&ref B (b2 n) x)) => #<ftype-pointer #x80000018>

(ftype-&ref B (b1 b2) x) => syntax error
(ftype-&ref B (b2 15) x) => run-time exception

(define y (make-ftype-pointer BB #x90000000))
(ftype-set! BB (bb2) y x)
(ftype-&ref BB (bb1 b2) y)           => #<ftype-pointer #x90000004>
(ftype-&ref BB (bb2 * b2) y)         => #<ftype-pointer #x80000004>
(let ([idx 1])                       => #<ftype-pointer #x80000030>
  (ftype-&ref BB (bb2 idx b2) y))

没有访问器或者index,那么返回的ftype-pointer可能与输入相等,如在上面第一个使用ftype-&ref的例子中。否则,ftype-pointer是被新分配出来的。

语法: (ftype-set! ftype-name (a ...) fptr-expr val-expr) 语法: (ftype-set! ftype-name (a ...) fptr-expr index val-expr) 返回: 未指定 语法: (ftype-ref ftype-name (a ...) fptr-expr) 语法: (ftype-ref ftype-name (a ...) fptr-expr index) 返回: an ftype-pointer object 库: (chezscheme)

这些形式用于向fptr-expr的值所指的对象中存储数据或者从该对象中获取数据,如果存在index,index为偏移量。fptr-expr的值必须是一个ftype-name类型的ftype指针(fptr),index必须是 或者fixnum,可能为负。index被自动乘以ftype-name类型的ftype的大小,允许把fptr当作ftype-name对象的数组,index是数组的索引。 或0当作index时与没有index是相同的。

访问器序列a...必须指定通过标识符ftype的有效路径。对于struct,union,bits ftypes,访问器必须是有效的ftype域名,对于pointer和array ftypes,访问器必须是 * 或者fixnum索引。对于array ftypes,索引必须是非负的,对于0长度的array ftypes,索引必须小于其长度。由访问器序列指定的域或者元素必须是标量域,例如,指针域或者包含基本类的域,如int, char, double。

对于ftype-set!,val-expr必须是对于指定域的合适类型的值,即一个合适类型的ftype pointer或一个合适的基础类型值。

对于signed和unsigned integer域,可接受-2w-1到2w-1范围的值,其中w是integer域的位宽。对于signed integer域,对于2w-1到2w-1范围的值被认为是负数的互补表示。对于unsigned integer域,同样地,对于-2w-1到-1范围的值被认为是正数的互补表示。

char和wchar_t(wchar)域的值由Scheme字符转换得到(ftype-set!)或者被转换成Scheme字符(ftype-ref),就像使用char->integer或integer->char。通过ftype-set!被保存到char域的字符必须有在0 ~ 255范围的Unicode标量值。在Windows和任何wchar_t(wchar)表示为16bit值的其他系统中,通过ftype-set!被保存到wchar_t(wchar)域的字符必须是0 ~ 216-1范围内的Unicode标量值。在那些wchar_t表示32bit值得系统上,任何字符能够被保存到wchar_t(wchar)域。

如下示例中假设 B 和 C 已经在上面的ftype-sizeof描述中定义。

(define b
  (make-ftype-pointer B
    (foreign-alloc
      (* (ftype-sizeof B) 3))))
(define c
  (make-ftype-pointer C
    (foreign-alloc (ftype-sizeof C))))

(ftype-set! B (b1) b 5)
(ftype-set! B (b1) b 1 6)
(ftype-set! B (b1) c 5)     => exception: ftype mismatch
(ftype-set! B (b2) b 0)     => exception: not a scalar
(ftype-set! B (b2 -1) b 0)  => exception: invalid index
(ftype-set! B (b2 0) b 50)
(ftype-set! B (b2 4) b 55)
(ftype-set! B (b2 10) b 55) => exception: invalid index

(ftype-set! C () c (ftype-&ref B () b 1))

(= (ftype-pointer-address (ftype-ref C () c))      => #t
   (+ (ftype-pointer-address b) (ftype-sizeof B)))
(= (ftype-pointer-address (ftype-&ref C (*) c))    => #t
   (+ (ftype-pointer-address b) (ftype-sizeof B)))
(= (ftype-pointer-address (ftype-&ref C (-1) c))   => #t
   (ftype-pointer-address b))

(ftype-ref C (-1 b1) c)                     => 5
(ftype-ref C (* b1) c)                      => 6
(ftype-ref C (-1 b2 0) c)                   => 50
(let ([i 4]) (ftype-ref C (-1 b2 i) c))     => 55

(ftype-set! C (-1 b2 0) c 75)
(ftype-ref B (b2 0) b)                      => 75
(foreign-free (ftype-pointer-address c))
(foreign-free (ftype-pointer-address b))

通过ftype-ref,function ftype指针能够被转换为Scheme可调用的过程。假设一个定义了memcpy的库通过load-shared-object被加载或者memcpy通过4.6节中描述的一种方法被注册,Scheme能调用的memcpy可被如下方式定义。

(define-ftype bvcopy_t (function (u8* u8* size_t) void))
(define bvcopy-fptr (make-ftype-pointer bvcopy_t "memcpy"))
(define bvcopy (ftype-ref bvcopy_t () bvcopy-fptr))

(define bv1 (make-bytevector 8 0))
(define bv2 (make-bytevector 8 57))
bv1 => #vu8(0 0 0 0 0 0 0 0)
bv2 => #vu8(57 57 57 57 57 57 57 57)
(bvcopy bv1 bv2 5)
bv1 => #vu8(57 57 57 57 57 0 0 0)

一个ftype pointer也可以作为C函数的返回值,该C函数被声明为返回一个指向function ftype的指针。

因此,ftype-ref用在一个function ftype上,是一种foreign-procedure(4.2节)的替代方式,用于创建C函数的Scheme可调用封装。

过程: (ftype-pointer-ftype fptr) 返回: fptr的ftype, 表示为一个s-expression libraries: (chezscheme)

fptr必须是一个ftype-pointer对象。

(define-ftype Q0
  (struct
    [x int]
    [y int]))
(define-ftype Q1
  (struct
    [x double]
    [y char]
    [z (endian big
         (bits
           [_ unsigned 3]
           [a unsigned 9]
           [b unsigned 4]))]
    [w (* Q0)]))
(define q1 (make-ftype-pointer Q1 0))
(ftype-pointer-ftype q1) => (struct
                            [x double]
                            [y char]
                            [z (endian big
                                 (bits
                                   [_ unsigned 3]
                                   [a unsigned 9]
                                   [b unsigned 4]))]
                            [w (* Q0)])

过程: (ftype-pointer->sexpr fptr) 返回: fptr所指向的对象的s-expression表示 库: (chezscheme)

fptr必须是一个ftype-pointer对象。

对于每个未命名域,如其域名为下划线的域,返回的s-expression中其对应域值也是下划线。类似的,如果域是不可访问的,例如其地址是无效的,那么值是一个无效的符号。

(define-ftype Frob
  (struct
    [p boolean]
    [q char]))
(define-ftype Snurk
  (struct
    [a Frob]
    [b (* Frob)]
    [c (* Frob)]
    [d (bits
         [_ unsigned 15]
         [dx signed 17])]
    [e (array 5 double)]))
(define x
  (make-ftype-pointer Snurk
    (foreign-alloc (ftype-sizeof Snurk))))
(ftype-set! Snurk (b) x
  (make-ftype-pointer Frob
    (foreign-alloc (ftype-sizeof Frob))))
(ftype-set! Snurk (c) x
  (make-ftype-pointer Frob 0))
(ftype-set! Snurk (a p) x #t)
(ftype-set! Snurk (a q) x #\A)
(ftype-set! Snurk (b * p) x #f)
(ftype-set! Snurk (b * q) x #\B)
(ftype-set! Snurk (d dx) x -2500)
(do ([i 0 (fx+ i 1)])
    ((fx= i 5))
  (ftype-set! Snurk (e i) x (+ (* i 5.0) 3.0)))
(ftype-pointer->sexpr x) => (struct
                            [a (struct [p #t] [q #\A])]
                            [b (* (struct [p #f] [q #\B]))]
                            [c (* (struct [p invalid] [q invalid]))]
                            [d (bits [_ _] [dx -2500])]
                            [e (array 5 3.0 8.0 13.0 18.0 23.0)])

4.6 提供外部过程访问 (Providing Access to Foreign Procedures)

可以通过以下提供的几种方式访问外部过程:

  • 可使用 load-shared-object 从共享对象"shared objects"加载外部过程。
  • 一个新的 Chez Scheme 映像编译时可链接额外的外部代码。(向安装Chez Scheme的人咨询详细信息)通常这些入口通过Sforeign_symbol或Sregister_symbol注册,参见4.8节。
  • 其他入口可动态加载或以其他方式由外部代码获得。通常也使用Sforeign_symbol Sregister_symbol进行注册。
  • 入口地址,比如函数指针,可被传递给Scheme,并用作foreign-procedure表达式中的entry表达式的值。通过这种方式,即使外部入口点没有按名称注册,也可以使用它们。

过程:(foreign-entry? entry-name) 返回:如果entry-name是一个存在的外部过程入口点返回#t,否则#f 库:(chezscheme)

entry-name 必须是 string。foreign-entry? 可用于判断外部过程的入口是否存在。

以下示例假定包含strlen定义的库已通过load-shared-object加载,或者strlen已经通过本节中介绍的其他方法注册。

(foreign-entry? "strlen") => #t
((foreign-procedure "strlen"
    (string) size_t)
 "hey!") => 4

过程:(foreign-entry entry-name) 返回:entry-name的地址,exact integer类型 库:(chezscheme)

entry-name必须是一个命名现有外部入口点的字符串。

以下示例假定包含strlen定义的库已通过load-shared-object加载,或者strlen已经通过本节中介绍的其他方法注册。

(let ([addr (foreign-entry "strlen")])
  (and (integer? addr) (exact? addr))) => #t

(define-ftype strlen-type (function (string) size_t))
(define strlen
  (ftype-ref strlen-type ()
    (make-ftype-pointer strlen-type "strlen")))
(strlen "hey!") => 4

过程:(foreign-address-name address) 返回:address对应的entry-name。若不存在,返回#f 库:(chezscheme)

以下示例假定包含strlen定义的库已通过load-shared-object加载,或者strlen已经通过本节中介绍的其他方法注册。

(foreign-address-name (foreign-entry "strlen")) => "strlen"

过程:(load-shared-object path) 返回:未指定 库:(chezscheme)

path必须是一个string。load-shared-object 加载由 path 指定的共享对象。共享对象可能是系统库或从普通C程序创建的文件。共享对象中的所有外部符号以及与shared objects链接的其他共享对象中可用的外部符号都变成可用的外部入口。

在Chez Scheme运行的大多数平台上都支持该过程。

如果path不以"."或者"/"开始,Chez Scheme 将在系统默认的一组搜索路径中查找共享对象。

在大多数Unix系统上,load-shared-object基于系统函数dlopen。在Windows下,load-shared-object基于LoadLibrary。请参阅这些例程,C编译器和加载器的文档,获取有关定位和构建共享对象的准确规则。

load-shared-object可以用来访问内置的C库函数,比如getenv。共享对象的名称因系统而异。

在Linux系统上: (load-shared-object "libc.so.6")

在Solaris,OpenSolaris,FreeBSD,NetBSD和OpenBSD系统上:

(load-shared-object "libc.so")

在MacOS X系统上:

(load-shared-object "libc.dylib")

在Windows上:

(load-shared-object "crtdll.dll")

一旦C库被加载,getenv应该是可用的外部入口。

(foreign-entry? "getenv") => #t

等效的Scheme过程可以用如下方式定义和调用:

(define getenv
    (foreign-procedure "getenv"
        (string)
        string))
(getenv "HOME") => "/home/elmer/fudd"
(getenv "home") => #f

load-shared-object也可以用来访问用户创建的库。假设C文件"env.c"中包含

int even(n) int n; { return n == 0 || odd(n - 1); }

C文件"odd.c"中包含

int odd(n) int n; { return n != 0 && even(n - 1); }

这些文件必须被编译并链接到共享对象才能被加载。其过程取决于宿主系统:

在Linux,FreeBSD,OpenBSD和OpenSolaris上:

(system "cc -fPIC -shared -o evenodd.so even.c odd.c")

根据主机配置的不同,可能需要 -m32 或 -m64 选项来指定32位或64位编译。

在MacOS X(Intel或PowerPC)系统上:

(system "cc -dynamiclib -o evenodd.so even.c odd.c") 根据主机配置的不同,可能需要 -m32 或 -m64 选项来指定32位或64位编译。

在32位Sparc Solaris上:

(system "cc -KPIC -G -o evenodd.so even.c odd.c")

在64位Sparc Solaris上:

(system "cc -xarch=v9 -KPIC -G -o evenodd.so even.c odd.c")

在Windows上,我们构建一个DLL(动态链接库)文件。为了使编译器生成适当的入口点,我们改变even.c为

#ifdef WIN32
#define EXPORT extern __declspec (dllexport)
#else
#define EXPORT extern
#endif

EXPORT int even(n) int n; { return n == 0 || odd(n - 1); }

odd.c为

#ifdef WIN32
#define EXPORT extern __declspec (dllexport)
#else
#define EXPORT extern
#endif

EXPORT int odd(n) int n; { return n != 0 && even(n - 1); }

然后,我们可以按如下所示构建DLL,并为其提供扩展名“.so”而不是“.dll”,以便与其他系统保持一致。

(system "cl -c -DWIN32 even.c")
(system "cl -c -DWIN32 odd.c")
(system "link -dll -out:evenodd.so even.obj odd.obj")

生成的“.so”文件可以加载到Scheme中,even和odd可以作为外部过程使用:

(load-shared-object "./evenodd.so")
(let ([odd (foreign-procedure "odd"
             (integer-32) boolean)]
      [even (foreign-procedure "even"
              (integer-32) boolean)])
  (list (even 100) (odd 100))) => (#t #f)

文件名以“./evenodd.so”定义,而不是简单的“evenodd.so”,因为有些系统在标准系统目录集中查找共享库,而不包含当前目录的。

过程:(remove-foreign-entry entry-name) 返回:未指定 库: (chezscheme)

remove-foreign-entry将阻止在其之后对entry-name指定的入口进行访问。如果目标不存在,将&assertion条件类型抛出异常。因为由foreign-procedure建立的访问不受影响,remove-foreign-entry可用于在一组需要的外部过程接口建立之后进行清理工作。

remove-foreign-entry可用于删除由Sforeign_symbol和Sregister_symbol注册的入口,但不能删除通过调用load-shared-object而创建的入口。

4.7 使用其他的外部语言 (Using Other Foreign Languages)

尽管Chez Scheme外部过程接口主要面向C中定义的过程或C库中可用的过程,但也可以调用其他语言中遵循C调用约定的过程。一个难点可能来自名字解析。基于Unix的C编译器通常会在外部名前加一个下划线,外部接口尝试以与主机C编译器一致的方式解释入口名。偶尔,如汇编代码文件,这个条目的名称可能不以被期望的方式解析。通过在入口名前添加一个“=”字符可防止。 例如,加载包含过程“foo”的汇编文件后,可能会有

(foreign-entry?“foo”) => #f
(foreign-entry?“=foo”)=> #t

4.8 C库过程 (C Library Routines)

通过一组C预处理宏和C可调用库函数提供额外的外部接口支持。其中一些函数允许C程序检查,分配和改变Scheme对象。另外一些允许C函数通过一些比4.3节中更基本的接口调用Scheme过程。其他一些允许开发定制的可执行镜像,并使用Scheme系统作为一个程序中的下属程序,例如,作为可扩展语言。

使用这些函数的C代码必须包含与Chez Scheme一起发布的“scheme.h”头文件,并且必须链接Chez Scheme内核(无论是静态还是动态)。头文件包含预处理宏定义和库函数的外部声明。文件可针对不同的Chez Scheme版本和针对的机器类型进行定制;不同Chez Scheme发布版本之间切换时,其应该保持不被修改,并且合适版本的头文件必须总是与特定版本的Chez Scheme对应使用来编译C代码。版本和机器类型在“scheme.h”中定义为 VERSION 和 MACHINE_TYPE。

每个函数的名字都以大写的S开头,例如,Sfixnump。许多名字是对应的Scheme过程的简单变化,如,Sstringto_symbol是string->symbol的等效C接口。大多数Chez Scheme可执行程序中的以大小S开头的后跟下划线(S)的外部可见入口并没有在这里介绍;应该避免使用它们。

除了scheme.h中不同的宏和外部声明外,头文件还定义(typedefs)了一些在头文件中使用的类型

  • ptr: Scheme 值类型
  • iptr: signed integer,与Scheme值类型相同大小
  • uptr: unsigned integer,与Scheme值类型相同大小
  • string_char: 单个Scheme string元素类型
  • octet: 单个Scheme bytevector元素类型(unsigned char)

不同平台上,这些类型会有所不同,尽管ptr通常是void *,iptr通常是long int,uptr通常是usigned long int。

在Windows上,包含scheme.h前定义SCHEME_IMPORT,会使得scheme.h使用extern declspec (dllimport)来声明其入口点,而不是默认的extern declspec (dllexport)方式。不定义SCHEME_IMPORT而是定义SCHEME_STATIC会使得scheme.h只使用extern来声明。Chez Scheme的静态库使用SCHEME_STATIC。

本章后续内容依次介绍每个C接口函数。每个过程的声明按照ANSI C 函数原型规则准确指定参数和返回值类型。Scheme对象具有ptr C类型,在“scheme.h”中定义。在合适的地方,使用C值作为参数和返回值来替代Scheme对象。

预处理宏可能会多次求值其参数(或者不求值),所以要小心确保这会不产生问题。

定制。这里介绍的函数被用于初始化Scheme系统,建立Scheme heap,从一个独立的程序运行Scheme系统。

[func] char * Skernel_version(void)
[func] void Sscheme_init(void (*abnormal_exit)(void))
[func] void Sset_verbose(int v)
[func] void Sregister_boot_file(const char *name)
[func] void Sregister_boot_file_fd(const char *name, int fd)
[func] void Sbuild_heap(const char *exec, void (*custom_init)(void))
[func] void Senable_expeditor(const char *history_file)
[func] void Sretain_static_relocation(void)
[func] int Sscheme_start(int argc, char *argv[])
[func] int Sscheme_script(char *scriptfile, int argc, char *argv[])
[func] int Sscheme_program(char *programfile, int argc, char *argv[])
[func] void Scompact_heap(void)
[func] void Sscheme_deinit(void)

Skernel_version返回代表Scheme版本的字符串。在使用以上所列的任何初始化函数前,其应该被用于与VERSION预处理宏进行比较,确保使用了正确的"scheme.h"头文件。

Sscheme_init使得Scheme系统初始化静态内存,为boot文件注册做准备。abnormal_exit参数(可能为null)应该是一个没有参数的C函数指针,其在初始化或后续的堆建立过程中失败时采取合适的措施。如果为null,默认的行为是调用exit(1)。

Sset_verbose指定verbose模式,v是非零值为打开,v为零时关闭。在verbose模式,系统会显示查找后续注册boot文件的过程。

Sregister_boot_file 搜索参数name中的boot文件,并注册在之后加载,而Sregister_boot_file_fd提供指定的boot file和一个文件描述符。当只有一个boot文件名字提供时,文件被打开,但是不被加载,直到通过Sbuild_heap建立堆后才被加载。当一个文件描述符被提供时,文件名name只被用来报告错误。只对第一个注册的boot文件,系统也会查找name文件依赖的boot文件,无论是直接还是间接方式。

Sbuild_heap从注册的boot文件创建Scheme heap。exec被假定为可执行镜像的名字或者路径,当没有boot文件被注册时,作为boot文件搜索的名字。只有当一个或多个boot文件被注册时,exec可以为null。custom_init必须是没有参数的C函数的指针(可以为null);如果不是null,在boot文件加载之前被调用。

Sscheme_start 调用交互启动过程,例如,scheme-start参数的值,一个Scheme string参数包含argv参数的前argc个元素,不包含argv[0]。Sscheme_script类似地调用脚步启动过程,例如,scheme-script参数值,一个Scheme string参数包含scriptfile和argv参数的前argc个元素,不包含argv[0]。Sscheme_program类似地调用程序启动过程,例如,scheme-program参数值,一个Scheme string参数包含programfile和argv参数的前argc个元素,不包含argv[0]。

Senable_expeditor启用表达式编辑器(见2.2节,14章),默认是不启用的,指定历史文件名history_file,从该文件恢复历史记录和保存历史记录。该过程必须在堆建立后调用,否则会产生错误。它也必须在Sscheme_start之前调用,这样才能起效。如果histroy_file参数是null指针,历史记录不能恢复或者保存。在scheme.h中如果预处理变量FEATURE_EXPEDITOR 被定义,那么对表达式编辑器的支持已经被编译进系统。

Sretain_static_relocation 会保留静态类型的代码对象重定位信息,为获得compute-size及相关过程的优势,这些代码对象由heap compaction创建。

Scompact_heap压缩Scheme的堆,将当前所有堆中的对象放入静态类型(static generation)。在静态类型中的对象不会被回收。也就是说,它们永远不会在回收过程中被移动并且它们的存储空间永远不会被收回,甚至它们变得不可访问。Scompact_heap在任何boot文件加载后会被隐式调用。

Sscheme_deinit 关闭任何打开的文件,撤回Scheme heap,将Scheme系统变成未初始化状态。

判断。这里表述的判断函数与Scheme中的判断函数名字相类似。判断函数以字母p结尾,用于替换在Scheme判断函数名字后面常见的问号。每个判断接收单个Scheme对象,返回一个布尔值(C integer)。

[macro] int Sfixnump(ptr obj)
[macro] int Scharp(ptr obj)
[macro] int Snullp(ptr obj)
[macro] int Seof_objectp(ptr obj)
[macro] int Sbwp_objectp(ptr obj)
[macro] int Sbooleanp(ptr obj)
[macro] int Spairp(ptr obj)
[macro] int Ssymbolp(ptr obj)
[macro] int Sprocedurep(ptr obj)
[macro] int Sflonump(ptr obj)
[macro] int Svectorp(ptr obj)
[macro] int Sbytevectorp(ptr obj)
[macro] int Sfxvectorp(ptr obj)
[macro] int Sstringp(ptr obj)
[macro] int Sbignump(ptr obj)
[macro] int Sboxp(ptr obj)
[macro] int Sinexactnump(ptr obj)
[macro] int Sexactnump(ptr obj)
[macro] int Sratnump(ptr obj)
[macro] int Sinputportp(ptr obj)
[macro] int Soutputportp(ptr obj)
[macro] int Srecordp(ptr obj)

访问器。部分在这里介绍的访问器对应Scheme过程中类似的名字,但是有些只在该接口中。Sfixnum_value, Schar_value, Sboolean_value,和Sflonum_value返回C等效的Scheme值。

[macro] iptr Sfixnum_value(ptr fixnum)
[macro] uptr Schar_value(ptr character)
[macro] int Sboolean_value(ptr obj)
[macro] double Sflonum_value(ptr flonum)

Sinteger_value 和 Sunsigned_value 与 Sfixnum_value 类似,除了他们不只接受 fixnum 参数,也接受在C integer或unsinged值范围内的 bignum 参数。Sinteger_value 和Sunsigned_value 接受与Scheme integer相同的范围。他们的区别只在返回类型,所以允许对负值或unsigned大数值的不同解释。

[func] iptr Sinteger_value(ptr integer)
[macro] uptr Sunsigned_value(ptr integer)

Sinteger32_value, Sunsigned32_value, Sinteger64_value, 和Sunsigned64_value 接受32位或64位范围的signed 或 unsigned Scheme integers,并且根据机器类型返回合适的integers。

[func] <32-bit int type> Sinteger32_value(ptr integer)
[macro] <32-bit unsigned type> Sunsigned32_value(ptr integer)
[func] <64-bit int type> Sinteger64_value(ptr integer)
[macro] <64-bit unsigned type> Sunsigned64_value(ptr integer)

Scar, Scdr, Ssymbol_to_string (对应symbol->string), and Sunbox与他们在Scheme中的对应函数相同。

[macro] ptr Scar(ptr pair)
[macro] ptr Scdr(ptr pair)
[macro] ptr Ssymbol_to_string(ptr sym)
[macro] ptr Sunbox(ptr box)

Sstring_length, Svector_length, Sbytevector_length, 和 Sfxvector_length 都返回一个代表对象中元素个数的 C integer值。

[macro] iptr Sstring_length(ptr str)
[macro] iptr Svector_length(ptr vec)
[macro] iptr Sbytevector_length(ptr bytevec)
[macro] iptr Sfxvector_length(ptr fxvec)

Sstring_ref, Svector_ref, Sbytevector_u8_ref, 和 Sfxvector_ref 对应它们在Scheme中的相应函数,除了index参数是C integers,Sstring_ref的返回值是一个C char类型值,Sbytevector_u8_ref的返回值是一个octet (unsigned char)。

[macro] char Sstring_ref(ptr str, iptr i)
[macro] ptr Svector_ref(ptr vec, iptr i)
[macro] octet Sbytevector_u8_ref(ptr fxvec, iptr i)
[macro] ptr Sfxvector_ref(ptr fxvec, iptr i)

Scheme bytevector 表示一个长度域紧跟一个octet(unsigned chars)序列。Sbytevector_data 返回一个指向octets序列开始的指针。在任何Scheme代码执行前,停止解引用由Sbytevector_data返回的指针或者锁定bytevector(见下面的Slock_object)到内存都需要极其小心,无论是通过调用进入Scheme或者从Scheme调用中返回。存储管理器会重定位或者回收指针所指的对象,并且会将其他数据拷贝到该对象。 (译者注:这里原文有拼写错误,unsignec chars -> unsigned chars)

[macro] octet * Sbytevector_data(ptr bytevec)

存取器。改变包含指针的存取对象,如pairs何vectors,必须代表存储管理器进行跟踪,参考文献[13]。这里介绍的操作在需要时自动执行这种跟踪。

[func] void Sset_box(ptr box, ptr obj)
[func] void Sset_car(ptr pair, ptr obj)
[func] void Sset_cdr(ptr pair, ptr obj)
[macro] void Sstring_set(ptr str, iptr i, char c)
[func] void Svector_set(ptr vec, iptr i, ptr obj)
[macro] void Sbytevector_u8_set(ptr bytevec, iptr i, octet n)
[macro] void Sfxvector_set(ptr fxvec, iptr i, ptr fixnum)

一些Scheme对象,例如procedures和numbers是不可变的,所以没有提供可以改变这些内容的操作。

构造器。这里介绍的构造器用于创建Scheme对象。一些对象,如fixnums和empty list,用瞬时值表示,它们不需要任何堆分配,例如pairs和vectors表示为指向堆分配对象的指针。

Snil, Strue, Sfalse, Sbwp_object, Seof_object, 和Svoid 创建常量瞬时值,表示空list(()),布尔值(#t和#f),broken-weak-pointer对象(#!bwp),eof对象(#!eof),和void对象。

[macro] ptr Snil
[macro] ptr Strue
[macro] ptr Sfalse
[macro] ptr Sbwp_object
[macro] ptr Seof_object
[macro] ptr Svoid

Fixnums, characters, booleans, flonums, 和 strings 可以用它们在C中等价方式创建。

[macro] ptr Sfixnum(iptr n)
[macro] ptr Schar(char c)
[macro] ptr Sboolean(int b)
[func] ptr Sflonum(double x)
[func] ptr Sstring(const char *s)
[func] ptr Sstring_of_length(const char *s, iptr n)
[func] ptr Sstring_utf8(const char *s, iptr n)

Sstring 创建一个C字符串s 的Scheme拷贝,而Sstring_of_length创建一个长度为n,从s中拷贝前n字节的新Scheme string。

如果C字符串以UTF-8编码,使用Sstring_utf8。指定n个字节长度,或者使用-1指定直到'\0'结束。

可以判断一个C integer是否在fixnum的范围内,通过比较fixnum的值与通过fixnum 从C integer创建的值。

#define fixnum_rangep(x) (Sfixnum_value(Sfixnum(x)) == x)

Sinteger 和 Sunsigned 可用于创建Scheme integers,无论它们是否在fixnum范围内。

[func] ptr Sinteger(iptr n)
[func] ptr Sunsigned(uptr n)

Sinteger 和 Sunsigned 在对待C integer负值和C unsigned integer值时有所不同,如果它们转换为integers会出现负值。Sinteger 把这些值转换成负的Scheme值,但是Sunsigned 把这些值转换成Scheme正值。例如,假设32位两种对于iptrs互补的表示,Sinteger(-1)和Sunsigned((iptr)0xffffffff) 都转换为Scheme integer -1,Sunsigned(0xffffffff) 和 Sunsigned((uptr)-1) 都被转换为Scheme integer #xffffffff(4294967295)。

无论使用哪个函数,Sinteger_value 和 Sunsigned_value 总会重新产生对应的C 输入值,因此下面的语句都等效于x,如果x是iptr。

Sinteger_value(Sinteger(x))
(iptr)Sunsigned_value(Sinteger(x))
Sinteger_value(Sunsigned((uptr)x))
(iptr)Sunsigned_value(Sunsigned((uptr)x))

类似地,下面的语句都等效于,如果x是uptr。

(uptr)Sinteger_value(Sinteger((iptr)x))
Sunsigned_value(Sinteger((iptr)x))
(uptr)Sinteger_value(Sunsigned(x))
Sunsigned_value(Sunsigned(x))

Sinteger32, Sunsigned32, Sinteger64, 和 Sunsigned64 与上面的类似,但是将它们的参数限制在32位或64位范围。

[func] ptr Sinteger32(<32-bit int type> n)
[func] ptr Sunsigned32(<32-bit unsigned type> n)
[func] ptr Sinteger64(<64-bit int type> n)
[func] ptr Sunsigned64(<64-bit unsigned type> n)

Scons 和 Sbox 与它们在Scheme中的对应函数相同。

[func] ptr Scons(ptr obj1, ptr obj2)
[func] ptr Sbox(ptr obj)

Sstring_to_symbol 与其在Scheme中的对应函数类似, string->symbol, 除了它接受C字符串作为输入。

[func] ptr Sstring_to_symbol(const char *s)

Smake_string, Smake_vector, Smake_bytevector, 和 Smake_fxvector 与它们在Scheme中的对应函数相同。

[func] ptr Smake_string(iptr n, int c)
[func] ptr Smake_vector(iptr n, ptr obj)
[func] ptr Smake_bytevector(iptr n, int fill)
[func] ptr Smake_fxvector(iptr n, ptr fixnum)

Smake_uninitialized_string 与一个参数的make-string函数类似。

[func] ptr Smake_uninitialized_string(iptr n)

Windows限定帮助函数。下面的帮助函数只在Windows中提供。

[func] char * Sgetenv(const char * name)

Sgetenv 返回 UTF-8-encoded 环境变量名的 UTF-8-encoded 值,如果该值存在,否则为NULL。当返回值不再需要时,要用free释放掉返回值。

[func] wchar_t * Sutf8_to_wide(const char *s)
[func] char * Swide_to_utf8(const wchar_t *s)

(译者注:原文中s前面有\)

Sutf8_to_wide 和 Swide_to_utf8 能将'\0'结尾的字符串在UTF-8-encoded 与 UTF-16LE-encoded两者间转换。当返回值不再需要时,要用free释放掉返回值。

访问top-level值。顶层变量绑定能通过Stop_level_value访问,或者通过Sset_top_level_value赋值。

[func] ptr Stop_level_value(ptr sym)
[func] void Sset_top_level_value(ptr sym, ptr obj)

这些过程给出在原始交互环境中绑定的快速访问,并且不反应对交互环境参数或顶层导入模块的修改。访问当前交互环境绑定的symbol,必须调用Scheme的top-level-value和set-top-level-value!函数。

锁定Scheme对象。储存管理器为了回收空间和压缩堆会周期性重新定位对象。该重定位过程对于Scheme程序完全透明,因为所有重新定位的对象的指针会被更新为指向对象的新地址。但是,储存管理器不能更新在Scheme堆外的Scheme指针。

通常规则下,所有来自C变量的指针或Scheme对象的数据结构在进入(或重新进入)Scheme之前应该被丢弃。即如果C过程收到来自Scheme或通过本节中的机制获取的对象,所有的对象指针应该被认为是无效的,一旦C过程调用进入Scheme中,或者返回到Scheme。对无效指针解引用或传递回Scheme会导致灾难性后果,包括不可恢复的内存段错误。前述内容不适用瞬时对象,如fixnums, characters, booleans, 或者空list。适用于所有在堆上分配的对象,包含pairs, vectors, strings, 所有的numbers除了fixnums,ports,procedures,和records。

实践中,保证C代码不保存Scheme对象的指针的最好方法是:如果可能的话,立即把Scheme对象转化成C的等效。在特定的情况下,不可能这么做,那么Scheme对象的保留是程序的C部分设计的必须内容。在这些情况下,对象可以通过库函数Slock_object锁定(或者,在Scheme中用等效的lock-object过程)

[func] void Slock_object(ptr obj)

锁定一个对象防止储存管理器回收或重定位对象。锁定要尽量少用,因为其会引入内存碎片,增加储存管理负担。如果对象没有被解锁的话,锁定也会导致储存空间的意外保留。锁定那些已经通过堆压缩(见上面的Scompact_heap)变成静态的对象不但是不必要的,而且是有害的。

对象可通过Sunlock_object解锁(unlock-object)。

[func] void Sunlock_object(ptr obj)

通过连续调用Slock_object或lock-object,对象可以被多次锁定,在该情况下,在其正在解锁前,必须通过调用相同次数的Sunlock_object或unlock-object来解锁该对象。

函数Sunlocked_objectp用于判断对象是否锁定。

[func] int Sunlocked_objectp(ptr obj)

当外部过程调用进入Scheme时,指向Scheme代码对象的返回地址与外部过程被隐式传递给C函数。因此系统会在从C调用返回Scheme前锁定代码对象,从Scheme返回后再解锁。锁定被自动执行;用户代码不需要锁定这些代码对象。

在锁定对象中的对象,例如在锁定的pair的car中的对象,一般不需要锁定,除非另外有个C指针指向该对象。

注册外部入口点。通过Sforeign_symbol或Sregister_symbol可使外部入口点对Scheme可见。

[func] void Sforeign_symbol(const char *name, void *addr)
[func] void Sregister_symbol(const char *name, void *addr)

objects文件或通过load-shared-object加载的共享对象中的外部入口点自动对系统可见。一旦外部入口点可见,可以通过foreign-procedure表达式创建入口点的Scheme可调用版本。Sforeign_symbol和Sregister_symbol允许程序注册非外部入口点,与Chez Scheme静态链接的代码中的入口点,和直接从C加载的代码的入口点,例如不适用load-shared-object。Sforeign_symbol和Sregister_symbol唯一的区别是当尝试注册已经存在的名字时Sforeign_symbol会抛出异常,而Sregister_symbol允许重新定义已存在的名字。

获取Scheme入口点。Sforeign_callable_entry_point 从由foreign-callable产生的代码对象中获取入口点,执行与其在Scheme中对应函数的相同操作,即Scheme过程foreign-callable-entry-point。

[func] (void (*) (void)) Sforeign_callable_entry_point(ptr code)

这可被用于防止从代码对象到地址的转换,直到真正需要这么做时才进行,这可以在有些情况下消除锁定代码对象的需要,如果再次调用返回Scheme,代码对象没有被保存。

Sforeign_callable_code_object执行相反的转换。

[func] ptr Sforeign_callable_code_object((void (*addr)(void)))

Scheme调用的底层支持。下面的函数提供从C调用Scheme过程的支持。下面的函数能够调用接收小数目参数(0-3)的Scheme过程。

[func] ptr Scall0(ptr procedure)
[func] ptr Scall1(ptr procedure, ptr obj1)
[func] ptr Scall2(ptr procedure, ptr obj1, ptr obj2)
[func] ptr Scall3(ptr procedure, ptr obj1, ptr obj2, ptr obj3)

在每种情况下,第一个参数procedure必须是Scheme过程。剩下的参数,必须是传递给过程的Scheme对象。本节前面介绍的工具可用于将C数据类型转换为Scheme的等价类型。从类似foreign-callable表达式的声明自动产生转换代码的程序与Chez Scheme一起发布。在大多数系统上,可以在Scheme库目录中被找到,在“foreign.ss”文件中。

Scheme过程可通过多种方法获得。例如,从Scheme到C的调用时作为参数接收到的,通过另外一个从C到Scheme的调用中获得的,从Scheme数据结构中获得的,或者通过Stop_level_value从顶层环境中获得的。

下面函数涉及长参数表的更通用的接口。

[func] void Sinitframe(iptr n)
[func] void Sput_arg(iptr i, ptr obj)
[func] ptr Scall(ptr procedure, iptr n)

C过程首先以一个参数调用Sinitframe,即传递给Scheme的参数个数。然后对每个参数调用Sput_arg(以任意顺序),指定Sput_arg的参数数字(以1开始)和参数。最后,调用Scall执行调用,指定Scheme过程和参数个数(与Sinitframe调用中相同的数字)。程序员必须确保在尝试任何其他Scheme调用和从Scheme返回前,Scheme调用通过Sinitframe初始化,以Scall结束。如果由于任何原因调用在Sinitframe已经被调用后没有完成,那就不能够再返回Scheme了。

下例用于展示上述简单接口和通用接口。

/* 特别傻的两个浮点数相乘的方法 */
double mul(double x, double y) {
    ptr times = Stop_level_value(Sstring_to_symbol("*"));

    return Sflonum_value(Scall2(times, Sflonum(x), Sflonum(y)));
}

/* 一样傻的使用五个参数调用printf的方法*/

/* 最好定义如下所示的接口用于调用进入Scheme,这样可以防止意外创建嵌套帧,
 * 帮助确保初始化调用如上所述完成。特定的版本为特定的C参数类型量身定制,
 * 内部可嵌入Scheme对象转换操作。
 */
ptr Scall5(ptr p, ptr x1, ptr x2, ptr x3, ptr x4, ptr x5) {
    Sinitframe(5);
    Sput_arg(1, x1);
    Sput_arg(2, x2);
    Sput_arg(3, x3);
    Sput_arg(4, x4);
    Sput_arg(5, x5);
    Scall(p, 5);
}

static void dumpem(char *s, int a, double b, ptr c, char *d) {
    printf(s, a, b, c, d);
}

static void foo(int x, double y, ptr z, char *s) {
    ptr ois, sip, read, expr, eval, c_dumpem;
    char *sexpr = "(foreign-procedure \"dumpem\" (string integer-32\
 double-float scheme-object string) void)";

  /* 这一系列语句是经过小心斟酌的,为了避免在进入Scheme后引用包含Scheme对象的变量 */
    ois = Stop_level_value(Sstring_to_symbol("open-input-string"));
    sip = Scall1(ois, Sstring(sexpr));
    read = Stop_level_value(Sstring_to_symbol("read"));
    expr = Scall1(read, sip);
    eval = Stop_level_value(Sstring_to_symbol("eval"));
    Sforeign_symbol("dumpem", (void *)dumpem);
    c_dumpem = Scall1(eval, expr);
    Scall5(c_dumpem,
           Sstring("x = %d, y = %g, z = %x, s = %s\n"),
           Sinteger(x),
           Sflonum(y),
           z,
           Sstring(s));
}

从 C 到 Scheme 的调用不应该发生在C中断处理函数中。当Scheme调用C时,系统保存特定的寄存器内容到一个寄存器保存区域。当C调用Scheme时,寄存器从寄存器保存区域恢复。因为中断可发生在计算的任何点处,寄存器内容保存位置可能包含无效信息,会导致Scheme系统失败,发生不正常操作。

激活,抑制,销毁线程。多线程版本的Scheme提供下面三个函数,当一个线程需要激活,抑制,销毁时,允许C代码通知Scheme。

[func] int Sactivate_thread(void)
[func] void Sdeactivate_thread(void)
[func] int Sdestroy_thread(void)

通过Scheme过程fork-thread可以创建一个激活状态的线程,不需要再激活。任何没被激活的线程,或者由除fork-thread外的其他机制创建的线程,在访问Scheme数据或Scheme执行代码之前必须被激活。由__collect_safe声明的外部可调用函数能够激活调用的线程。否则,必须用Sactivate_thread来激活线程。线程第一次激活后函数返回1,直到激活的线程被Sdestroy_thread销毁前,后续每个调用都返回0。

当激活状态的线程在C代码中时会阻止存储管理系统的垃圾回收过程,当可能会花大量的时间在C代码中时,线程必须通过Sdeactivate_thread抑制,或者通过__collect_safe声明的foreign-procedure。当线程调用C库函数时这尤其重要,例如read,该函数可能会不定期的阻塞。线程一旦被抑制,线程必须不能接触任何Scheme数据或执行任何Scheme代码直到被重新激活,但有一个例外。例外就是线程可以访问或者甚至修改锁定的Scheme对象,例如锁定的字符串,其不包含任何指向未锁定的Scheme对象。(当线程在没有激活时,没被锁定的对象可能会被垃圾回收器重新定位。)

Sdestroy_thread 用于通知Scheme系统线程被关闭,并且任何与线程相关的数据可以被释放。

底层同步基本函数。头文件定义几个预处理宏,可用于与对应ftype锁操作方式相同的锁定内存地址操作(见15.4和15.5)。

[macro] void INITLOCK(void *addr)
[macro] void SPINLOCK(void *addr)
[macro] void UNLOCK(void *addr)
[macro] void LOCKED_INCR(void *addr, int *ret)
[macro] void LOCKED_DECR(void *addr, int *ret)

LOCKED_INCR 和 LOCKED_DECR 设置ret为一个非零(true)值,如果增加或减少的值为0。否者它们会把ret置0。

4.9 示例:套接字操作 (Example: Socket Operations)

该节展示了使用Scheme和C代码结合的一个简单的socket接口。C代码定义了一组方便的底层操作系统接口,用于在更高层级的Scheme代码中实现打开,关闭,读取,写入sockets。

C代码(csocket.c)在下面给出,紧接着是Scheme代码(socket.ss)。代码需要很少或者无需修改就能在大多数Unix系统中运行,且可以被修改在windows下运行(使用Windows的WinSock接口)。

代码后面有一个会话例子,展示了socket接口。在9.7中有一个例子,展示了如何使用该socket接口建立一个进程端口,允许通过Scheme端口透明的读取和写入子进程。

C code.

/* csocket.c */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <unistd.h>

/* c_write 尝试写入整个缓冲,通过中断,socket延时和部分缓冲写入。*/
int c_write(int fd, char *buf, ssize_t start, ssize_t n) {
    ssize_t i, m;

    buf += start;
    m = n;
    while (m > 0) {
        if ((i = write(fd, buf, m)) < 0) {
            if (errno != EAGAIN && errno != EINTR)
                return i;
        } else {
            m -= i;
            buf += i;
        }
    }
    return n;
}

/* c_read 通过中断和socket延时*/
int c_read(int fd, char *buf, size_t start, size_t n) {
    int i;

    buf += start;
    for (;;) {
        i = read(fd, buf, n);
        if (i >= 0) return i;
        if (errno != EAGAIN && errno != EINTR) return -1;
    }
}

/* bytes_ready(fd) 返回true,如果在id为fd的socket中有可读的字节*/
int bytes_ready(int fd) {
    int n;

    (void) ioctl(fd, FIONREAD, &n);
    return n;
}

/* socket 支持*/

/* do_socket() 创建一个新的 AF_UNIX socket */
int do_socket(void) {
    return socket(AF_UNIX, SOCK_STREAM, 0);
}

/* do_bind(s, name) 绑定name 到socket s */
int do_bind(int s, char *name) {
    struct sockaddr_un sun;
    int length;

    sun.sun_family = AF_UNIX;
    (void) strcpy(sun.sun_path, name);
    length = sizeof(sun.sun_family) + sizeof(sun.sun_path);

    return bind(s, (struct sockaddr*)(&sun), length);
}

/* do_accept 接收在socket s上的连接 */
int do_accept(int s) {
    struct sockaddr_un sun;
    socklen_t length;

    length = sizeof(sun.sun_family) + sizeof(sun.sun_path);

    return accept(s, (struct sockaddr*)(&sun), &length);
}

/* do_connect 初始化一个socket连接 */
int do_connect(int s, char *name) {
    struct sockaddr_un sun;
    int length;

    sun.sun_family = AF_UNIX;
    (void) strcpy(sun.sun_path, name);
    length = sizeof(sun.sun_family) + sizeof(sun.sun_path);

    return connect(s, (struct sockaddr*)(&sun), length);
}

/* get_error 返回操作系统的错误状态*/
char* get_error(void) {
    extern int errno;
    return strerror(errno);
}

Scheme code.

;;; socket.ss

;;; Requires csocket.so, built from csocket.c.
(load-shared-object "./csocket.so")

;;; Requires from C library:
;;;   close, dup, execl, fork, kill, listen, tmpnam, unlink
(case (machine-type)
  [(i3le ti3le a6le ta6le) (load-shared-object "libc.so.6")]
  [(i3osx ti3osx a6osx ta6osx) (load-shared-object "libc.dylib")]
  [else (load-shared-object "libc.so")])

;;; basic C-library stuff

(define close
  (foreign-procedure "close" (int)
    int))

(define dup
  (foreign-procedure "dup" (int)
    int))

(define execl4
  (let ((execl-help
         (foreign-procedure "execl"
           (string string string string void*)
           int)))
    (lambda (s1 s2 s3 s4)
      (execl-help s1 s2 s3 s4 0))))

(define fork
  (foreign-procedure "fork" ()
    int))

(define kill
  (foreign-procedure "kill" (int int)
    int))

(define listen
  (foreign-procedure "listen" (int int)
    int))

(define tmpnam
  (foreign-procedure "tmpnam" (void*)
    string))

(define unlink
  (foreign-procedure "unlink" (string)
    int))

;;; routines defined in csocket.c

(define accept
  (foreign-procedure "do_accept" (int)
    int))

(define bytes-ready?
  (foreign-procedure "bytes_ready" (int)
    boolean))

(define bind
  (foreign-procedure "do_bind" (int string)
    int))

(define c-error
  (foreign-procedure "get_error" ()
    string))

(define c-read
  (foreign-procedure "c_read" (int u8* size_t size_t)
    ssize_t))

(define c-write
  (foreign-procedure "c_write" (int u8* size_t ssize_t)
    ssize_t))

(define connect
  (foreign-procedure "do_connect" (int string)
    int))

(define socket
  (foreign-procedure "do_socket" ()
    int))

;;; higher-level routines

(define dodup
 ; (dodup old new) closes old and dups new, then checks to
 ; make sure that resulting fd is the same as old
  (lambda (old new)
    (check 'close (close old))
    (unless (= (dup new) old)
      (error 'dodup
        "couldn't set up child process io for fd ~s" old))))

(define dofork
 ; (dofork child parent) forks a child process and invokes child
 ; without arguments and parent with the child's pid
  (lambda (child parent)
    (let ([pid (fork)])
      (cond
        [(= pid 0) (child)]
        [(> pid 0) (parent pid)]
        [else (error 'fork (c-error))]))))

(define setup-server-socket
 ; create a socket, bind it to name, and listen for connections
  (lambda (name)
    (let ([sock (check 'socket (socket))])
      (unlink name)
      (check 'bind (bind sock name))
      (check 'listen (listen sock 1))
      sock)))

(define setup-client-socket
 ; create a socket and attempt to connect to server
  (lambda (name)
    (let ([sock (check 'socket (socket))])
      (check 'connect (connect sock name))
      sock)))

(define accept-socket
 ; accept a connection
  (lambda (sock)
    (check 'accept (accept sock))))

(define check
 ; signal an error if status x is negative, using c-error to
 ; obtain the operating-system's error message
  (lambda (who x)
    (if (< x 0)
        (error who (c-error))
        x)))

(define terminate-process
 ; kill the process identified by pid
  (lambda (pid)
    (define sigterm 15)
    (kill pid sigterm)
    (void)))

Sample session.

> (define client-pid)
> (define client-socket)
> (let* ([server-socket-name (tmpnam 0)]
         [server-socket (setup-server-socket server-socket-name)])
   ; fork a child, use it to exec a client Scheme process, and set
   ; up server-side client-pid and client-socket variables.
    (dofork   ; child
      (lambda ()
       ; the child establishes the socket input/output fds as
       ; stdin and stdout, then starts a new Scheme session
        (check 'close (close server-socket))
        (let ([sock (setup-client-socket server-socket-name)])
          (dodup 0 sock)
          (dodup 1 sock))
        (check 'execl (execl4 "/bin/sh" "/bin/sh" "-c" "exec scheme -q"))
        (errorf 'client "returned!"))
      (lambda (pid) ; parent
       ; the parent waits for a connection from the client
        (set! client-pid pid)
        (set! client-socket (accept-socket server-socket))
        (check 'close (close server-socket)))))
> (define put ; procedure to send data to client
    (lambda (x)
      (let ([s (format "~s~%" x)])
        (c-write client-socket s (string-length s)))
      (void)))
> (define get ; procedure to read data from client
    (let ([buff (make-string 1024)])
      (lambda ()
        (let ([n (c-read client-socket buff (string-length buff))])
          (printf "client:~%~a~%server:~%" (substring buff 0 n))))))
> (get)
server:
> (put '(let ([x 3]) x))
> (get)
client:
3
server:
> (terminate-process client-pid)
> (exit)

Chez Scheme Version 9 User's Guide Copyright © 2018 Cisco Systems, Inc. Licensed under the Apache License Version 2.0 (full copyright notice.). Revised October 2018 for Chez Scheme Version 9.5.1 about this book

results matching ""

    No results matching ""