现在的位置: 首页 > 综合 > 正文

OCaml与C的互操作

2013年09月11日 ⁄ 综合 ⁄ 共 16230字 ⁄ 字号 评论关闭

本文总结OCaml与C的交互:在C中如何分配和修改ocaml value,如何将ocaml value转换为C struct, 以及异常处理。

章节目录
OCaml中调用C函数示例
value类型
C中表示OCaml数据类型
C中操作Ocaml values
与垃圾收集器和谐相处
从C到OCaml的回调
完整示例:Win32下的kill

OCaml中调用C函数示例

先以最简单的hello world开始:

hello.ml

  external print_hello: unit -> unit = "caml_print_hello"
 
  let _ = print_hello ();;
 

hello_stubs.c

 #include <stdio.h>
  #include <caml/mlvalues.h>
  #include <caml/memory.h>

  CAMLprim value
  caml_print_hello (value unit)
  {
      CAMLparam1(unit);
  
      printf("Hello world!\n");
      fflush(stdout);
  
      CAMLreturn (Val_unit);
  }

编译并运行:

字节码:
ocamlc -custom -o hello.exe hello.ml hello_stubs.c

本地代码:
ocamlopt -o hello.exe hello.ml hello_stubs.c

执行:
hello.exe
Hello world!

或者在toplevel中使用:

生成自定义的toplevel:
ocamlmktop -o hellotop.exe -custom  hello_stubs.c hello.ml

执行:
hellotop.exe

输入 Hello.print_hello();;

Hello world!
- : unit = ()

OCaml中声明函数

在OCaml中使用 external 关键字声明C函数:

external name : type = C-function-name

C函数名不需要与OCaml的函数名相同。

外部函数在接口文件.mli中可以作为一般val声明:

val name : type

这隐藏了使用C函数进行实现的细节,当然也可以显式声明为外部函数:

external name : type = C-function-name

后面的方法更高效,因为它允许模块的使用者直接调用C函数,而不是先调 用相应的Caml函数。

C中实现函数

如果函数参数小于等于5个,则C函数接受指定个数的value类型参数,并返 回一个value类型的结果。value类型用来表示Caml values。它编码了几种 基本类型(整数,浮点数,字符串,…),还有Caml数据结构。 后面会介绍 value类型相关的转换函数和宏。

超过5个参数的函数需要实现两个C函数。第一个用于字节码编译器ocamlc, 接受2个参数:指向Caml values数组的指针和表示参数个数的整数。 另一个用于本地代码编译器ocamlopt,直接接受所有参数。例如,下面这个 接受7个参数的函数add_nat:

CAMLprim value add_nat_native(value nat1, value ofs1, value len1,
                              value nat2, value ofs2, value len2,
                              value carry_in)
{
  ...
}

CAMLprim value add_nat_bytecode(value *argv, int argn)
{
  return add_nat_native(argv[0], argv[1], argv[2], argv[3],
                        argv[4], argv[5], argv[6]);
}

在OCaml中必须指明这两个函数:

external name : type = bytecode-C-function-name native-code-C-function-name

例如,add_nat声明如下:

external add_nat: nat -> int -> int -> int -> nat -> int -> int -> int -> int
                = "add_nat_bytecode" "add_nat_native"

实现一个函数实际上有两个步骤:
1,解码指定的Caml value参数到C value, 编码返回值到一个Caml value;
2,从参数计算出结果。

除了非常简单的函数,最好采用两个分离的C函数来实现这两个步骤。 第一个函数完成实际的运算,接受C值作为参数并返回一个C值。 第二个函数,也叫做"stub code",通过转换Caml values参数到C values, 调用第一个函数,转换返回的C value到Caml value,来对第一个函数进行简 单地包装。例如,以下的stub code:

CAMLprim value input(value channel, value buffer, value offset, value length)
{
  return Val_long(getblock((struct channel*) channel,
                          &Byte(buffer, Long_val(offset)),
                          Long_val(length)));
}

(这里的Val_long,Long_val是value类型的转换宏,后面将会讨论。 CAMLprim宏用来保证这个函数是导出的,并且可以被Caml访问).主要工作 都有getblock来完成。

