调用 Fortran 库
Fortran 并不是人们编写新代码的首选语言,但它仍然在科学界广泛使用。 许多用于进行数值计算的库是用 Fortran 编写的,而且永远不会用 C 或 C++ 编写。 不过,由于 Fortran 通常编译成与 C 程序相同的目标格式,并且名称重整最少,因此完全有可能从 OCaml 调用 Fortran 过程。
本教程将逐步介绍为 Fortran 函数编译接口模块的过程。 这里涉及的步骤与使用 C 函数包装的步骤相同,但需要考虑一些 Fortran 特有的因素。
Fortran 函数包含在一个名为 func.f 的文件中,并具有以下签名
subroutine gtd6(integer iyd, real sec, real alt, real lat, real lon, real dens(8), real temp(2))
iyd
、sec
、alt
、lat
和 lon
参数是输入参数,而 dens
和 temp
是输出参数。
以下所有示例都使用 GNU Fortran 77 编译器 (g77)。 这些示例尚未在 GNU Fortran 90 编译器 (gfort) 上测试,直到它经过一段时间验证后才会进行测试。
步骤 1:编译 Fortran 过程
C/C++ 只有一个子例程类别(函数),而 Fortran 有两个:函数和子例程。 函数等效于非 void C 函数,因为它接受参数并始终返回值。 子例程等效于 void C 函数。
当 g77 编译 Fortran 函数时,它会创建一个命名函数,并在其后添加一个下划线。 如果 Fortran 函数名包含任何下划线,则编译后的函数名将添加两个下划线。 可以通过此名称调用生成的函数。 子例程将被转换为返回 int 的 C 函数。
要将 funcs.f 文件编译成目标文件,可以使用以下命令:
prompt> g77 -c funcs.f
这将生成文件“funcs.o”。 然后,您可以通过执行以下命令查看编译后的函数的名称:
prompt> nm funcs.o
在此输出中,您将看到一行包含以下内容:
T gtd6_
这表明已创建函数 gtd6_,并且它位于目标文件中。
Fortran 支持整数和实数类型,并且它们使用这些名称。 在我们的例子中,我们只有实数和整数类型。 实数等效于 C 中的双精度数,而整数等效于 C 中的长整数。 此外,Fortran 传递所有内容都是通过引用传递的,因此我们 gtd6 函数的对应 C 原型是
int gtd6_(integer *iyd, real* sec, real* alt, real* glat, real* glong, real* dens, real* temp);
请注意,调用者必须知道 dens
和 temp
实际上是数组。 如果未传递数组,则会导致段错误,因为 gtd6_ 函数将它们用作数组(这也是 OCaml 闪耀的另一个原因)。
步骤 2:创建 C 包装器
由于 OCaml 的外部函数接口是基于 C 的,因此需要创建一个 C 包装器。 为了避免传递回数组值时出现困难,我们将简单地创建一个函数,该函数将返回由函数计算的温度数组的第二个元素,并忽略其他返回值(这是函数的非常常见的用法)。 此函数将位于源文件 wrapper.c 中。
CAMLprim value gtd6_t (value iydV, value secVal, value altVal, value latVal, value lonVal) {
CAMLparam5( iydV, secVal, altVal, latVal, lonVal );
long iyd = Long_val( iydV );
float sec = Double_val( secVal );
float alt = Double_val( altVal );
float lat = Double_val( latVal );
float lon = Double_val( lonVal );
gtd6_(&iyd, &sec, &alt, &glat, &glon, d, t);
CAMLreturn( caml_copy_double( t[1] ) );
}
一些值得注意的要点
- 该文件必须包含 OCaml 头文件
alloc.h
、memory.h
和mlvalue.h
。 - 该函数首先调用 CAMLparam5 宏。 这是任何使用 CAML 类型的函数开头都需要的。
- 该函数使用 Double_val 和 Long_val 宏从 OCaml 值对象中提取 C 类型。
- 所有值都通过引用传递给 gtd6_ 过程,如原型所要求的那样。
- 该函数使用 copy_caml_double 函数和 CAMLreturn 宏来创建包含返回值的新值并分别返回它。
步骤 3:编译共享库。
现在有了这两个源文件 funcs.f 和 wrapper.c,我们需要创建一个可以由 OCaml 加载的共享库。 最好将其作为多步骤过程进行,因此以下是一些命令:
prompt> g77 -c funcs.f
prompt> cc -I<ocaml include path> -c wrapper.c
prompt> cc -shared -o wrapper.so wrapper.o funcs.o -lg2c
这将创建一个名为 wrapper.so 的共享对象库,其中包含 Fortran 函数和包装器函数。 -lg2c 选项是必需的,因为它提供了所使用内置 Fortran 函数的实现。
步骤 4:现在到 OCaml
现在,在 OCaml 文件 (gtd6.ml) 中,我们必须定义对该函数的外部引用以及一个调用它的函数。
external temp : int -> float -> float -> float -> float -> float = "gtd6_t"
let () =
print_double (temp 1 2.0 3.0 4.0 5.0);
print_newline ()
这告诉 OCaml temp 函数接受 5 个参数并返回单个浮点数,并调用 C 函数 gtd6_t。
此时,给出的步骤是将此编译成字节码。 我还没有太多编译成本机的经验,因此我会让其他人帮忙(或者等到我学会如何做)。
prompt> ocamlc -c gtd6.ml prompt> ocamlc -o test gtd6.cmo wrapper.so
瞧,我们已经从 OCaml 调用了 Fortran 函数。