第 22 章 使用 C 语言与 OCaml 交互

本章介绍如何将用 C 语言编写的用户定义原语与 OCaml 代码链接起来,并从 OCaml 函数中调用它们,以及这些 C 函数如何回调到 OCaml 代码。

1 概述和编译信息

1.1 声明原语

定义::= ...
 externalvalue-name:typexpr=external-declaration
 
external-declaration::=string-literal [ string-literal [ string-literal ] ]

用户原语在实现文件或 structend 模块表达式中使用 external 关键字声明

        external name : type = C-function-name

这将值名称 name 定义为一个类型为 type 的函数,通过调用给定的 C 函数来执行。例如,以下是标准库模块 Stdlib 中声明 seek_in 原语的方式

        external seek_in : in_channel -> int -> unit = "caml_ml_seek_in"

具有多个参数的原语始终是柯里化的。C 函数的名称不必与 ML 函数相同。

因此定义的外部函数可以在接口文件或 sigend 签名中指定为普通值

        val name : type

从而隐藏它们作为 C 函数的实现,或明确地指定为“显式”外部函数

        external name : type = C-function-name

后者效率稍高,因为它允许模块的客户端直接调用 C 函数,而不是通过相应的 OCaml 函数。另一方面,如果库模块在顶层具有副作用,则不应在库模块中使用它,因为这种直接调用会干扰链接器在链接时从库中删除未用模块的算法。

原语的元数(参数数量)是根据其在 external 声明中的 OCaml 类型自动确定的,方法是计算类型中函数箭头的数量。例如,上面的 seek_in 的元数为 2,并且调用 caml_ml_seek_in C 函数时传递了两个参数。同样地,

    external seek_in_pair: in_channel * int -> unit = "caml_ml_seek_in_pair"

的元数为 1,并且 caml_ml_seek_in_pair C 函数接收一个参数(它是一个 OCaml 值对)。

在确定原语的元数时,不会展开类型缩写。例如,

        type int_endo = int -> int
        external f : int_endo -> int_endo = "f"
        external g : (int -> int) -> (int -> int) = "f"

f 的元数为 1,但 g 的元数为 2。这允许原语返回一个函数值(如上面的 f 示例中):只需记住在类型缩写中命名函数返回值类型即可。

该语言接受外部声明,除了 C 函数的名称外,还包含一个或两个标志字符串。这些标志保留用于标准库的实现。

1.2 实现原语

元数为 n ≤ 5 的用户原语由 C 函数实现,这些函数接受 n 个类型为 value 的参数,并返回类型为 value 的结果。类型 value 是 OCaml 值表示的类型。它将几种基本类型的对象(整数、浮点数、字符串、 ‍…) 以及 OCaml 数据结构编码。类型 value 以及相关的转换函数和宏将在下面详细介绍。例如,以下是实现 In_channel.input 原语的 C 函数的声明,该原语接受 4 个参数

CAMLprim value input(value channel, value buffer, value offset, value length)
{
  ...
}

当原语函数在 OCaml 程序中应用时,将调用 C 函数,并将原语应用到的表达式的值作为参数传递给它。函数返回的值将作为函数应用的结果传递回 OCaml 程序。

元数大于 5 的用户原语应由两个 C 函数实现。第一个函数与字节码编译器 ocamlc 一起使用,接收两个参数:指向 OCaml 值数组的指针(参数值),以及提供的参数数量的整数。另一个函数与原生代码编译器 ocamlopt 一起使用,直接获取其参数。例如,以下是 7 个参数原语 Nat.add_nat 的两个 C 函数

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]);
}

两个 C 函数的名称必须在原语声明中给出,如下所示

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

例如,对于 add_nat,声明如下

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

实现用户原语实际上是两个独立的任务:一方面,解码参数以从给定的 OCaml 值中提取 C 值,并将返回值编码为 OCaml 值;另一方面,从参数实际计算结果。除了非常简单的原语外,通常最好使用两个不同的 C 函数来实现这两个任务。第一个函数实际实现原语,接受原生 C 值作为参数,并返回一个原生 C 值。第二个函数通常称为“桩代码”,是对第一个函数的简单包装,它将参数从 OCaml 值转换为 C 值,调用第一个函数,并将返回的 C 值转换为 OCaml 值。例如,以下是 Int64.float_of_bits 原语的桩代码

CAMLprim value caml_int64_float_of_bits(value vi)
{
  return caml_copy_double(caml_int64_float_of_bits_unboxed(Int64_val(vi)));
}

(在这里,caml_copy_doubleInt64_val 是类型 value 的转换函数和宏,将在后面介绍。宏 CAMLprim 展开为必要的编译器指令,以确保该函数被导出并可从 OCaml 访问。) 繁重的工作由函数 caml_int64_float_of_bits_unboxed 完成,该函数声明如下

double caml_int64_float_of_bits_unboxed(int64_t i)
{
  ...
}

要编写对 OCaml 值进行操作的 C 代码,请使用以下包含文件

包含文件提供
caml/mlvalues.hvalue 类型的定义以及转换宏
caml/alloc.h分配函数(用于创建结构化的 OCaml 对象)
caml/memory.h各种与内存相关的函数和宏(用于 GC 接口、结构的原地修改等)。
caml/fail.h用于引发异常的函数(参见第 ‍22.4.5 节)
caml/callback.h从 C 到 OCaml 的回调(参见第 ‍22.7 节)。
caml/custom.h对自定义块的操作(参见第 ‍22.9 节)。
caml/intext.h用于为自定义块编写用户定义的序列化和反序列化函数的操作(参见第 ‍22.9 节)。
caml/threads.h用于在存在多个线程的情况下进行接口操作的操作(参见第 ‍22.12 节)。

这些文件位于 OCaml 标准库目录的 caml/ 子目录中,该目录由命令 ocamlc -where 返回(通常是 /usr/local/lib/ocaml/usr/lib/ocaml)。

OCaml 运行时系统包含三个主要部分:字节码解释器、内存管理器,以及实现基本操作的一组 C 函数。提供了一些字节码指令来调用这些 C 函数,这些函数由它们在函数表(基本操作表)中的偏移量指定。

在默认模式下,OCaml 链接器会为标准运行时系统生成字节码,并提供一组标准基本操作。对不在此标准集中基本操作的引用会导致“不可用的 C 原语”错误。(除非支持 C 库的动态加载 - 参见下面第 ‍22.1.4 节。)

在“自定义运行时”模式下,OCaml 链接器会扫描目标文件并确定所需的基本操作集。然后,它通过以下方式构建合适的运行时系统:

这将构建一个包含所需基本操作的运行时系统。OCaml 链接器会为这个自定义运行时系统生成字节码。字节码被追加到自定义运行时系统的末尾,以便在输出文件(自定义运行时 + 字节码)启动时自动执行它。

要以“自定义运行时”模式进行链接,请使用以下方式执行 ocamlc 命令:

如果您使用的是原生代码编译器 ocamlopt,则不需要 -custom 标志,因为 ocamlopt 的最终链接阶段总是构建一个独立的可执行文件。要构建一个混合的 OCaml/C 可执行文件,请使用以下命令执行 ocamlopt 命令:

从 Objective Caml 3.00 开始,可以将 -custom 选项以及 C 库的名称记录在 OCaml 库文件 .cma.cmxa 中。例如,考虑一个 OCaml 库 mylib.cma,它由 OCaml 对象文件 a.cmob.cmo 构建,它们引用 libmylib.a 中的 C 代码。如果库构建如下:

        ocamlc -a -o mylib.cma -custom a.cmo b.cmo -cclib -lmylib

库的用户只需链接 mylib.cma

        ocamlc -o myprog mylib.cma ...

系统将自动添加 -custom-cclib -lmylib 选项,实现与以下相同的效果:

        ocamlc -o myprog -custom a.cmo b.cmo ... -cclib -lmylib

当然,另一种方法是不使用额外选项构建库

        ocamlc -a -o mylib.cma a.cmo b.cmo

然后要求用户在链接时自己提供 -custom-cclib -lmylib 选项

        ocamlc -o myprog -custom mylib.cma ... -cclib -lmylib

然而,对于库的最终用户来说,前一种方法更方便。

从 Objective Caml 3.03 开始,提供了一种使用 -custom 代码静态链接 C 代码的替代方法。在这种模式下,OCaml 链接器生成一个纯字节码可执行文件(没有嵌入的自定义运行时系统),它只记录包含 C 代码的动态加载库的名称。然后,标准 OCaml 运行时系统 ocamlrun 动态加载这些库,并在执行字节码之前解析对所需基本操作的引用。

此功能目前适用于除 Cygwin 64 位之外的所有 OCaml 支持的平台。

要使用 OCaml 代码动态链接 C 代码,C 代码必须首先编译成共享库(在 Unix 下)或 DLL(在 Windows 下)。这涉及 1- 使用适当的 C 编译器标志编译 C 文件以生成位置无关代码(如果操作系统需要),以及 2- 从生成的​​对象文件构建共享库。生成的共享库或 DLL 文件必须安装在 ocamlrun 可以在程序启动时找到它的位置(参见第 ‍15.3 节)。最后(步骤 3),使用以下命令执行 ocamlc 命令:

不要设置 -custom 标志,否则您将回到第 ‍22.1.3 节中描述的静态链接。 ocamlmklib 工具(参见第 ‍22.14 节)自动执行步骤 2 和 3。

与静态链接一样,也可以(并且建议)在 OCaml .cma 库存档中记录 C 库的名称。再次考虑一个 OCaml 库 mylib.cma,它由 OCaml 对象文件 a.cmob.cmo 构建,它们引用 dllmylib.so 中的 C 代码。如果库构建如下:

        ocamlc -a -o mylib.cma a.cmo b.cmo -dllib -lmylib

库的用户只需链接 mylib.cma

        ocamlc -o myprog mylib.cma ...

系统将自动添加 -dllib -lmylib 选项,实现与以下相同的效果:

        ocamlc -o myprog a.cmo b.cmo ... -dllib -lmylib

使用这种机制,mylib.cma 库的用户不需要知道它引用了 C 代码,也不需要知道此 C 代码是静态链接的(使用 -custom)还是动态链接的。

1.5 选择静态链接和动态链接