使用C代码操作OCaml values,可以使用以下头文件:

头文件 提供功能
caml/mlvalues.h 定义value类型和转换宏
caml/alloc.h 分配函数(用于创建结构化的Caml对象)
caml/memory.h 各种内存相关的函数和宏(主要是GC接口)
caml/fail.h 引发异常的函数
caml/callback.h 从C到Caml的回调
caml/custom.h 自定义块的操作
caml/intext.h 对自定义块进行用户定义的序列化和反序列化操作
caml/threads.h 多线程操作

value类型

一个value类型的对象可以是:

  • 一个整数
  • 一个指向堆中内存块的指针 (比如caml_alloc_*分配的内存块)
  • 一个指向堆外的对象的指针 (比如malloc分配的内存块,或者一个C变量)。

整型value

整型values使用31-bit有符号整数(64位架构上使用63-bit)。它们属于 unboxed(unallocated,不进行内存分配,也就是直接存放在cpu寄存器中)。 ocaml中char, bool, int都用整型表示。

堆(ocaml中存放数据的堆,除了整型value都在此堆上分配、释放)上 的块被垃圾收集器管理,因此有一些严格的限制。每个块都有一个头包含 这个块的大小(以word为单位)和这个块的tag。 tag表示块的内容如何组织。 一个小于No_scan_tag 的tag表示一个结构化的块,包含了结构良好的value, 它会被垃圾收集器循环遍历扫描每个字段。一个大于等于No_scan_tag 的tag是一个原始块,它的内容不会被垃圾收集器扫描。

Tag 块的内容
0 to No_scan_tag - 1 结构化的块(Caml对象的数组)。每个字段为一个value
Closure_tag 一个函数闭包。第一个字段为指向代码的指针,后面的字段为环境中的value
String_tag 一个字符串
Double_tag 一个双精度浮点数
Double_array_tag 一个双精度浮点数组
Abstract_tag 一个抽象数据类型
Custom_tag 一个抽象数据类型,包含了用户定义的析构,比较,哈希,序列化和反序列化相关函数。

堆外的指针

任何在堆外以字对齐的指针都可以安全地和value类型相互转换。这包括 malloc返回的指针,使用&操作符获得的C变量指针(最小为一个字)。

注意:如果malloc返回的指针转换为value类型并返回到Caml,使用free进 行显式内存回收是很危险的,因为这个指针在Caml中可能仍然有效。更糟 的是,使用free回收内存可以比Caml堆重分配晚,原先指向Caml堆外面的指针 现在指向了Caml堆内部,造成垃圾收集器混乱。为了避免这些问题,推荐把 指针封装为一个Caml块,可以用Abstract_tag或Custom_tag。

示例:inspector

知道了OCaml value在C中的表示方式,就可以在C中编写函数查看这些底层 表示. 此示例实现读取value并输出value的类型.

inspect_stubs.c

#include <stdio.h>
#include <caml/mlvalues.h>
#include <caml/memory.h>


/* 缩进显示 */
void margin(int n)
{
    while (n-- > 0)  printf(".");
    return;
}

/*
 * 输出OCaml value的类型                                
 * v, OCaml value                                   
 * m, 缩进宽度                                      
 */
void print_block(value v, int m)
{
    int size, i;
    margin(m);
    if (Is_long(v)) {
        printf("直接量 (%d)\n", Long_val(v));
        return;
    }

    printf ("内存块: 大小=%d  -  ", size = Wosize_val(v));
    switch (Tag_val(v)) {
    case Closure_tag:
        printf ("闭包:包含 %d 个自由变量\n", size - 1);
        margin(m+4);
        printf("代码指针: %p\n", Code_val(v));
        for (i = 1; i < size; i++) print_block(Field(v,i), m+4);
        break;
    case String_tag:
        printf ("字符串: %s (%s)\n", String_val(v), (char *) v);
        break;
    case Double_tag:
        printf ("浮点数: %g\n", Double_val(v));
        break;
    case Double_array_tag:
        printf ("浮点数组: ");
        for (i = 0; i < size / Double_wosize; i ++ )
            printf("  %g", Double_field(v, i));
        printf("\n");
        break;
    case Abstract_tag:
        printf ("抽象类型\n");
        break;
    case Custom_tag:
        printf ("抽象类型,包含用户自定义的析构函数...\n");
        break;
    default:
        if (Tag_val(v) >= No_scan_tag) {
            printf ("未知tag");
            break;
        }
        printf ("structured block (tag = %d):\n", Tag_val(v));
        for (i = 0; i < size; i++)
            print_block(Field(v, i), m+4);
    }
    return;
}

