调用 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))

iydsecaltlatlon 参数是输入参数,而 denstemp 是输出参数。

以下所有示例都使用 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);

请注意,调用者必须知道 denstemp 实际上是数组。 如果未传递数组,则会导致段错误,因为 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] ) );
}

一些值得注意的要点

  1. 该文件必须包含 OCaml 头文件 alloc.hmemory.hmlvalue.h
  2. 该函数首先调用 CAMLparam5 宏。 这是任何使用 CAML 类型的函数开头都需要的。
  3. 该函数使用 Double_val 和 Long_val 宏从 OCaml 值对象中提取 C 类型。
  4. 所有值都通过引用传递给 gtd6_ 过程,如原型所要求的那样。
  5. 该函数使用 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 函数。

仍然需要帮助吗?

帮助改进我们的文档

所有 OCaml 文档都是开源的。 发现错误或不清楚的地方? 提交一个拉取请求。

OCaml

创新。 社区。 安全。