在描述了两种使用 OCaml 代码链接 C 代码的不同方法后,现在我们将回顾每种方法的优缺点,以帮助混合 OCaml/C 库的开发人员做出决定。

动态链接的主要优点是它保留了字节码可执行文件的平台独立性。也就是说,字节码可执行文件不包含任何机器代码,因此可以在平台 A 上编译并在其他平台 BC、… 上执行,只要这些平台都提供了所需的共享库。相反,由 ocamlc -custom 生成的可执行文件只能在创建它们的平台上运行,因为它们包含特定于该平台的定制运行时系统。此外,动态链接会导致更小的可执行文件。

动态链接的另一个优点是库的最终用户不需要在他们的机器上安装 C 编译器、C 链接器和 C 运行时库。这在 Unix 和 Cygwin 下不是什么大问题,但许多 Windows 用户不愿意安装 Microsoft Visual C,仅仅是为了能够执行 ocamlc -custom

动态链接有两个缺点。第一个是生成的可执行文件不是独立的:它需要共享库以及 ocamlrun 安装在执行代码的机器上。如果您希望分发一个独立的可执行文件,最好使用 ocamlc -custom -ccopt -staticocamlopt -ccopt -static 静态链接它。动态链接还会引发“DLL 地狱”问题:必须注意确保在启动时找到正确版本的共享库。

动态链接的第二个缺点是它使库的构建变得复杂。用于编译位置无关代码和构建共享库的 C 编译器和链接器标志在不同的 Unix 系统之间差异很大。此外,动态链接并非所有 Unix 系统都支持,需要在库的 Makefile 中提供一个回退到静态链接的情况。 ocamlmklib 命令(参见第 ‍22.14 节)尝试隐藏其中一些系统依赖性。

总之:在原生 Windows 端口下,强烈建议使用动态链接,因为没有可移植性问题,并且对于最终用户来说方便得多。在 Unix 下,对于成熟的、常用的库,应该考虑使用动态链接,因为它可以提高字节码可执行文件的平台独立性。对于新的或很少使用的库,静态链接在可移植性方面更容易设置。

1.6 构建独立的自定义运行时系统

每次使用 C 库链接 OCaml 代码时构建一个自定义运行时系统有时很不方便,就像 ocamlc -custom 所做的那样。一方面,在某些系统(链接器性能不好或远程文件系统速度慢)上构建运行时系统速度很慢;另一方面,字节码文件的平台独立性会丢失,迫使我们为每个感兴趣的平台执行一次 ocamlc -custom 链接。

一种替代 ocamlc -custom 的方法是单独构建一个自定义运行时系统,该系统集成了所需的 C 库,然后生成“纯”字节码可执行文件(不包含自己的运行时系统),这些可执行文件可以在此自定义运行时系统上运行。这是通过 -make-runtime-use-runtime 标志实现的。例如,要构建一个集成“Unix”和“Threads”库的 C 部分的自定义运行时系统,请执行以下操作:

        ocamlc -make-runtime -o /home/me/ocamlunixrun unix.cma threads.cma

要生成在该运行时系统上运行的字节码可执行文件,请执行以下操作:

        ocamlc -use-runtime /home/me/ocamlunixrun -o myprog \
                unix.cma threads.cma your .cmo and .cma files

然后,可以像往常一样启动字节码可执行文件 myprogmyprog args/home/me/ocamlunixrun myprog args

请注意,字节码库 unix.cmathreads.cma 必须给出两次:在构建运行时系统时(以便 ocamlc 知道需要哪些 C 基本操作)以及在构建字节码可执行文件时(以便实际链接 unix.cmathreads.cma 中的字节码)。

2 value 类型

所有 OCaml 对象都由 C 类型 value 表示,该类型在包含文件 caml/mlvalues.h 中定义,以及用于操作该类型的值的宏。类型为 value 的对象可以是:

2.1 整数

整数类型表示 63 位有符号整数(在 32 位架构上为 31 位)。它们是无盒的(未分配内存)。

2.2

堆中的块由垃圾回收器管理,因此具有严格的结构约束。每个块包含一个头,其中包含块的大小(以字为单位)和块的标签。标签控制块内容的结构方式。小于 No_scan_tag 的标签表示结构化块,包含格式良好的值,垃圾回收器会递归地遍历这些值。大于或等于 No_scan_tag 的标签表示原始块,其内容不会被垃圾回收器扫描。为了方便使用诸如相等性和结构化输入输出之类的特定多态原语,结构化块和原始块根据其标签进一步分类如下

标签块的内容
0 到 No_scan_tag−1结构化块(OCaml 对象的数组)。每个字段都是一个 value
Closure_tag表示函数值的闭包。第一个字是代码片段的指针,其余字是 value,包含环境。
String_tag字符字符串或字节序列。
Double_tag双精度浮点数。
Double_array_tag双精度浮点数的数组或记录。
Abstract_tag表示抽象数据类型的块。
Custom_tag表示抽象数据类型的块,该数据类型附加了用户定义的终结、比较、哈希、序列化和反序列化函数。

2.3 堆外的指针

在早期版本的 OCaml 中,可以通过将指针强制转换为 value 类型,来使用对堆外地址的字对齐指针作为 OCaml 值。这种用法在 OCaml 5.0 之后不再支持。

从 OCaml 中操作指向堆外块的指针的正确方法是,将这些指针存储在带有标签 Abstract_tagCustom_tag 的 OCaml 块中,然后将这些块用作 OCaml 值。

以下是如何在 Abstract_tag 块中封装 C 类型为 ty * 的堆外指针的示例。第 ‍22.6 节提供了一个使用 Custom_tag 块的更完整的示例。

/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
  value v = caml_alloc(1, Abstract_tag);
  *((ty **) Data_abstract_val(v)) = p;
  return v;
}

/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
  return *((ty **) Data_abstract_val(v));
}

或者,可以将堆外指针视为“本机”整数,即在 32 位平台上是带盒子的 32 位整数,在 64 位平台上是带盒子的 64 位整数。

/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
  return caml_copy_nativeint((intnat) p);
}

/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
  return (ty *) Nativeint_val(v);
}

对于至少 2 对齐的指针(最低位保证为零),我们还有另一种有效的表示形式,即 OCaml 带标签的整数。

/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
  assert (((uintptr_t) p & 1) == 0);  /* check correct alignment */
  return (value) p | 1;
}

/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
  return (ty *) (v & ~1);
}

3 OCaml 数据类型的表示

本节介绍如何将 OCaml 数据类型编码到 value 类型中。

3.1 原子类型

OCaml 类型编码
int无盒整数。
char无盒整数(ASCII 码)。
float带有标签 Double_tag 的块。
bytes带有标签 String_tag 的块。
string带有标签 String_tag 的块。
int32带有标签 Custom_tag 的块。
int64带有标签 Custom_tag 的块。
nativeint带有标签 Custom_tag 的块。

3.2 元组和记录

元组由指向带有标签 0 的块的指针表示。

记录也由带有标签 0 的块表示。记录类型声明中标签的顺序决定了记录字段的布局:声明为第一个的标签相关联的值存储在块的字段 0 中,与第二个标签相关联的值存储在字段 1 中,依此类推。

作为优化,所有字段都具有静态类型 float 的记录表示为浮点数数组,带有标签 Double_array_tag。(请参阅以下有关数组的部分。)

作为另一个优化,不可拆箱记录类型是特殊表示的;不可拆箱记录类型是仅具有一个字段的不可变记录类型。不可拆箱类型将通过两种方式之一表示:带盒子的或无盒子的。带盒子的记录类型如上所述表示(通过带有标签 0 或 Double_array_tag 的块)。无盒子的记录类型直接由其字段的值表示(即,没有块来表示记录本身)。

表示方式的优先级从高到低,如下所示:

3.3 数组

整数和指针数组表示方式与元组和记录相同,即作为指向带有标签 0 的块的指针。它们使用 Field 宏进行读取,使用 caml_modify 函数进行写入。

类型为 floatarray 的值(由 Float.Array 模块操作),以及其声明仅包含 float 字段的记录,使用一种高效的无盒子表示方式:带有标签 Double_array_tag 的块,其内容由原始双精度值组成,这些值本身不是有效的 OCaml 值。它们应该使用 Double_flat_fieldStore_double_flat_field 宏进行访问。

最后,类型为 float array 的数组可以使用带盒子或无盒子表示方式,具体取决于编译器的配置方式。它们目前默认使用无盒子表示方式,但可以通过将 --disable-flat-float-array 标志传递给“configure”脚本,使其使用带盒子表示方式。它们应该使用 Double_array_fieldStore_double_array_field 宏进行访问,这些宏在两种模式下都能正常工作。

3.4 具体数据类型

构造项由无盒整数表示(用于常量构造函数),或由其标签编码构造函数的块表示(用于非常量构造函数)。给定具体类型的所有常量构造函数和非常量构造函数分别从 0 开始编号,顺序与它们在具体类型声明中出现的顺序一致。常量构造函数由等于其构造函数编号的无盒整数表示。用 n 个参数声明的非常量构造函数由大小为 n、带有构造函数编号的标签的块表示;n 个字段包含其参数。示例

构造项表示
()Val_int(0)
falseVal_int(0)
trueVal_int(1)
[]Val_int(0)
h::t大小为 2、标签为 0 的块;第一个字段包含 h,第二个字段包含 t

为了方便起见,caml/mlvalues.h 定义了宏 Val_unitVal_falseVal_trueVal_emptylist,分别引用 ()falsetrue[]

以下示例说明了如何为构造函数分配整数和块标签

type t =
  | A             (* First constant constructor -> integer "Val_int(0)" *)
  | B of string   (* First non-constant constructor -> block with tag 0 *)
  | C             (* Second constant constructor -> integer "Val_int(1)" *)
  | D of bool     (* Second non-constant constructor -> block with tag 1 *)
  | E of t * t    (* Third non-constant constructor -> block with tag 2 *)

作为优化,不可拆箱具体数据类型是特殊表示的;如果具体数据类型只有一个构造函数,而这个构造函数只有一个参数,则该数据类型是不可拆箱的。不可拆箱具体数据类型表示方式与不可拆箱记录类型相同:请参阅第 ‍22.3.2 节中的说明。