/* 包装函数 */
CAMLprim value
inspect_block (value v)
{
    CAMLparam1(v);

    print_block (v, 4);
    fflush(stdout);

    CAMLreturn (v);
}

inspector.ml

external inspect : 'a -> 'a = "inspect_block";;

print_endline "使用Inspector.inspect查看OCaml value\n"

注意这两个文件都用cp936编码保存(虽然ocaml要求源文件都用utf-8编码),否 则cmd会显示乱码.编译并执行:

ocamlmktop -custom -o inspector.exe inspector.ml inspect_stubs.c
inspector.exe

进行测试:

# Inspector.inspect 5;; (* 测试整数 *)
....直接量 (5)
- : int = 5
# Inspector.inspect "string";; (* 字符串 *)
....内存块: 大小=2  -  字符串: string (string)
- : string = "string"
# Inspector.inspect false;;
....直接量 (0)
- : bool = false
# Inspector.inspect [1; 2; 3];; (* 列表 *)
....内存块: 大小=2  -  structured block (tag = 0):
........直接量 (1)
........内存块: 大小=2  -  structured block (tag = 0):
............直接量 (2)
............内存块: 大小=2  -  structured block (tag = 0):
................直接量 (3)
................直接量 (0)
- : int list = [1; 2; 3]
# Inspector.inspect [|1; 2; 3|];; (* 数组 *)
....内存块: 大小=3  -  structured block (tag = 0):
........直接量 (1)
........直接量 (2)
........直接量 (3)
- : int array = [|1; 2; 3|]

# Inspector.inspect 3.14;; (* 浮点数 *)
....内存块: 大小=2  -  浮点数: 3.14
- : float = 3.14
# Inspector.inspect [| 1.11; 2.22; 3.33 |];; (* 浮点数组 *)
....内存块: 大小=6  -  浮点数组:   1.11  2.22  3.33
- : float array = [|1.11; 2.22; 3.33|]

# let add x y = x + y;; 
val add : int -> int -> int = <fun>
# Inspector.inspect add;; (* 函数 *)
....内存块: 大小=1  -  闭包:包含 0 个自由变量
........代码指针: 002EC9A8
- : int -> int -> int = <fun>
# let add1 = add 5;;
val add1 : int -> int = <fun>
# Inspector.inspect add1;; (* 柯里化函数 *)
....内存块: 大小=3  -  闭包:包含 2 个自由变量
........代码指针: 002EC9A4
........内存块: 大小=1  -  闭包:包含 0 个自由变量
............代码指针: 002EC9A8
........直接量 (5)
- : int -> int = <fun>

# type point = {  x : int; y : int};;
type point = { x : int; y : int; }
# Inspector.inspect { x = 600; y = 480 } ;; (* 测试record *)
....内存块: 大小=2  -  structured block (tag = 0):
........直接量 (600)
........直接量 (480)
- : point = {x = 600; y = 480}

这就是所有的OCaml数据类型了,与C做一下比较: char, bool, int都作为int直接量,与C的char, bool, int相似;Tag为0的 块与C的数组相似; OCaml中的浮点数使用块来实现,不过专门提供了Double_tag作 优化.string也有一个专门的Tag表示,函数也是数据类型的一种, OCaml中的用户自定义类型(record)是使用array实现的. Abstract类型用 来表示只知道这个类型,但不知道其具体实现,用于在接口中隐藏类型的实现.

C中表示OCaml数据类型

下面讲解OCaml数据类型如何编码为value类型.

原子(Atomic)类型

Caml类型 编码方式
int Unboxed整数
char Unboxed整数(ASCII码)
float tag为Double_tag的块
string tag为String_tag的块
int32 tag为Custom_tag的块
int64 tag为Custom_tag的块
nativeint tag为Custom_tag的块

Tuples和records

tuples为指向块的指针,使用tag 0. records也是tag为0的块.record类型中字段的顺序决定了record的布局:第 一个声明的字段存储在块中的字段0处,与字段关联的值存放在字段1中.

为了优化,所有字段都为float类型的record作为一个浮点数数组表示,使用 Double_array_tag.

数组(arrays)

整数或指针数组表示为tuples,一个指向tag为0的块的指针. 它们使用 Field宏进行访问,caml_modify函数修改.

浮点数数组(float array 类型)有一个特殊的,未封装的,更有效率的表 示.这些数组表示为指向tag为
Double_array_tag
的块的指针.通过 Double_fieldStore_double_field 进行访问和修改.

具体(Concrete)数据类型

具体项 表示
() Val_int(0)
false Val_int(0)
true Val_int(1)
[] Val_int(0)
h::t tag=0,size=2的块;第一个字段包含h,第二个字段为t

caml/mlvalues.h定义了宏 Val_unit, Val_false,
Val_true
表示(), false和true.

可以使用inspector查看这些值:

# Inspector.inspect ();;
....直接量 (0)
- : unit = ()
# Inspector.inspect false;;
....直接量 (0)
- : bool = false
# Inspector.inspect true;;
....直接量 (1)
- : bool = true
# Inspector.inspect [];;
....直接量 (0)
- : 'a list = []
# Inspector.inspect (1::[]);;
....内存块: 大小=2  -  structured block (tag = 0):
........直接量 (1)
........直接量 (0)
- : int list = [1]

C中操作Ocaml values

类型测试

  • Is_long(v) 如果v是整数直接量返回true,否则为false
  • Is_block(v) 如果v指向一个块则返回true,否则返回false

操作整数

  • Val_long(l) 转换C long int l到OCaml value
  • Val_int(i) 转换 C int i到OCaml value
  • Val_bool(x) 转换C整数x到OCaml bool value
  • Long_val(v) 转换OCaml v到C long int
  • Int_Val(v) 转换OCaml v到C int
  • Bool_Val(v) 如果OCaml bool v的值为false返回0, true返回1
  • Value_true, Value_false表示OCaml bool true和false

访问块

  • Wosize_val(v) 返回块v的size(以word为单位),不包含header.
  • Tag_val(v) 返回块v的tag
  • Field(v, n) 返回结构化块v第n个字段的value.字段的序号从0到 Wosize_val(v)-1
  • Store_field(b, n, v) 存储value v到value b的第n个字段中,b必须为 结构化块.
  • Code_val(v) 返回闭包v的代码部分
  • caml_string_length(v) 返回string v的长度
  • Byte(v, n) 返回string v的第n个字符,类型为 char.
  • Byte_u(v, n) 返回string v的第n个字符,类型为 unsigend char.
  • String_val(v) 返回指向string v的第一个字符的指针,类型为 char*. 这个指针是一个有效的C字符串:字符串的最后一个字符为空字 符null.但是OCaml字符串中可以包含空字符,这会让大多数操作字符串的 C函数失败.
  • Double_val(v) 返回一个浮点数,类型为 double
  • Double_field(v, n) 返回浮点数组v(tag为Double_array_tag的块)的第 n个元素.
  • Store_double_field(v, n, d) 存储双精度浮点数d到浮点数组v的第n个 元素中.
  • Data_custom_val(v) 返回一个指向自定义块v的数据部分的指针.指针的 类型为 void *,需要转换为自定义块包含的类型.
  • Int32_val(v) 返回 int32 v 包含的32位整数
  • Int64_val(v) 返回 int64 v 包含的64位整数
  • Nativeint_val(v) 返回 nativeint v 包含的长整型