3.5 对象

对象由带有标签 Object_tag 的块表示。块的第一个字段引用对象的类和关联的方法集,格式无法从 C 中轻松利用。第二个字段包含唯一的对象 ID,用于比较。对象的剩余字段包含对象实例变量的值。直接访问实例变量是不安全的,因为类型系统没有保证对象中包含的实例变量。

可以使用 C 函数 caml_get_public_method(在 <caml/mlvalues.h> 中声明)从对象中提取公共方法。由于公共方法标签的哈希方式与变体标签相同,并且方法是将 self 作为第一个参数的函数,因此如果要从 C 侧进行方法调用 foo#bar,则应该调用

  callback(caml_get_public_method(foo, hash_variant("bar")), foo);

3.6 多态变体

与构造的术语类似,多态变体值以整数(对于没有参数的多态变体)或块(对于带参数的多态变体)表示。与构造的术语不同,变体构造函数不是从 0 开始编号的,而是由哈希值(OCaml 整数)标识,由 C 函数 hash_variant(在 <caml/mlvalues.h> 中声明)计算:例如,名为 VConstr 的变体构造函数的哈希值为 hash_variant("VConstr")

变体值 `VConstrhash_variant("VConstr") 表示。变体值 `VConstr(v) 由大小为 2 且标记为 0 的块表示,字段编号 0 包含 hash_variant("VConstr"),字段编号 1 包含 v