表达式Field(v, n), Byte(v, n)和Byte_u(v, n)是一个有效的左值.因此 它们可以通过赋值修改value v中的值. 赋值给Field(v, n)时要注意避免垃圾 收集器产生问题.

分配OCaml块

以下函数在OCaml堆上分配内存.

简单接口

  • Atom(t) 返回一个tag为t的"atom"(大小为0的块).
  • caml_alloc(n, t) 返回一个tag为t,大小为n的块.如果t小于 No_scan_tag,为了满足GC的约束,块中的字段被初始化为有效的value.
  • caml_alloc_tuple(n) 返回一个n个字的块,tag为0
  • caml_alloc_string(n) 返回一个长度为n的字符串value.字符串初始化 为垃圾数据.
  • caml_copy_string(s) 返回一个null结束的C字符串(char *)的拷贝
  • caml_copy_double(d) 返回一个值为 double d的浮点value
  • caml_copy_int32(i), copy_int64(i)和caml_copy_nativeint(i)返回一 个OCaml类型为
    int32
    , int64nativeint 的value,分别使用 整数i初始化.
  • caml_alloc_array(f, a) 分配一个value数组,在输入数组a的每个元素 上调用函数f生成的value(类似OCaml中的map). 数组a是一个以空指针 结尾的指针数组. (不要使用这个函数构造浮点数组)
  • caml_copy_string_array(p) 分配一个字符串数组,从字符串数组指针 p(char **)复制.p必须以NULL结尾.

底层接口

以下函数比caml_alloc更有效率,但更难使用.

从分配函数的观点来看,块可以按照大小分为:大小为0的块,小块(size小于等于 Max_young_wosize),大块(size大于Max_young_wosize).

  • caml_alloc_small(n, t) 返回一个大小为n(单位为word,n <= Max_young_wosize),tag为t的小块.如果这个块是一个结构化块 (t <No_scan_tag),那么这个块的字段(分配后包含的是垃圾) 必须在下次分配前初始化(在块的字段上直接赋值)为合法的value.
  • caml_alloc_shr(n, t) 返回一个大小为n, tag为t的块.大小可以大于 Max_young_wosize.(也可以小于它,但是它没有caml_alloc_small有效 率.) 如果是一个结构化块,必须在下次分配前使用合法的value进行初始化 (使用caml_initialize函数).

为什么结构化的块需要在下次分配前初始化呢?前面已经讲过,结构化的块会被 垃圾收集器遍历每个字段.当进行分配内存的时候,有可能触发GC,如果没有 初始化,GC就会访问垃圾数据,造成错误.

引发异常

两个引发标准异常的函数:

  • caml_failwith(s) 参数s是一个空字符结尾的C字符串(类型为char*), 引发 Failure s异常.
  • caml_invalid_argument(s) 引发 Invalid_argument s异常

在C中引发任意异常要复杂一点:异常标识符在OCaml中动态分配,然后注册 给C进行通信,C中获得此异常标识符后,就可以使用下面的函数引发异常:

  • caml_raise_constant(id) 引发没有参数的异常id
  • caml_raise_with_arg(id, v) 引发带有参数value v的异常id
  • caml_raise_with_args(id, n, v) 引发带有参数value v[ 0 ],…,v[n-1]的异常id
  • caml_raise_with_string(id, s) s是一个空字符结尾的C字符串,引发带有参数s的拷贝的异常id.

与垃圾收集器和谐相处

关于OCaml GC的详细信息,参考ocaml-ora ch.9 Garbage Collection

堆中不使用的块被垃圾收集器自动清理.这就需要C代码在操作堆上分配的块时, 进行一些合作.

简单接口

本节描述的所有宏都在 memory.h 中声明.

规则1 一个参数类型或本地变量类型为value的函数必须调用CAMLparam 宏并使用CAMLreturn, CAMLreturn0 或 CAMLreturnT.

有6个 CAMLparam 宏: CAMLparam0CAMLparam5, 分别接受0-5个参数.如果value 类型的函数参数小于等于5个, 直接在这些参数上使用相应的宏.如果超过5个,在前5个参数上使用CAMLparam5,在剩下的参数上使用CAMLxparam 宏.

CAMLreturn 宏用来代替C关键字return.所有x类型为 value
return x
必须使用 CAMLreturn (x) 代替, 或者用 CAMLreturnT (t, x) (t是x的类型);所有不带参数的return 使用CAMLreturn0 代替. 如果你的C函数返回void,必须使用
CAMLreturn0 . 示例:

void foo (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  ...
  CAMLreturn0;
}

规则2 类型为value的局部变量必须使用CAMLlocal宏声明.values数组 使用CAMLlocalN声明.这些宏必须在函数开头使用,不能在嵌套块中.

CAMLlocal1CAMLlocal5 声明和初始化1-5个类型为
value
的局部变量. CAMLlocalN(x, n) 声明和初始化一个类型为 value [n]的局部变量. 如果需要超过5个局部变量,可以多次调用这些宏. 示 例:

value bar (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  CAMLlocal1(result);
  result = caml_alloc (3, 0);
  ...
  CAMLreturn(result);
}

规则3 给结构化块的字段赋值必须使用Store_field宏(一般块)或Store_double_field宏(包含浮点数的数组或records).其它类型的赋值不要使用Store_field和Store_double_field.

Store_field (b, n, v) 存储value v到value b的第n个字段,b 必须为块(Is_block (b)必须为true).

示例:

value bar (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  CAMLlocal1 (result);

  result = caml_alloc (3, 0);
  Store_field (result, 0, v1);
  Store_field (result, 1, v2);
  Store_field (result, 2, v3);

  CAMLreturn (result);
}

规则4 包含values的全局变量必须使用函数 caml_register_global_root注册到垃圾收集器

使用 caml_register_global_root(&v) 注册全局变量v时,要在一个 有效值第一次存储到v之前或之后调用. 在注册和存储value之间不要 调用任何OCaml runtime函数或宏.

一个注册过的全局变量v可以调用 caml_remove_global_root(&v) 反注 册.

如果全局变量v的内容注册之后不会修改,可以使用性能更好的 caml_register_generational_global_root(&v)caml_remove_generational_global_root(&v) 进行注册和反注册. 如果需要这册许多全局变量,这能改善性能.

底层接口

现在讲解底层分配函数caml_alloc_small和caml_alloc_shr的相关规则.如 果你只使用caml_alloc则可以忽略这部分.

规则5 在使用底层函数分配一个结构化块(块的tag小于No_scan_tag)之后,这个块的所有字段在下次分配操作之前必须包含有效值.如果这个块使 用caml_alloc_small分配,可以对块的字段进行直接赋值:

Field(v, n) = vn;

如果使用caml_alloc_shr分配块,使用caml_initialize函数填充:

caml_initialize(&Field(v, n), vn);

下一次分配可能会触发垃圾收集器.垃圾收集器假定所有的结构化块包含 有效的value.新创建的块包含随机数据,一般不能表示为有效value.

如果你需要在字段获得它们的值之前进行分配操作,首先使用一个常量 value进行初始化(如 Val_unit),然后分配,然后修改字段为正确的 value.

规则6 对块的字段进行赋值,例如

Field(v, n) = w;

只有在块v是最后一个使用 caml_alloc_small 分配的时候才安全;也就是说在分配v和对v的字段进行赋值之间没有其它的分配操作.其它情况下,不要直接赋值.如果一个块使用caml_alloc_shr分配,用caml_initialize进行第一次赋值.

其它情况下,你是更新一个已经包含有效值的字段;这时,使用 caml_modify 函数:

caml_modify(&Field(v, n), w);

为了展示以上规则,这个C函数构造并返回一个列表,包含参数指定的两个 整数.首先,我们使用简单分配函数:

//注意OCaml中列表的内存表示,可以使用前面的inspector程序查看
value alloc_list_int (int i1, int i2)
{
   CAMLparam0 ();
   CAMLlocal2 (result, r);

   r = caml_alloc(2, 0);
   Store_field(r, 0, Val_int(i2));
   Store_field(r, 1, Val_int(0));
   result = caml_alloc(2, 0);
   Store_field(result, 0, Val_int(i1));
   Store_field(result, 1, r);

   CAMLreturn (result);
}