与构造的值不同,带有多个参数的多态变体值不会被扁平化。也就是说,`VConstr(v, w) 由大小为 2 的块表示,其字段编号 1 包含对 (v, w) 的表示,而不是大小为 3 的块,在字段 1 和 2 中包含 vw

4 值操作

4.1 种类测试

4.2 整数操作

4.3 访问块

表达式 Field(v, n)Byte(v, n)Byte_u(v, n) 是有效的左值。因此,它们可以被赋值,从而导致对值 v 的就地修改。直接分配给 Field(v, n) 必须谨慎操作,以避免混淆垃圾收集器(见下文)。

4.4 分配块

简单接口

低级接口

以下函数比 caml_alloc 效率略高,但使用起来也更加困难。

从分配函数的角度来看,块根据它们的大小划分为零大小的块、小块(大小小于或等于 Max_young_wosize)和大块(大小大于 Max_young_wosize)。常量 Max_young_wosize 在包含文件 mlvalues.h 中声明。保证它至少为 64(字),因此可以假设任何大小小于或等于 64 的块都是小块。对于在运行时计算大小的块,必须将大小与 Max_young_wosize 进行比较,以确定正确的分配过程。

4.5 抛出异常

提供两个函数来抛出两个标准异常

从 C 抛出任意异常更加微妙:异常标识符是由 OCaml 程序动态分配的,因此必须使用下面第 ‍22.7.3 节中描述的注册机制将其传达给 C 函数。在 C 中恢复异常标识符后,以下函数实际上会抛出异常

5 与垃圾回收器和谐共处

堆中的未使用块会由垃圾回收器自动回收。这需要来自操作堆分配块的 C 代码的一些配合。

5.1 简单接口

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

规则 1 具有类型为 value 的参数或局部变量的函数必须以调用其中一个 CAMLparam 宏开头,并以 CAMLreturnCAMLreturn0CAMLreturnT 结束。

有六个 CAMLparam 宏:CAMLparam0CAMLparam5,分别接受零到五个参数。如果您的函数不超过 5 个类型为 value 的参数,请使用相应的宏并将这些参数作为参数传递。如果您的函数有超过 5 个类型为 value 的参数,请使用 CAMLparam5 作为五个参数,并使用一个或多个对 CAMLxparam 宏的调用来处理其余参数(CAMLxparam1CAMLxparam5)。

CAMLreturnCAMLreturn0CAMLreturnT 用于替换 C 关键字 return;任何在入口处使用 CAMLparam 宏的函数都应该在所有出口点使用 CAMLreturn。作为 OCaml externs 导出的 C 函数必须返回一个 value,并且它们应该使用 CAMLreturn (x) 而不是 return x。一些辅助函数可能操作 OCaml 值,但返回 void 或其他数据类型。返回 void 的过程应该显式地使用 CAMLreturn0,并且没有任何隐式返回。返回某种类型 t 的 C 数据的辅助函数应该使用 CAMLreturnT (t, x) 而不是 return x

注意

某些 C 编译器会对每个 CAMLparamCAMLlocal 的使用发出关于未使用的变量 caml__dummy_xxx 的错误警告。您应该忽略它们。


示例

CAMLprim value my_external (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  ...
  CAMLreturn (Val_unit);
}


static void helper_procedure (value v1, value v2)
{
  CAMLparam2 (v1, v2);
  ...
  CAMLreturn0;
}

static int helper_function (value v1, value v2)
{
  CAMLparam2 (v1, v2);
  ...
  CAMLreturnT (int, 0);
}
注意

如果您的函数是具有超过 5 个参数的原语,用于与字节码运行时一起使用,则其参数不是 value,并且不能声明(它们具有类型 value *int)。

警告

CAMLreturn0 仅应用于返回 void 的内部过程。 CAMLreturn(Val_unit) 应该用于返回 OCaml 单位值的函数。原语(可以从 OCaml 调用的 C 函数)永远不应该返回 void。

规则 2 类型为 value 的局部变量必须使用其中一个 CAMLlocal 宏进行声明。 value 数组使用 CAMLlocalN 声明。这些宏必须在函数开头使用,而不是在嵌套块中使用。

CAMLlocal1CAMLlocal5 声明并初始化一个到五个类型为 value 的局部变量。变量名作为参数传递给宏。 CAMLlocalN(x, n) 声明并初始化一个类型为 value [n] 的局部变量。如果您有超过 5 个局部变量,可以使用对这些宏的多次调用。

示例

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

CAMLlocal(和 CAMLxparam)只能在 CAMLparam 之后调用。如果一个函数声明了局部值,但没有接受任何值参数,则应该以 CAMLparam0 () 开头。

static value foo (int n)
{
  CAMLparam0 ();;
  CAMLlocal (result);
  ...
  CAMLreturn (result);
}
规则 3 对结构化块的字段的赋值必须使用 Store_field 宏(对于普通块)、Store_double_array_field 宏(对于 float array 值)或 Store_double_flat_field(对于 floatarray 值和浮点数记录)完成。其他赋值不能使用 Store_fieldStore_double_array_fieldStore_double_flat_field

Store_field (b, n, v) 将值 v 存储在值 b 的字段号 n 中,它必须是一个块(即 Is_block(b) 必须为真)。

示例

CAMLprim 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);
}
警告

Store_fieldStore_double_field 的第一个参数必须是通过 CAMLparam* 声明的变量或通过 CAMLlocal* 声明的参数,以确保在评估其他参数时触发的垃圾收集不会在第一个参数计算后使其失效。

与 CAMLlocalN 一起使用

使用 CAMLlocalN 声明的值数组不能使用 Store_field 写入。相反,请使用普通的 C 数组语法。

规则 4 包含值的全局变量必须使用 caml_register_global_root 函数在垃圾回收器中注册,除了全局变量和只包含 OCaml 整数(而不是指针)的位置不需要注册。

对于 OCaml 堆之外的任何内存位置,如果它包含一个值并且不能保证从另一个已注册的全局变量或位置、使用 CAMLlocal 声明的局部变量或使用 CAMLparam 声明的函数参数访问,也是如此。只要它包含这样的值,就必须注册。

全局变量 v 的注册是在第一次将有效值存储到 v 之前或之后通过调用 caml_register_global_root(&v) 来完成的;同样,任意位置 p 的注册是通过调用 caml_register_global_root(p) 来完成的。

您不能在注册和存储值之间调用任何 OCaml 运行时函数或宏。您也不能在变量 v(同样,位置 p)中存储任何不是有效值的内容。

注册会导致每次在 OCaml 堆中移动该变量或内存位置中的值时,更新其内容。在存在线程的情况下,必须采取适当的同步措施,以避免在读取或写入值时与垃圾回收器发生竞争条件。(参见第 22.12.2 节。)

已注册的全局变量 v 可以通过调用 caml_remove_global_root(&v) 取消注册。

如果全局变量 v 的内容在注册后很少修改,则通过调用 caml_register_generational_global_root(&v) 来注册 v(在用有效 value 初始化之后,但在任何分配或调用 GC 函数之前),并调用 caml_remove_generational_global_root(&v) 取消注册,可以获得更好的性能。在这种情况下,您不能直接修改 v 的值,但必须使用 caml_modify_generational_global_root(&v,x) 将其设置为 x。垃圾回收器利用 v 在调用 caml_modify_generational_global_root 之间不会被修改的保证来减少扫描次数。如果 v 的修改频率低于次要收集的频率,这将提高性能。

注意

CAML 宏使用以 caml__ 开头的标识符(局部变量、类型标识符、结构标记)。在您的程序中不要使用任何以 caml__ 开头的标识符。

5.2 低级接口

我们现在给出与低级分配函数 caml_alloc_smallcaml_alloc_shr 对应的 GC 规则。如果您坚持使用简化的分配函数 caml_alloc,则可以忽略这些规则。

规则 5 使用低级函数分配结构化块(标记小于 No_scan_tag 的块)后,必须在下一个分配操作之前用有效值填充该块的所有字段。如果该块已使用 caml_alloc_small 分配,则通过直接赋值到块的字段来完成填充:

        Field(v, n) = vn;
如果该块已使用 caml_alloc_shr 分配,则通过 caml_initialize 函数完成填充

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

下一个分配可能会触发垃圾收集。垃圾回收器假设所有结构化块都包含有效值。新创建的块包含随机数据,这些数据通常不代表有效值。

如果您确实需要在字段可以接收最终值之前进行分配,请先用一个常量值(例如 Val_unit)进行初始化,然后进行分配,最后用正确的值修改字段(参见规则 6)。

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

        Field(v, n) = w;
仅当 v 是由 caml_alloc_small 新分配的块时,才安全;也就是说,如果在分配 v 和分配给该字段之间没有进行任何分配。在所有其他情况下,切勿直接赋值。如果块刚刚由 caml_alloc_shr 分配,请使用 caml_initialize 为第一次分配字段赋值

        caml_initialize(&Field(v, n), w);
否则,您正在更新先前包含格式良好的值的字段;然后,调用 caml_modify 函数

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

为了说明上述规则,以下是一个 C 函数,该函数构建并返回一个包含作为参数给出的两个整数的列表。首先,我们使用简化的分配函数来编写它

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

  r = caml_alloc(2, 0);                   /* Allocate a cons cell */
  Store_field(r, 0, Val_int(i2));         /* car = the integer i2 */
  Store_field(r, 1, Val_emptylist);       /* cdr = the empty list [] */
  result = caml_alloc(2, 0);              /* Allocate the other cons cell */
  Store_field(result, 0, Val_int(i1));    /* car = the integer i1 */
  Store_field(result, 1, r);              /* cdr = the first cons cell */
  CAMLreturn (result);
}

此处,注册 result 不是严格必需的,因为在它获得值后不会进行任何分配,但注册所有类型为 value 的局部变量更简单、更安全。

以下是使用低级分配函数编写的相同函数。我们注意到,cons 单元格是小的块,可以使用 caml_alloc_small 分配,并通过对它们的字段进行直接赋值来填充。

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

  r = caml_alloc_small(2, 0);             /* Allocate a cons cell */
  Field(r, 0) = Val_int(i2);              /* car = the integer i2 */
  Field(r, 1) = Val_emptylist;            /* cdr = the empty list [] */
  result = caml_alloc_small(2, 0);        /* Allocate the other cons cell */
  Field(result, 0) = Val_int(i1);         /* car = the integer i1 */
  Field(result, 1) = r;                   /* cdr = the first cons cell */
  CAMLreturn (result);
}

在上面的两个示例中,列表是自下而上构建的。以下是另一种自上而下的方法。它效率较低,但说明了 caml_modify 的使用。

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

  r = caml_alloc_small(2, 0);             /* Allocate a cons cell */
  Field(r, 0) = Val_int(i1);              /* car = the integer i1 */
  Field(r, 1) = Val_int(0);               /* A dummy value
  tail = caml_alloc_small(2, 0);          /* Allocate the other cons cell */
  Field(tail, 0) = Val_int(i2);           /* car = the integer i2 */
  Field(tail, 1) = Val_emptylist;         /* cdr = the empty list [] */
  caml_modify(&Field(r, 1), tail);        /* cdr of the result = tail */
  CAMLreturn (r);
}

直接执行 Field(r, 1) = tail 将是不正确的,因为分配 tail 自分配 r 以来已经发生了。

5.3 延迟操作和异步异常

从 4.10 版本开始,分配函数保证不运行任何 OCaml 代码,包括终结器、信号处理程序以及在同一域上运行的其他线程。相反,它们的执行被延迟到以后的安全点。

来自 <caml/signals.h> 的函数 caml_process_pending_actions 执行任何挂起的信号处理程序和终结器、Memprof 回调、抢占式 systhread 切换,以及请求的小型和大型垃圾收集。特别是,它可以引发异步异常,并从同一域对 OCaml 堆进行变异。建议在长时间运行的非阻塞 C 代码中的安全点定期调用它。

提供了变体 caml_process_pending_actions_exn,它返回异常,而不是直接将其引发到 OCaml 代码中。必须使用 Is_exception_result 测试其结果,如果需要,则需要使用 Extract_exception 进行后续操作。它通常用于在重新引发之前清理。

    CAMLlocal1(exn);
    ...
    exn = caml_process_pending_actions_exn();
    if(Is_exception_result(exn)) {
      exn = Extract_exception(exn);
      ...cleanup...
      caml_raise(exn);
    }

在第 ‍22.7.1 节中,详细介绍了异常返回值的正确使用,尤其是在垃圾收集存在的情况下。

6 一个完整的示例

本节概述了如何将 Unix curses 库中的函数提供给 OCaml 程序使用。首先,以下是声明 curses 原语和数据类型的接口 curses.ml

(* File curses.ml -- declaration of primitives and data types *)
type window                   (* The type "window" remains abstract *)
external initscr: unit -> window = "caml_curses_initscr"
external endwin: unit -> unit = "caml_curses_endwin"
external refresh: unit -> unit = "caml_curses_refresh"
external wrefresh : window -> unit = "caml_curses_wrefresh"
external newwin: int -> int -> int -> int -> window = "caml_curses_newwin"
external addch: char -> unit = "caml_curses_addch"
external mvwaddch: window -> int -> int -> char -> unit = "caml_curses_mvwaddch"
external addstr: string -> unit = "caml_curses_addstr"
external mvwaddstr: window -> int -> int -> string -> unit
         = "caml_curses_mvwaddstr"
(* lots more omitted *)

要编译此接口

        ocamlc -c curses.ml

要实现这些函数,我们只需要提供存根代码;核心函数已经在 curses 库中实现。存根代码文件 curses_stubs.c 如下所示

/* File curses_stubs.c -- stub code for curses */
#include <curses.h>
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/alloc.h>
#include <caml/custom.h>

/* Encapsulation of opaque window handles (of type WINDOW *)
   as OCaml custom blocks. */

static struct custom_operations curses_window_ops = {
  "fr.inria.caml.curses_windows",
  custom_finalize_default,
  custom_compare_default,
  custom_hash_default,
  custom_serialize_default,
  custom_deserialize_default,
  custom_compare_ext_default,
  custom_fixed_length_default
};

/* Accessing the WINDOW * part of an OCaml custom block */
#define Window_val(v) (*((WINDOW **) Data_custom_val(v)))

/* Allocating an OCaml custom block to hold the given WINDOW * */
static value alloc_window(WINDOW * w)
{
  value v = caml_alloc_custom(&curses_window_ops, sizeof(WINDOW *), 0, 1);
  Window_val(v) = w;
  return v;
}

CAMLprim value caml_curses_initscr(value unit)
{
  CAMLparam1 (unit);
  CAMLreturn (alloc_window(initscr()));
}

CAMLprim value caml_curses_endwin(value unit)
{
  CAMLparam1 (unit);
  endwin();
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_refresh(value unit)
{
  CAMLparam1 (unit);
  refresh();
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_wrefresh(value win)
{
  CAMLparam1 (win);
  wrefresh(Window_val(win));
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_newwin(value nlines, value ncols, value x0, value y0)
{
  CAMLparam4 (nlines, ncols, x0, y0);
  CAMLreturn (alloc_window(newwin(Int_val(nlines), Int_val(ncols),
                                  Int_val(x0), Int_val(y0))));
}

CAMLprim value caml_curses_addch(value c)
{
  CAMLparam1 (c);
  addch(Int_val(c));            /* Characters are encoded like integers */
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_mvwaddch(value win, value x, value y, value c)
{
  CAMLparam4 (win, x, y, c);
  mvwaddch(Window_val(win), Int_val(x), Int_val(y), Int_val(c));
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_addstr(value s)
{
  CAMLparam1 (s);
  addstr(String_val(s));
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_mvwaddstr(value win, value x, value y, value s)
{
  CAMLparam4 (win, x, y, s);
  mvwaddstr(Window_val(win), Int_val(x), Int_val(y), String_val(s));
  CAMLreturn (Val_unit);
}

/* This goes on for pages. */

可以使用以下命令编译文件 curses_stubs.c

        cc -c -I`ocamlc -where` curses_stubs.c

或者,更简单地说

        ocamlc -c curses_stubs.c

(当传递一个 .c 文件时,ocamlc 命令只是使用正确的 -I 选项在该文件上调用 C 编译器。)

现在,以下是一个使用 curses 模块的示例 OCaml 程序 prog.ml

(* File prog.ml -- main program using curses *)
open Curses;;
let main_window = initscr () in
let small_window = newwin 10 5 20 10 in
  mvwaddstr main_window 10 2 "Hello";
  mvwaddstr small_window 4 3 "world";
  refresh();
  Unix.sleep 5;
  endwin()

要编译和链接此程序,请运行

       ocamlc -custom -o prog unix.cma curses.cmo prog.ml curses_stubs.o -cclib -lcurses

(在某些机器上,您可能需要使用 -cclib -lcurses -cclib -ltermcap-cclib -ltermcap 来代替 -cclib -lcurses。)

7 高级主题:从 C 到 OCaml 的回调

到目前为止,我们已经描述了如何从 OCaml 调用 C 函数。在本节中,我们将展示 C 函数如何调用 OCaml 函数,无论是作为回调(OCaml 调用 C,C 再调用 OCaml),还是将主程序编写在 C 中。

7.1 从 C 中应用 OCaml 闭包

C 函数可以将 OCaml 函数值(闭包)应用于 OCaml 值。以下函数用于执行应用程序

如果函数 f 没有返回,而是引发了超出应用范围的异常,那么此异常将被传播到下一个封闭的 OCaml 代码,跳过 C 代码。也就是说,如果 OCaml 函数 f 调用 C 函数 g,C 函数 g 回调 OCaml 函数 h,而 OCaml 函数 h 抛出了一个游离异常,那么 g 的执行将被中断,异常将被传播回 f

如果 C 代码希望捕获超出 OCaml 函数范围的异常,它可以使用 caml_callback_exncaml_callback2_exncaml_callback3_exncaml_callbackN_exn 函数。这些函数接受与其非 _exn 对应函数相同的参数,但会捕获超出范围的异常,并将它们返回给 C 代码。caml_callback*_exn 函数的返回值 v 必须使用宏 Is_exception_result(v) 进行测试。如果宏返回“false”,则没有发生异常,并且 v 是 OCaml 函数返回的值。如果 Is_exception_result(v) 返回“true”,则异常已超出范围,可以使用 Extract_exception(v) 恢复其值(异常描述符)。

警告

如果 OCaml 函数返回了一个异常,则在调用可能触发垃圾收集的函数之前,应将 Extract_exception 应用于异常结果。否则,如果 v 在垃圾收集期间可达,则运行时可能会崩溃,因为 v 不包含有效值。

示例

    CAMLprim value call_caml_f_ex(value closure, value arg)
    {
      CAMLparam2(closure, arg);
      CAMLlocal2(res, tmp);
      res = caml_callback_exn(closure, arg);
      if(Is_exception_result(res)) {
        res = Extract_exception(res);
        tmp = caml_alloc(3, 0); /* Safe to allocate: res contains valid value. */
        ...
      }
      CAMLreturn (res);
    }

7.2 获取或注册 OCaml 闭包以供 C 函数使用

有两种方法可以获取要传递给上面描述的 callback 函数的 OCaml 函数值(闭包)。一种方法是将 OCaml 函数作为参数传递给原始函数。例如,如果 OCaml 代码包含以下声明

    external apply : ('a -> 'b) -> 'a -> 'b = "caml_apply"

相应的 C 存根可以这样编写

    CAMLprim value caml_apply(value vf, value vx)
    {
      CAMLparam2(vf, vx);
      CAMLlocal1(vy);
      vy = caml_callback(vf, vx);
      CAMLreturn(vy);
    }

另一种可能性是使用 OCaml 提供的注册机制。此注册机制允许 OCaml 代码在某个全局名称下注册 OCaml 函数,并允许 C 代码通过此全局名称检索相应的闭包。

在 OCaml 端,注册是通过评估 Callback.register n v 来完成的。此处,n 是全局名称(任意字符串),v 是 OCaml 值。例如

    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),可以获得在名称 n 下注册的值的指针。然后,必须取消对返回的指针的引用才能恢复实际的 OCaml 值。如果在名称 n 下没有注册任何值,则将返回空指针。例如,以下是一个调用上面 OCaml 函数 f 的 C 包装器

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

caml_named_value 返回的指针是常量,可以安全地缓存在 C 变量中,以避免重复名称查找。指向的值不能从 C 中更改。但是,它可能会在垃圾收集期间更改,因此必须始终在使用点重新计算。以下是一个更有效的 call_caml_f 变体,它仅调用一次 caml_named_value

    void call_caml_f(int arg)
    {
        static const value * closure_f = NULL;
        if (closure_f == NULL) {
            /* First time around, look up by name */
            closure_f = caml_named_value("test function");
        }
        caml_callback(*closure_f, Val_int(arg));
    }

7.3 注册 OCaml 异常以供 C 函数使用

上面描述的注册机制也可以用于将异常标识符从 OCaml 传递到 C。OCaml 代码通过评估 Callback.register_exception n exn 来注册异常,其中 n 是一个任意名称,exn 是要注册的异常的异常值。例如

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

然后,C 代码可以使用 caml_named_value 恢复异常标识符,并将其作为第一个参数传递给函数 raise_constantraise_with_argraise_with_string(在第 ‍22.4.5 节中描述),以实际引发异常。例如,以下是一个引发带有给定参数的 Error 异常的 C 函数

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

7.4 C 中的主程序

在正常操作中,混合 OCaml/C 程序首先执行 OCaml 初始化代码,然后可能会继续调用 C 函数。我们说主程序是 OCaml 代码。在某些应用程序中,C 代码扮演主程序的角色是可取的,并在需要时调用 OCaml 函数。这可以通过以下方式实现

7.5 将 OCaml 代码嵌入 C 代码中

自定义运行时模式下的字节码编译器 (ocamlc -custom) 通常将字节码追加到包含自定义运行时的可执行文件。这有两个后果。首先,最终的链接步骤必须由 ocamlc 执行。其次,OCaml 运行时库必须能够从命令行参数中找到可执行文件的名称。当使用 caml_main(argv) 就像在第 ‍22.7.4 节中那样,这意味着 argv[0]argv[1] 必须包含可执行文件名。

另一种方法是将字节码嵌入 C 代码中。 -output-obj-output-complete-objocamlc 为此提供的选项。它们会导致 ocamlc 编译器输出一个包含 OCaml 程序字节码的 C 对象文件 (.o 文件,Windows 下为 .obj),以及一个 caml_startup 函数。由 ocamlc -output-complete-obj 生成的 C 对象文件还包含运行时和自动链接库。由 ocamlc -output-objocamlc -output-complete-obj 生成的 C 对象文件可以与 C 代码一起使用标准 C 编译器链接,或者存储在 C 库中。

必须从主 C 程序中调用 caml_startup 函数以初始化 OCaml 运行时并执行 OCaml 初始化代码。就像 caml_main 一样,它接受一个包含命令行参数的 argv 参数。与 caml_main 不同,此 argv 参数仅用于初始化 Sys.argv,而不用于查找可执行文件的名称。

如果异常从顶级模块初始化程序中逃逸,caml_startup 函数将调用未捕获的异常处理程序(或在 ocamldebug 下运行时进入调试器)。可以通过使用 caml_startup_exn 函数并使用 Is_exception_result 测试结果(如果需要,后面跟着 Extract_exception)来在 C 代码中捕获此类异常。

-output-obj-output-complete-obj 选项也可以用来获取 C 源文件。更有趣的是,这些选项还可以直接生成一个包含 OCaml 代码、OCaml 运行时系统以及传递给 ocamlc 的任何其他静态 C 代码的共享库 (.so 文件,Windows 下为 .dll) (.o.a,分别为 .obj.lib)。这种对 -output-obj-output-complete-obj 的使用与正常的链接步骤非常相似,但它不会生成一个自动运行 OCaml 代码的主程序,而是生成一个可以按需运行 OCaml 代码的共享库。 -output-obj-output-complete-obj 的三种可能行为(生成 C 源代码 .c,C 对象文件 .o,共享库 .so)是根据结果文件的扩展名(使用 -o 给出)选择的。

原生代码编译器 ocamlopt 也支持 -output-obj-output-complete-obj 选项,使其输出一个 C 对象文件或一个共享库,其中包含命令行中所有 OCaml 模块的原生代码,以及 OCaml 启动代码。初始化是通过调用 caml_startup(或 caml_startup_exn)执行的,就像字节码编译器的情况一样。由 ocamlopt -output-complete-obj 生成的文件还包含运行时和自动链接库。

对于最终的链接阶段,除了由 -output-obj 生成的对象文件外,您还必须提供 OCaml 运行时库 (libcamlrun.a 用于字节码,libasmrun.a 用于原生代码),以及 OCaml 库使用的所有 C 库。例如,假设您的程序的 OCaml 部分使用了 Unix 库。使用 ocamlc,您应该执行以下操作

        ocamlc -output-obj -o camlcode.o unix.cma other .cmo and .cma files
        cc -o myprog C objects and libraries \
           camlcode.o -L‘ocamlc -where‘ -lunix -lcamlrun

使用 ocamlopt,您应该执行以下操作

        ocamlopt -output-obj -o camlcode.o unix.cmxa other .cmx and .cmxa files
        cc -o myprog C objects and libraries \
           camlcode.o -L‘ocamlc -where‘ -lunix -lasmrun

对于最终的链接阶段,除了由 -output-complete-obj 生成的对象文件外,您只需提供 OCaml 运行时所需的 C 库即可。

例如,假设您的程序的 OCaml 部分使用了 Unix 库。使用 ocamlc,您应该执行以下操作

        ocamlc -output-complete-obj -o camlcode.o unix.cma other .cmo and .cma files
        cc -o myprog C objects and libraries \
           camlcode.o C libraries required by the runtime, eg -lm  -ldl -lcurses -lpthread

使用 ocamlopt,您应该执行以下操作

        ocamlopt -output-complete-obj -o camlcode.o unix.cmxa other .cmx and .cmxa files
        cc -o myprog C objects and libraries \
           camlcode.o C libraries required by the runtime, eg -lm -ldl
警告

在某些端口上,最终链接阶段需要特殊选项,这些选项将由 -output-obj-output-complete-obj 选项生成的​​对象文件与程序的其余部分链接在一起。这些选项在编译 OCaml 时生成的配置文件 Makefile.config 中以变量 OC_LDFLAGS 的形式显示。

堆栈回溯。

当由 ocamlc -g 生成的 OCaml 字节码嵌入到 C 程序中时,不会包含任何调试信息,因此无法在未捕获的异常上打印堆栈回溯。当由 ocamlopt -g 生成的原生代码嵌入到 C 程序中时,情况并非如此:堆栈回溯信息可用,但需要通过编程方式打开回溯机制。这可以通过从 OCaml 侧调用 Printexc.record_backtrace true 在一个 OCaml 模块的初始化中实现。这也可以通过在 OCaml-C 胶合代码中调用 caml_record_backtraces(1); 从 C 侧实现。(caml_record_backtracesbacktrace.h 中声明)

卸载运行时。

如果使用 -output-obj 生成的共享库要由单个进程重复加载和卸载,则必须注意显式卸载 OCaml 运行时,以避免各种系统资源泄漏。

从 4.05 开始,可以使用 caml_shutdown 函数优雅地关闭运行时,这等效于以下操作

由于共享库可能有多个客户端同时使用,因此为了方便起见,可以多次调用 caml_startup(和 caml_startup_pooled),前提是每次这样的调用都与对 caml_shutdown 的相应调用配对(以嵌套方式)。一旦对 caml_startup 没有未完成的调用,运行时将被卸载。

一旦运行时被卸载,就无法再次启动,除非重新加载共享库并重新初始化其静态数据。因此,目前该功能仅适用于构建可重新加载的共享库。

Unix 信号处理。

根据目标平台和操作系统,原生代码运行时系统可能会在调用 caml_startup 时为一个或多个 SIGSEGVSIGTRAPSIGFPE 信号安装信号处理程序,并在调用 caml_shutdown 时将这些信号重置为其默认行为。用 C 编写的程序不应该尝试自己处理这些信号。

8 带回调的进阶示例

本节演示了第 22.7 节中描述的回调功能。我们将以一种可以与 C 代码链接并像任何 C 函数一样从 C 中调用的方式打包一些 OCaml 函数。OCaml 函数在以下 mod.ml OCaml 源代码中定义

(* File mod.ml -- some "useful" OCaml functions *)

let rec fib n = if n < 2 then 1 else fib(n-1) + fib(n-2)

let format_result n = Printf.sprintf "Result is: %d\n" n

(* Export those two functions to C *)

let _ = Callback.register "fib" fib
let _ = Callback.register "format_result" format_result

以下是从 C 中调用这些函数的 C 存根代码

/* File modwrap.c -- wrappers around the OCaml functions */

#include <stdio.h>
#include <string.h>
#include <caml/mlvalues.h>
#include <caml/callback.h>

int fib(int n)
{
  static const value * fib_closure = NULL;
  if (fib_closure == NULL) fib_closure = caml_named_value("fib");
  return Int_val(caml_callback(*fib_closure, Val_int(n)));
}

char * format_result(int n)
{
  static const value * format_result_closure = NULL;
  if (format_result_closure == NULL)
    format_result_closure = caml_named_value("format_result");
  return strdup(String_val(caml_callback(*format_result_closure, Val_int(n))));
  /* We copy the C string returned by String_val to the C heap
     so that it remains valid after garbage collection. */
}

现在我们将 OCaml 代码编译成 C 对象文件,并将其与 modwrap.c 中的存根代码以及 OCaml 运行时系统一起放入 C 库中

        ocamlc -custom -output-obj -o modcaml.o mod.ml
        ocamlc -c modwrap.c
        cp `ocamlc -where`/libcamlrun.a mod.a && chmod +w mod.a
        ar r mod.a modcaml.o modwrap.o

(也可以使用 ocamlopt -output-obj 而不是 ocamlc -custom -output-obj。在这种情况下,用 libasmrun.a(原生代码运行时库)替换 libcamlrun.a(字节码运行时库)。)

现在,我们可以在任何 C 程序中使用 fibformat_result 这两个函数,就像普通的 C 函数一样。只要记得在之前调用一次 caml_startup(或 caml_startup_exn)。

/* File main.c -- a sample client for the OCaml functions */

#include <stdio.h>
#include <caml/callback.h>

extern int fib(int n);
extern char * format_result(int n);

int main(int argc, char ** argv)
{
  int result;

  /* Initialize OCaml code */
  caml_startup(argv);
  /* Do some computation */
  result = fib(10);
  printf("fib(10) = %s\n", format_result(result));
  return 0;
}

要构建整个程序,只需按如下方式调用 C 编译器

        cc -o prog -I `ocamlc -where` main.c mod.a -lcurses

(在某些机器上,您可能需要将 -ltermcap-lcurses -ltermcap 放在 -lcurses 的位置。)

9 进阶主题:自定义块

带有标记 Custom_tag 的块同时包含任意用户数据和指向 C 结构体的指针,类型为 struct custom_operations,该结构体将用户提供的终结、比较、散列、序列化和反序列化函数与该块相关联。

9.1 struct custom_operations

struct custom_operations<caml/custom.h> 中定义,包含以下字段

注意:附加到自定义块描述符的 finalizecomparehashserializedeserialize 函数只允许与 OCaml 运行时进行有限的交互。在这些函数内部,不要调用任何 OCaml 分配函数,也不要执行任何回调到 OCaml 代码。不要使用 CAMLparam 来注册这些函数的参数,也不要使用 CAMLreturn 来返回结果。不要抛出异常(为了在反序列化期间发出错误信号,请使用 caml_deserialize_error)。不要删除全局根。有疑问时,请谨慎行事。在 serializedeserialize 函数内部,允许(甚至推荐)使用第 22.9.4 节中的相应函数。

9.2 分配自定义块

自定义块必须通过 caml_alloc_customcaml_alloc_custom_mem 分配。

caml_alloc_custom(ops, size, used, max)

返回一个新的自定义块,其中包含 size 字节的用户数据空间,其关联操作由 ops 给出(指向 struct custom_operations 的指针,通常作为 C 全局变量静态分配)。

两个参数 usedmax 用于控制当已完成对象包含指向堆外资源的指针时垃圾回收的速度。一般来说,OCaml 增量主要收集器会根据程序的分配速率调整其速度。程序分配越快,GC 工作越努力,以便快速回收不可达的块,并避免出现大量的“浮动垃圾”(GC 尚未回收的未引用对象)。

通常,分配速率是通过计算已分配块的堆内大小来衡量的。但是,已完成对象通常包含指向堆外内存块和其他资源(例如文件描述符、X Windows 位图等)的指针。对于这些块,块的堆内大小不能很好地衡量程序分配的资源数量。

两个参数 usedmax 使 GC 了解已完成块消耗了多少堆外资源:您将分配给此对象的资源数量作为参数 used 提供,并将您希望在浮动垃圾中看到的最大数量作为参数 max 提供。单位是任意的:GC 只关心比率 used / max

例如,如果您正在分配一个包含 w x h 像素的 X Windows 位图的已完成块,并且您不想有超过 1 百万像素的未回收位图,请指定 used = w * hmax = 1000000。

另一种描述 usedmax 参数影响的方法是使用完整的 GC 周期。如果您分配许多自定义块,其 used / max = 1 / N,那么 GC 将在每 N 次分配后进行一次完整的周期(检查堆中的每个对象,并在那些不可达的对象上调用最终化函数)。例如,如果 used = 1 且 max = 1000,那么 GC 将至少在每 1000 次分配自定义块后进行一次完整的周期。

如果您的已完成块不包含指向堆外资源的指针,或者如果您不理解上面的讨论,只需将 used 设置为 0,并将 max 设置为 1 即可。但是,如果您后来发现最终化函数没有“足够频繁”地被调用,请考虑增加 used / max 比例。

caml_alloc_custom_mem(ops, size, used)

当您的自定义块仅包含堆外内存(使用 malloccaml_stat_alloc 分配的内存)且不包含其他资源时,请使用此函数。 used 应该是您的自定义块所持有的堆外内存的字节数。此函数类似于 caml_alloc_custom,只是 max 参数由用户(通过 custom_major_ratiocustom_minor_ratiocustom_minor_max_size 参数)控制,并且与堆大小成正比。它从 OCaml 4.08.0 开始可用。

9.3 访问自定义块

自定义块 v 的数据部分可以通过指针 Data_custom_val(v) 访问。此指针的类型为 void *,应将其转换为存储在自定义块中的数据的实际类型。

自定义块的内容不会被垃圾回收器扫描,因此不能在 OCaml 堆中包含任何指针。换句话说,不要将 OCaml value 存储在自定义块中,也不要使用 FieldStore_fieldcaml_modify 来访问自定义块的数据部分。相反,任何 C 数据结构(不包含堆指针)都可以存储在自定义块中。

9.4 编写自定义序列化和反序列化函数

以下函数在 <caml/intext.h> 中定义,用于以可移植的方式写入和读回自定义块的内容。当数据在小端机器上写入,并在大端机器上读回时,这些函数会处理字节序转换。

函数操作
caml_serialize_int_1写入一个 1 字节整数
caml_serialize_int_2写入一个 2 字节整数
caml_serialize_int_4写入一个 4 字节整数
caml_serialize_int_8写入一个 8 字节整数
caml_serialize_float_4写入一个 4 字节浮点数
caml_serialize_float_8写入一个 8 字节浮点数
caml_serialize_block_1写入一个 1 字节数量的数组
caml_serialize_block_2写入一个 2 字节数量的数组
caml_serialize_block_4写入一个 4 字节数量的数组
caml_serialize_block_8写入一个 8 字节数量的数组
caml_deserialize_uint_1读取一个无符号 1 字节整数
caml_deserialize_sint_1读取一个有符号 1 字节整数
caml_deserialize_uint_2读取一个无符号 2 字节整数
caml_deserialize_sint_2读取一个有符号 2 字节整数
caml_deserialize_uint_4读取一个无符号 4 字节整数
caml_deserialize_sint_4读取一个有符号 4 字节整数
caml_deserialize_uint_8读取一个无符号 8 字节整数
caml_deserialize_sint_8读取一个有符号 8 字节整数
caml_deserialize_float_4读取一个 4 字节浮点数
caml_deserialize_float_8读取一个 8 字节浮点数
caml_deserialize_block_1读取一个 1 字节数量的数组
caml_deserialize_block_2读取一个 2 字节数量的数组
caml_deserialize_block_4读取一个 4 字节数量的数组
caml_deserialize_block_8读取一个 8 字节数量的数组
caml_deserialize_error在反序列化过程中发出错误信号;input_valueMarshal.from_... 在清理其内部数据结构后会抛出 Failure 异常

序列化函数附加到它们所应用的自定义块。显然,反序列化函数不能以这种方式附加,因为在反序列化开始时自定义块还不存在!因此,包含反序列化函数的 struct custom_operations 必须使用 register_custom_operations 函数(在 <caml/custom.h> 中声明)预先注册到反序列化器。反序列化通过从输入流中读取标识符、分配一个大小在输入流中指定的自定义块、在注册的 struct custom_operation 块中搜索具有相同标识符的块,并调用其 deserialize 函数来填充自定义块的数据部分来进行。

9.5 选择标识符

struct custom_operations 中的标识符必须仔细选择,因为它们必须唯一地标识用于序列化和反序列化操作的数据结构。特别地,请考虑在标识符中包含版本号;这样,数据格式以后可以更改,但仍然可以提供向后兼容的反序列化函数。

_(下划线字符)开头的标识符保留用于 OCaml 运行时系统;不要将它们用于您的自定义数据。我们建议使用 URL (http://mymachine.mydomain.com/mylibrary/version-number) 或 Java 风格的包名 (com.mydomain.mymachine.mylibrary.version-number) 作为标识符,以最大程度地降低标识符冲突的风险。

9.6 已完成块

自定义块概括了在 OCaml 3.00 版本之前存在的已完成块。为了向后兼容,自定义块的格式与已完成块的格式兼容,caml_alloc_final 函数仍然可用以分配具有给定最终化函数的自定义块,但具有默认的比较、哈希和序列化函数。(特别是,最终化函数不能访问 OCaml 运行时。)

caml_alloc_final(n, f, used, max) 返回一个大小为 n+1 个字的新自定义块,其最终化函数为 f。第一个字保留用于存储自定义操作;其余 n 个字可用于您的数据。两个参数 usedmax 用于控制垃圾回收的速度,如 caml_alloc_custom 中所述。

10 高级主题:Bigarrays 和 OCaml-C 接口

本节介绍如何使用 Bigarrays 在 C 存根代码中将 C 或 Fortran 代码与 OCaml 代码连接起来。

10.1 包含文件

C 存根文件中必须包含包含文件 <caml/bigarray.h>。它声明了下面讨论的函数、常量和宏。

10.2 从 C 或 Fortran 访问 OCaml Bigarray

如果 v 是一个表示 Bigarray 的 OCaml value,表达式 Caml_ba_data_val(v) 返回指向数组数据部分的指针。此指针的类型为 void *,可以将其转换为适合数组的 C 类型(例如 double []char [][10] 等)。

可以使用以下方法从 C 中查询 OCaml Bigarray 的各种特性

C 表达式返回值
Caml_ba_array_val(v)->num_dims维数
Caml_ba_array_val(v)->dim[i]i 维度
Caml_ba_array_val(v)->flags & CAML_BA_KIND_MASK数组元素类型

数组元素类型是以下常量之一

常量元素类型
CAML_BA_FLOAT1616 位半精度浮点数
CAML_BA_FLOAT3232 位单精度浮点数
CAML_BA_FLOAT6464 位双精度浮点数
CAML_BA_SINT88 位有符号整数
CAML_BA_UINT88 位无符号整数
CAML_BA_SINT1616 位有符号整数
CAML_BA_UINT1616 位无符号整数
CAML_BA_INT3232 位有符号整数
CAML_BA_INT6464 位有符号整数
CAML_BA_CAML_INT31 位或 63 位有符号整数
CAML_BA_NATIVE_INT32 位或 64 位(平台原生)整数
CAML_BA_COMPLEX3232 位单精度复数
CAML_BA_COMPLEX6464 位双精度复数
CAML_BA_CHAR8 位字符
警告

Caml_ba_array_val(v) 必须立即解引用,不能存储在任何地方,包括局部变量。它解析为一个派生指针:它不是有效的 OCaml 值,而是指向 GC 管理的内存区域。因此,此值不得存储在跨 GC 可能仍然存活的任何内存位置。

以下示例显示了将二维 Bigarray 传递给 C 函数和 Fortran 函数。

    extern void my_c_function(double * data, int dimx, int dimy);
    extern void my_fortran_function_(double * data, int * dimx, int * dimy);

    CAMLprim value caml_stub(value bigarray)
    {
      int dimx = Caml_ba_array_val(bigarray)->dim[0];
      int dimy = Caml_ba_array_val(bigarray)->dim[1];
      /* C passes scalar parameters by value */
      my_c_function(Caml_ba_data_val(bigarray), dimx, dimy);
      /* Fortran passes all parameters by reference */
      my_fortran_function_(Caml_ba_data_val(bigarray), &dimx, &dimy);
      return Val_unit;
    }

10.3 将 C 或 Fortran 数组包装为 OCaml Bigarray

可以使用 caml_ba_alloccaml_ba_alloc_dims 函数将指向已分配的 C 或 Fortran 数组的指针 p 包装并返回给 OCaml 作为 Bigarray。

以下示例说明了如何将静态分配的 C 和 Fortran 数组提供给 OCaml。

    extern long my_c_array[100][200];
    extern float my_fortran_array_[300][400];

    CAMLprim value caml_get_c_array(value unit)
    {
      long dims[2];
      dims[0] = 100; dims[1] = 200;
      return caml_ba_alloc(CAML_BA_NATIVE_INT | CAML_BA_C_LAYOUT,
                           2, my_c_array, dims);
    }

    CAMLprim value caml_get_fortran_array(value unit)
    {
      return caml_ba_alloc_dims(CAML_BA_FLOAT32 | CAML_BA_FORTRAN_LAYOUT,
                                2, my_fortran_array_, 300L, 400L);
    }

11 高级主题:更便宜的 C 调用

本节描述如何使调用 C 函数更便宜。

注意: 这仅适用于原生编译器。因此,无论何时使用这些方法中的任何一种,都必须提供一个忽略所有特殊注释的替代字节码存根。

11.1 传递未装箱的值

我们之前说过,所有 OCaml 对象都由 C 类型 value 表示,并且必须使用诸如 Int_val 之类的宏来从 value 类型解码数据。但是,可以告诉 OCaml 原生代码编译器为我们执行此操作,并将参数未装箱传递给 C 函数。同样,可以告诉 OCaml 预期结果未装箱并为我们装箱。

动机是,通过让 ‘ocamlopt‘ 处理装箱,它通常可以决定完全抑制它。

例如,让我们考虑这个例子

external foo : float -> float -> float = "foo"

let f a b =
  let len = Array.length a in
  assert (Array.length b = len);
  let res = Array.make len 0. in
  for i = 0 to len - 1 do
    res.(i) <- foo a.(i) b.(i)
  done

浮点数数组在 OCaml 中未装箱,但是 C 函数 foo 期望其参数为装箱的浮点数,并返回一个装箱的浮点数。因此,OCaml 编译器别无选择,只能装箱 a.(i)b.(i),并将 foo 的结果解箱。这会导致分配 3 * len 个临时浮点数。

现在,如果我们将参数和结果用 [@unboxed] 进行注释,则原生代码编译器将能够避免所有这些分配

external foo
  :  (float [@unboxed])
  -> (float [@unboxed])
  -> (float [@unboxed])
  = "foo_byte" "foo"

在这种情况下,C 函数必须如下所示

CAMLprim double foo(double a, double b)
{
  ...
}

CAMLprim value foo_byte(value a, value b)
{
  return caml_copy_double(foo(Double_val(a), Double_val(b)))
}

为了方便起见,当所有参数和结果都用 [@unboxed] 进行注释时,可以仅在声明本身上放置一次属性。因此,我们也可以改写为

external foo : float -> float -> float = "foo_byte" "foo" [@@unboxed]

下表总结了哪些 OCaml 类型可以解箱,以及相应的 C 类型

OCaml 类型C 类型
floatdouble
int32int32_t
int64int64_t
nativeintintnat

类似地,可以将未标记的 OCaml 整数在 OCaml 和 C 之间传递。这是通过用 [@untagged] 对参数和/或结果进行注释来完成的

external f : string -> (int [@untagged]) = "f_byte" "f"

相应的 C 类型必须是 intnat.

注意: 不要将 C int 类型与 (int [@untagged]) 相对应地使用。这是因为它们的大小通常不同。

可以使用 [@untagged] 对任何立即类型进行注释,即像 int 一样表示的类型。这包括 boolchar、任何仅具有常量构造函数的变体类型。注意:这并不包括 Unix.file_descr,它在所有平台上都不表示为整数。

11.2 直接 C 调用

为了能够在 C 函数执行过程中运行垃圾收集器,OCaml 原生代码编译器会在 C 调用周围生成一些簿记代码。从技术上讲,它使用 C 函数 caml_c_call 包装每个 C 调用,该函数是 OCaml 运行时的组成部分。

对于重复调用的小型函数,这种间接调用可能会对性能产生很大影响。但是,如果我们知道 C 函数不会分配、不会引发异常,并且不会释放域锁(请参见第 ‍22.12.2 节),则不需要此操作。我们可以通过用 [@@noalloc] 属性注释外部声明来告知 OCaml 原生代码编译器此事实

external bar : int -> int -> int = "foo" [@@noalloc]

在这种情况下,从 OCaml 调用 bar 与调用任何其他 OCaml 函数一样便宜,只是 OCaml 编译器无法内联 C 函数……

11.3 示例:无间接调用 C 库函数

使用这些属性,可以无间接调用 C 库函数。例如,OCaml 标准库中定义了许多数学函数就是这样

external sqrt : float -> float = "caml_sqrt_float" "sqrt"
  [@@unboxed] [@@noalloc]
(** Square root. *)

external exp : float -> float = "caml_exp_float" "exp" [@@unboxed] [@@noalloc]
(** Exponential. *)

external log : float -> float = "caml_log_float" "log" [@@unboxed] [@@noalloc]
(** Natural logarithm. *)

12 高级主题:多线程

在混合 OCaml/C 应用程序中使用多个线程(共享内存并发)需要采取特殊预防措施,本节对此进行了描述。

12.1 注册从 C 创建的线程

仅当调用线程为 OCaml 运行时系统所知时,才有可能从 C 回调到 OCaml。从 OCaml 创建的线程(通过系统线程库的 Thread.create 函数)会自动为运行时系统所知。如果应用程序从 C 创建了其他线程,并希望从这些线程回调到 OCaml 代码,则必须先将它们注册到运行时系统中。以下函数在包含文件 <caml/threads.h> 中声明。

12.2 使用 systhreads 并行执行长时间运行的 C 代码

域是 OCaml 程序的并行执行单元。使用 systhreads 库时,多个线程可能附加到同一个域。但是,在任何时间,这些线程中最多只有一个可以执行 OCaml 代码或使用 OCaml 运行时系统的 C 代码(按域)。从技术上讲,这是通过一个“域锁”强制执行的,任何线程在域内执行此类代码时都必须持有该锁。

当 OCaml 调用实现原语的 C 代码时,域锁处于锁定状态,因此 C 代码可以完全访问运行时系统的功能。但是,同一域中的任何其他线程都不能与原语的 C 代码并发执行 OCaml 代码。另请参见第 ‍9.6 章,了解使用多个域时的行为。

如果 C 原语运行时间较长或执行可能阻塞的输入/输出操作,它可以显式释放域锁,使同一域中的其他 OCaml 线程能够与它的操作并发运行。C 代码必须在返回 OCaml 之前重新获取域锁。这是通过以下函数实现的,这些函数在包含文件 <caml/threads.h> 中声明。

这些函数在释放和获取锁之前和之后通过调用异步回调(第 ‍22.5.3 节)轮询待处理的信号。因此,它们可以执行任意 OCaml 代码,包括引发异步异常。

调用 caml_release_runtime_system() 之后,直到调用 caml_acquire_runtime_system() 之前,C 代码不能访问任何 OCaml 数据,也不能调用运行时系统的任何函数,也不能回调 OCaml 代码。因此,在调用 caml_release_runtime_system() 之前,必须将 OCaml 提供给 C 原语的参数复制到 C 数据结构中,并且在 caml_acquire_runtime_system() 返回后,必须将要返回给 OCaml 的结果编码为 OCaml 值。

例如,以下 C 原语调用 gethostbyname 来查找主机名的 IP 地址。 gethostbyname 函数可能会阻塞很长时间,因此我们选择在它运行时释放 OCaml 运行时系统。

CAMLprim stub_gethostbyname(value vname)
{
  CAMLparam1 (vname);
  CAMLlocal1 (vres);
  struct hostent * h;
  char * name;

  /* Copy the string argument to a C string, allocated outside the
     OCaml heap. */
  name = caml_stat_strdup(String_val(vname));
  /* Release the OCaml run-time system */
  caml_release_runtime_system();
  /* Resolve the name */
  h = gethostbyname(name);
  /* Free the copy of the string, which we might as well do before
     acquiring the runtime system to benefit from parallelism. */
  caml_stat_free(name);
  /* Re-acquire the OCaml run-time system */
  caml_acquire_runtime_system();
  /* Encode the relevant fields of h as the OCaml value vres */
  ... /* Omitted */
  /* Return to OCaml */
  CAMLreturn (vres);
}

Caml_state 评估为域状态变量,并在调试模式下检查域锁是否被持有。在 C API 的关键入口点,正常模式下也会进行这样的检查;这就是为什么在没有正确拥有域锁的情况下调用一些运行时函数和宏会导致致命错误的原因:no domain lock held。变体 Caml_state_opt 不执行任何检查,但在域锁未被持有时评估为 NULL。这使您能够确定属于域的线程当前是否持有其域锁,以用于各种目的。

从 C 到 OCaml 的回调必须在持有 OCaml 运行时系统的域锁的情况下执行。如果回调是由未释放运行时系统的 C 原语执行的,则这种情况自然会发生。如果 C 原语先前释放了运行时系统,或者回调是从未从 OCaml 调用(例如,GUI 应用程序中的事件循环)的其他 C 代码执行的,则必须在回调之前获取运行时系统,并在之后释放。

  caml_acquire_runtime_system();
  /* Resolve OCaml function vfun to be invoked */
  /* Build OCaml argument varg to the callback */
  vres = callback(vfun, varg);
  /* Copy relevant parts of result vres to C data structures */
  caml_release_runtime_system();

注意:上述 acquirerelease 函数是在 OCaml 3.12 中引入的。较旧的代码使用以下历史名称,这些名称在 <caml/signals.h> 中声明

直观地说:“阻塞部分”是指不使用 OCaml 运行时系统的 C 代码片段,通常是阻塞输入/输出操作。

13 高级主题:与 Windows Unicode API 接口

本节包含一些关于编写使用 Windows Unicode API 的 C 存根的一般指南。

Windows 下的 OCaml 系统可以在构建时以两种模式之一进行配置

在下文中,我们说一个字符串具有OCaml 编码,如果它在 Unicode 模式下使用 UTF-8 编码,在传统模式下使用当前代码页编码,或者在 Unix 下是任意字符串。一个字符串具有平台编码,如果它在 Windows 下使用 UTF-16 编码,或者在 Unix 下是任意字符串。

从 C 存根编写者的角度来看,与 Windows Unicode API 交互的挑战有两个方面

Windows 下的本机 C 字符类型是 WCHAR,宽两个字节,而 Unix 下是 char,宽一个字节。类型 char_os<caml/misc.h> 中定义,它代表每个平台的具体 C 字符类型。平台编码中的字符串的类型为 char_os *

以下函数被公开以帮助编写兼容的 C 存根。要使用它们,您需要包含 <caml/misc.h><caml/osdeps.h>

注意:caml_stat_strdup_to_oscaml_stat_strdup_of_os 返回的字符串使用 caml_stat_alloc 分配,因此它们需要在不再需要时使用 caml_stat_free 释放。

示例

我们希望以在 Unix 和 Windows 下都工作的方式绑定函数 getenv。在 Unix 下,此函数的原型是

    char *getenv(const char *);

而 Windows 下的 Unicode 版本的原型是

    WCHAR *_wgetenv(const WCHAR *);

char_os 方面,这两个函数都接受类型为 char_os * 的参数,并返回相同类型的结果。我们首先选择要绑定的函数的正确实现

#ifdef _WIN32
#define getenv_os _wgetenv
#else
#define getenv_os getenv
#endif

其余的绑定对于两个平台都是相同的

#include <caml/mlvalues.h>
#include <caml/misc.h>
#include <caml/alloc.h>
#include <caml/fail.h>
#include <caml/osdeps.h>
#include <stdlib.h>

CAMLprim value stub_getenv(value var_name)
{
  CAMLparam1(var_name);
  CAMLlocal1(var_value);
  char_os *var_name_os, *var_value_os;

  var_name_os = caml_stat_strdup_to_os(String_val(var_name));
  var_value_os = getenv_os(var_name_os);
  caml_stat_free(var_name_os);

  if (var_value_os == NULL)
    caml_raise_not_found();

  var_value = caml_copy_string_of_os(var_value_os);

  CAMLreturn(var_value);
}

14 构建混合 C/OCaml 库:ocamlmklib

命令 ocamlmklib 方便构建包含 OCaml 代码和 C 代码的库,并且可以在静态链接和动态链接模式下使用。此命令在 Objective Caml 3.11 之后在 Windows 下可用,在 Objective Caml 3.03 之后在其他操作系统下可用。

命令 ocamlmklib 接受三种类型的参数

它生成以下输出

此外,还识别以下选项

-cclib-ccopt-I-linkall
这些选项按原样传递给 ocamlcocamlopt。请参阅这些命令的文档。
-rpath-R-Wl,-rpath-Wl,-R
这些选项按原样传递给 C 编译器。请参阅 C 编译器的文档。
-custom
强制只构建静态链接库,即使支持动态链接。
-failsafe
如果在构建共享库时出现问题(例如,某些支持库不可用为共享库),则回退到构建静态链接库。
-Ldir
dir 添加到支持库的搜索路径中 (-llib)。
-ocamlc cmd
使用 cmd 而不是 ocamlc 来调用字节码编译器。
-ocamlopt cmd
使用 cmd 而不是 ocamlopt 来调用原生代码编译器。
-o output
设置生成的 OCaml 库的名称。 ocamlmklib 将生成 output.cma 和/或 output.cmxa。如果没有指定,则默认为 a
-oc outputc
设置生成的 C 库的名称。 ocamlmklib 将生成 liboutputc.so(如果支持共享库)和 liboutputc.a。如果没有指定,则默认为使用 -o 给出的输出名称。
示例

考虑一个用于读取和写入压缩文件的标准 libz C 库的 OCaml 接口。假设此库位于 /usr/local/zlib 中。此接口由一个 OCaml 部分 zip.cmo/zip.cmx 和一个 C 部分 zipstubs.o 组成,其中包含围绕 libz 入口点的存根代码。以下命令构建 OCaml 库 zip.cmazip.cmxa,以及配套的 C 库 dllzip.solibzip.a

ocamlmklib -o zip zip.cmo zip.cmx zipstubs.o -lz -L/usr/local/zlib

如果支持共享库,则执行以下命令

ocamlc -a -o zip.cma zip.cmo -dllib -lzip \
        -cclib -lzip -cclib -lz -ccopt -L/usr/local/zlib
ocamlopt -a -o zip.cmxa zip.cmx -cclib -lzip \
        -cclib -lzip -cclib -lz -ccopt -L/usr/local/zlib
gcc -shared -o dllzip.so zipstubs.o -lz -L/usr/local/zlib
ar rc libzip.a zipstubs.o

注意:此示例是在 Unix 系统上。其他系统上的确切命令行可能有所不同。

如果不支持共享库,则改为执行以下命令

ocamlc -a -custom -o zip.cma zip.cmo -cclib -lzip \
        -cclib -lz -ccopt -L/usr/local/zlib
ocamlopt -a -o zip.cmxa zip.cmx -lzip \
        -cclib -lz -ccopt -L/usr/local/zlib
ar rc libzip.a zipstubs.o

而不是同时构建字节码库、原生代码库和 C 库,ocamlmklib 可以调用三次来分别构建每个库。因此,

ocamlmklib -o zip zip.cmo -lz -L/usr/local/zlib

构建字节码库 zip.cma,以及

ocamlmklib -o zip zip.cmx -lz -L/usr/local/zlib

构建原生代码库 zip.cmxa,以及

ocamlmklib -o zip zipstubs.o -lz -L/usr/local/zlib

构建 C 库 dllzip.solibzip.a。请注意,支持库 (-lz) 和相应的选项 (-L/usr/local/zlib) 必须在 ocamlmklib 的所有三次调用中给出,因为它们在不同时间需要,具体取决于是否支持共享库。

15 注意事项:内部运行时 API

并非所有在 caml/ 目录中可用的头文件都在前面的部分中描述过。所有未提及的头文件都是内部运行时 API 的一部分,对此没有稳定性保证。如果你真的需要访问此内部运行时 API,本节提供一些指南,可以帮助你编写可能不会在每个新版本的 OCaml 上中断的代码。

注意

依赖内部 API 来处理他们认为现实且有用的用例的程序员鼓励在错误跟踪器中打开改进请求。

15.1 内部变量和 CAML_INTERNALS

从 OCaml 4.04 开始,可以通过在加载 caml 头文件之前定义 CAML_INTERNALS 宏来访问内部运行时 API 的每个部分。如果未定义此宏,则内部运行时 API 的部分将被隐藏。

如果使用内部 C 变量,请勿手动重新定义它们。你应该通过包含相应的头文件来导入这些变量。这些变量的表示形式在 OCaml 4.10 中已经改变过一次,并且仍在不断发展。如果你的代码依赖于这种内部且脆弱的属性,那么它在某个时间点会失效。

例如,而不是重新定义 caml_young_limit

extern int caml_young_limit;

这在 OCaml ≥ 4.10 中失效,你应该包含 minor_gc 头文件

#include <caml/minor_gc.h>

15.2 OCaml 版本宏

最后,如果包含正确头文件还不够,或者如果你需要支持比 OCaml 4.04 更早的版本,头文件 caml/version.h 应该可以帮助你定义自己的兼容层。此文件提供一些宏,用于定义当前的 OCaml 版本。特别是,OCAML_VERSION 宏描述了当前版本,其格式为 MmmPP。例如,如果你需要对早于 4.10.0 的版本进行一些特定处理,你可以写

#include <caml/version.h>
#if OCAML_VERSION >= 41000
...
#else
...
#endif