下面使用底层分配函数 caml_alloc_small :

value alloc_list_int (int i1, int i2)
{
   CAMLparam0 ();
   CAMLlocal2 (result, r);

   r = caml_alloc_small(2, 0);
   Field(r, 0) = Val_int(i2);
   Field(r, 1) = Val_int(0);
   result = caml_alloc_small(2, 0);
   Field(result, 0) = Val_int(i1);
   Field(result, 1) = r

   CAMLreturn (result);
}

前两个例子中,列表自底向上构造.这里使用另一种方法,自顶向下. 它的效率比较低,但是展示了 caml_modify 的用法.

value alloc_list_int (int i1, int i2)
{
   CAMLparam0 ();
   CAMLlocal2 (tail, r);

   r = caml_alloc_small(2, 0);
   Field(r, 0) = Val_int(i1);
   Field(r, 1) = Val_int(0);
   tail = caml_alloc_small(2, 0);
Field(tail, 0) = Val_int(i2);
   Field(tail, 1) = Val_int(0);
   caml_modify(&Field(r, 1), tail);

   CAMLreturn (result);
}

从C到OCaml的回调

至此,我们讲解了如何从OCaml中调用C函数.这一节,我们展示C函数如何调用 OCaml函数,包括回调(OCaml调用C,C又调用OCaml)和C作为主程序.

从C中调用OCaml闭包

OCaml中调用函数称为应用,因为函数就是表达式,调用函数,就是把表达式 应用到参数上. 比如计算一个数的平方用数学方式来描述:一个数字 x的平方就是x * x,提供x的值,套用这个公式就能计算出结果. 用 OCaml描述:let square x = x * x ,这个表达式叫做let binding. square只是x * x的别名(调用square 2也可以用(fun x -> x * x) 2), square 2就是把x * x应用到2上.

C函数可以应用OCaml函数式value(闭包)到OCaml values.以下函数提供 调用方法:

  • caml_callback(f, a) 应用函数式value f到value a, 返回f返回的value.
  • caml_callback2(f, a, b) 应用函数式value f到a和b
  • caml_callback3(f, a, b, c) 应用函数式value f到a, b, c
  • caml_callbackN(f, n, args) 应用函数式value f到包含n个参数的 value数组args

如果函数f没有返回,而引发了一个异常,这个异常会传播到下个OCaml代码, 跳过C代码.也就是说,如果一个OCaml函数f调用C函数g,回调一个OCaml函数 h,h引发一个异常,这时g的执行中断,异常被传递回f.

如果C代码希望捕获异常,它可以调用caml_callback_exn, caml_callback2_exn,caml_callback3_exn,caml_callbackN_exn. 这些函数接受的参数与不带_exn的函数相同,但会捕获异常并返回到C代 码.caml_callback*_exn函数返回的value v必须使用Is_exception_result(v) 测试.返回"false"则没有异常,value v 是OCaml函数返回的结果.如果返回"true",则有异常,它(异常描述符)的value可以使用Extract_exception(v)
获得.

注册OCaml闭包给C函数

callback 函数的主要问题在于获得一个要调用的OCaml函数的闭包. 为此,OCaml提供了一个简单的注册机制,OCaml代码可以注册OCaml函数到一 个全局名字,然后C代码可以通过这个全局名字获得相应的闭包.

在OCaml中,通过使用 Callback.register n v注册.n是一个全局名字(任 意字符串),v是一个OCaml value.例如:

let f x = print_string " f is applied to "; print_int x; print_newline()
let _ = Callback.register "test function" f

在C中,使用 caml_named_value (n)获得对应value的指针. 如果名字n 没有对应的value,返回一个空指针. 例如:下面的C包装器调用了上面的 OCaml函数f:

void call_caml_f(int arg)
{
  caml_callback(*caml_named_value("test function"), Val_int(arg));
}

caml_named_value 返回的指针是固定的,因此可以安全地使用C变量进行 缓存,避免重复的名字查找.另一方面,指针指向的value可以在垃圾收集时 修改,因此在使用指针时必须重新计算. 下面是一个更有效率的包装方式:

void call_caml_f(int arg)
{
  static value *closure_f = NULL;
  if (closure_f == NULL) {
    closure_f = caml_named_value("test function");
  }
  caml_callback(*closure_f, Value_int(arg));
}

注册OCaml异常给C函数

上面的注册机制也可以用来实现从OCaml到C中的异常标识符通信. OCaml代码使用 Callback.register_exception n exn,注册异常. 例如:

exception Error of string
let _ = Callback.register_exception "test exception" (Error "any string")

C代码使用 caml_named_value 获得异常标识符, 然后将它作为 raise_constant,raise_with_arg, 和raise_with_string 的参数引发异常. 例如,下面的C函数使用给定的参数引发Error 异常:

void raise_error(char *msg)
{
  caml_raise_with_string(*caml_named_value("test exception", msg);
}

C作为主程序

一般情况下,混合OCaml和C的程序依靠执行OCaml的初始化代码启动.然后可 能会调用C代码.我们说主程序为OCaml代码.在一些程序中,需要使用C代码 来完成主程序的角色,当需要时调用OCaml函数. 这可以通过以下步骤完 成:

  • 程序的C部分必须提供一个 main 函数,它将覆盖OCaml运行时提供的 main 函数.将和一般的C程序一样从用户定义的main 函数执行.
  • 在某个地方,C代码必须调用 caml_main(argv) 来初始化OCaml代码. argv 参数是一个以NULL结尾的C字符串数组(类型char **),它表示 命令行参数,和传递给main 的第二个参数一样.OCaml数组
    Sys.argv 将从这个参数初始化. 在字节码编译器中,argv[ 0]
    argv[ 1]
    也用来查找包含字节码的文件.
  • 调用 caml_main 初始化OCaml运行时系统,加载字节码(在字节码编 译的情况下),并执行OCaml程序的初始化代码.一般来说,这些初始化代 码使用Camlback.register 注册回调函数.一旦OCaml初始化代码完 成,将返回到调用caml_main 的C代码中继续执行.
  • 这时C代码可以通过回调机制调用OCaml函数.

嵌入OCaml代码到C代码中

字节码编译器在自定义运行时(custom runtime)模式中(ocamlc -custom)只是添加字节码到 包含自定义运行时的可执行文件中. 这有两个步骤.首先,最终链接必须通 过ocamlc 完成. 第二,OCaml运行时必须能通过命令行参数找到可执行 文件的名字.当使用caml_main(argv) 时,argv[ 0]
argv[ 1] 必须包含可执行文件的名字.

另一个嵌入字节码到C代码的方法是使用 ocamlc-output-obj 选 项.它告诉ocamlc 编译器输出一个包含OCaml字节码的C对象文件 (.o 文件, windows下为.obj), 和caml_startup 函数. 这个C对象文件可 以使用标准C编译器链接,或存储进一个C library中.

caml_startup 函数必须从主C程序中调用,为了初始化OCaml运行时和执 行OCaml初始化代码. 和
caml_main
一样,它接受一个包含命令行参数 的 argv 参数.与 caml_main 不同的是, 这个argv参数只是用来 初始化Sys.argv,不会用来查找可执行文件的名字.

-output-obj 选项也可以用来获得C源代码文件. 另外,它还能直接产生 一个包含OCaml代码、OCaml运行时系统和其它传递给ocamlc 的静态C 代码(.o, .a,或者 .obj, .lib) 的共享库(.so文件, Windows下为.dll)。 这种方式和一个普通的链接步骤很类似,只不过它产生一个共享库,可 以在需要的时候运行OCaml代码。-output-obj 这三种行为通过 输出文件的扩展名来选择(使用-o)。

本地编译器 ocamlopt 也支持 -output-obj 选项, 可以输出一个C 对象文件或一个包含命令行上指定的所有OCaml模块的本地代码的共享库 和OCaml起始代码。和字节码

抱歉!评论已关闭.