C、Fortran与Python

C/Fortran 的互用性

大多数情况下,一个程序是用一种语言编写的,但在某些情况下,有可能会在一个可执行文件中混合使用一种以上的语言。其中一种情况是当一个库是用一种语言编写的时候,但却被另一种语言的程序使用。在这种情况下,库的编写者可能让我们很轻易地使用这个库;本节是让我们发现自己处于库作者的位置。我们将集中讨论 C/C++Fortran 之间的互用性。

这个问题很复杂,因为这两种语言已经存在了很长时间,而最近各种的语言标准已经引入了一些机制来促进互用性。然而,仍然有很多旧代码存在,而且并非所有的编译器都支持最新的标准。因此,我们同时讨论旧的和新的解决方案。

链接器规定

如上所述,编译器将源文件变成二进制文件,其中不再有任何源语言的痕迹:它实际上包含了机器语言的功能。然后,链接器将匹配可能在不同的文件中的调用和定义。使用多种语言的问题是编译器对如何将有不同的概念的函数名称从源文件翻译成二进制文件。

让我们看看代码(我们可以在 tutorials/linking 中找到示例文件):

  1. // C:
  2. Subroutine foo()
  3. Return
  4. End Subroutine
  5. ! Fortran
  6. void foo() {
  7. return;
  8. }

编译后,我们可以用 nm 来查看二进制对象文件:

  1. %% nm fprog.o
  2. 0000000000000000 T _foo_
  3. ....
  4. %% nm cprog.o
  5. 0000000000000000 T _foo
  6. ....

我们看,在内部,foo 例程有不同的名字:Fortran 的名字后面有一个下划线。这使得我们很难从 C 语言中调用 Fortran 例程,反之亦然。可能名称不匹配的是:

  • Fortran 编译器附加了一个下划线,这是最常见的情况。
  • 有时它可以附加两个下划线。
  • 通常情况下,例程名称在对象文件中是小写的,但也有可能是大写的。

由于 C 是一种流行的编写库的语言,这意味着这个问题经常在 C 库中解决:

  • 在所有的 C 函数名称上加一个下划线;
  • 或包括一个简单的封装器调用:
  1. int SomeCFunction(int i,float f)
  2. {
  3. .....
  4. }
  5. int SomeCFunction_(int i,float f)
  6. {
  7. return SomeCFunction(i,f);
  8. }

Fortran 2003 中的 C 语言绑定

在最新的 Fortran 标准中,有明确的 C 语言绑定,使我们可以声明变量和例程的外部名称:

  1. module operator
  2. real, bind(C) :: x
  3. contains
  4. subroutine s() bind(C,name=’s’)
  5. return
  6. end subroutine
  7. end module
  8. %% ifort -c fbind.F90
  9. %% nm fbind.o
  10. .... T _s
  11. .... C _x

也可以将数据类型声明为与C语言兼容:

  1. Program fdata
  2. use iso_c_binding
  3. type, bind(C) :: c_comp
  4. real (c_float) :: data
  5. integer (c_int) :: i
  6. type (c_ptr) :: ptr
  7. end type
  8. end Program fdata

C++ 连接

C++ 编写的库提供了进一步的问题,C++ 编译器通过在一个 name mangling 的进程合并类和方法的名称来制造外部符号。我们可以迫使编译器生成对其他语言可理解的名称,方法是:

  1. #ifdef __cplusplus
  2. extern"C" {
  3. #endif
  4. .
  5. .
  6. place declarations here
  7. .
  8. .
  9. #ifdef __cplusplus
  10. }
  11. #endif

例如,编译以下内容

  1. #include <stdlib.h>
  2. int foo(int x) {
  3. return x;
  4. }

并用 nm 检查输出,得到的结果是:

  1. 0000000000000010 s EH_frame1
  2. 0000000000000000 T _foo

另一方面,将相同的程序编译为 C++ 语言后,可以得到

  1. 0000000000000010 s EH_frame1
  2. 0000000000000000 T __Z3fooi

我们可以看到 foo 的名字是一些杂乱无章的东西,所以我们不能从一个不同语言的程序中调用这个程序。另一方面,如果我们加上 extern 声明:

  1. #include <stdlib.h>
  2. #ifdef __cplusplus
  3. extern"C" {
  4. #endif
  5. int foo(int x) {
  6. return x;
  7. }
  8. #ifdef __cplusplus
  9. }
  10. #endif

我们又得到了与 C 语言相同的链接器符号,因此该例程可以从 CFortran 中调用。如果我们的主程序是 C语言,我们可以使用 C++ 编译器作为链接器。如果主程序是用 Fortran 编写的,我们需要使用 Fortran 编译器作为链接器。然后有必要为 C++ 的系统例程连接额外的库。例如,在英特尔编译器中,需要在链接行中加入 -lstdc++ -lc 。如果我们将其他语言链接到 C++ 主程序中,也需要使用 extern 的方法。例如,一个Fortran 子程序 foo 应该被声明为:

  1. extern "C" {
  2. void foo_();
  3. }

在这种情况下,我们再次使用 C++ 编译器作为链接器。

复数

C/C++Fortran 中的复杂数据类型是相互兼容的。下面是一个 C++ 的例子程序与 Lapack 的复杂向量缩放例程 zscal 连接的例子。

  1. // zscale.cxx
  2. extern "C" {
  3. void zscal_(int*,double complex*,double complex*,int*);
  4. }
  5. complex double *xarray,*yarray, scale=2.;
  6. xarray = new double complex[n]; yarray = new double complex[n];
  7. zscal_(&n,&scale,xarray,&ione);

数组

CFortran 对于存储多维数组有不同的规定,当我们在不同语言编写的程序之间传递一个数组时,我们需要意识到 Fortran 以列为主的顺序存储多维数组,见图28.1。对于二维数组 A(i,j) ,这意味着每一列中的元素是连续存储的:一个2×2的数组被存储为 A(1,1), A(2,1), A(1,2), A(2,2) 。三维和更高维的数组是一个明显的延伸:有时会说 “左边的索引变化最快“。 C 语言的数组是以行为主的顺序存储的:每一行的元素都是连续存储的,然后列是按顺序放在内存中。

在内存中按顺序排列。一个2×2的数组A[2][2]被存储为A[1][1], A[1][2], A[2][1] ,A[2][2]。

关于 C 语言中的数组的一些备注:

  • C(在 C99 标准之前)只在有限的意义上有多维数组。我们可以声明它们,但如果我们把它们传递给另一个 C 语言函数,它们就不再是多维的了:它们变成了普通的 float *(或其他类型)数组。
  • C语言中的多维数组看起来好像是 float \* *类型的,也就是说,一个指针数组指向(单独分配的)行数组。我们肯定可以实现这一点:
  1. float **A;
  2. A = (float**)malloc(m*sizeof(float*));
  3. for (i=0; i<n; i++)
  4. A[i] = (float*)malloc(n*sizeof(float));

仔细阅读标准可以发现,多维数组实际上是一个单一的内存块,不涉及进一步的指针。

鉴于上述对传递多维数组的限制,以及 C 程序无法分辨它是由 Fortran 还是 C 语言调用的事实,最好不要在C语言中使用多维数组,而应仿效它们:

  1. float *A;
  2. A = (float*)malloc(m*n*sizeof(float));
  3. #define SUB(i,j,m,n) i+j*m
  4. for (i=0; i<m; i++)
  5. for (j=0; j<n; j++)
  6. .... A[SUB(i,j,m,n)] ....

其中,为了实现互用性,我们以列为主的方式存储这些元素。

数组排列

由于 SIMD 向量指令等原因,使用对齐分配( aligned allocation )可能是有利的。例如,”16字节对齐”意味着我们的数组的起始地址,用字节表示,是16的倍数。

C 语言中,我们可以用 posix memalign 强制进行这种对齐。在 Fortran 中,没有通用的机制。英特尔的编译器允许这样写:

  1. double precision, allocatable :: A(:), B(:)
  2. !DIR$ ATTRIBUTES ALIGN : 32 :: A, B

字符串

编程语言在处理字符串的方式上有很大不同。

  • C 语言中,字符串是一个字符数组;字符串的结束是由一个空字符表示的,即 ascii 码 0,它有一个全零的比特模式。被称为空终止( null termination
  • Fortran 中,一个字符串是一个字符数组。长度被保存在一个内部变量中,作为一个隐藏参数传递给子程序。
  • Pascal 中,一个字符串是一个数组,在第一个位置有一个表示长度的整数。由于只有一个字节,所以在 Pascal 中,字符串的长度不能超过255个字符。

正如我们所看到的,在不同语言之间传递字符串是充满危险的。这种情况由于将字符串作为子程序参数传递并不是标准的,而变得更加糟糕。

例子:Fortran 中的主程序传递一个字符串

  1. Program Fstring
  2. character(len=5) :: word = "Word"
  3. call cstring(word)
  4. end Program Fstring

和C程序接收一个字符串和它的长度:

  1. #include <stdlib.h>
  2. #include <stdio.h>.
  3. void cstring_(char *txt,int txtlen) {
  4. printf("length = %d\n",txtlen);
  5. printf("<<")。
  6. for (int i=0; i<txtlen; i++)
  7. printf("%c",txt[i])
  8. printf(">>\n");
  9. }

这输出了:

  1. length = 5
  2. <<Word >>

要将一个 Fortran 字符串传递给 C 程序,我们需要附加一个空字符:

  1. call cfunction (’A string//CHAR(0))

一些编译器支持扩展,为促进这一点,编写:

  1. DATA forstring /’This is a null-terminated string.’C/

最近,” C/Fortran 互用作性标准 “为此提供了一个系统的解决方案。

子程序参数

C 语言中,我们可以向该函数传递一个它需要的 float 参数,或当该函数要在调用环境中修改变量的值时传递 float **。Fortran* 没有这种区别:每个变量都是通过引用传递的。这有一些奇怪的后果:如果我们把一个值37传给一个子程序,编译器会用这个值分配一个无名变量,并传递它的地址,而不是该值。

对于 FortranC 程序的接口,这意味着一个 Fortran 程序在 C 程序看来,其所有的参数都是 “\ 参数。反过来说,如果我们想让一个 C 语言的子程序可以从 Fortran 中调用,那么它所有的 参数都必须是”*“*的。这意味着一方面,我们有时会通过引用传递一个变量而我们却想用值来传递。

更糟的是,这意味着 C 语言的子程序如下:

  1. void mysub(int **iarray) {
  2. *iarray = (int*)malloc(8*sizeof(int));
  3. return;
  4. }

不能从 Fortran 中调用。有一个方法可以解决这个问题(请看 Fortran77 的接口到 Petsc 例程 VecGetValues 的接口),如果更聪明的话,我们可以使用 POINTER 变量来解决这个问题。

输入/输出

两种语言都有自己的处理输入/输出的系统,而且不可能真正遇到。基本上,如果 Fortran 例程做I/O,主程序就必须是 Fortran 的。因此,最好是尽可能地隔离 I/O ,并在混合语言编程中使用 C 来处理 I/O

Fortran2003 中的 Fortran/C 互用性

目前许多编译器都不支持最新版本的 Fortran,但它有与 C 语言对接的机制。

  • 有一个模块包含了命名的种类,这样就可以声明

    1. INTEGER,KIND(C_SHORT) :: i
  • Fortran 的指针是比较复杂的对象,所以把它们传递给C是很困难的;Fortran2003 有一个 机制来处理 C 语言的指针,它只是地址。

  • 可以使 Fortran 派生类型与 C 结构兼容。

Python 调用 C 代码

由于计算效率高,C 语言是用于程序最低层的合理语言。另一方面,由于它的表达能力,Python 是最上层的良好候选语言。那么想从一个 Python 程序中调用 C 程序是一个合乎逻辑的想法。这可以通过 python ctypes 模块来实现。

  1. 我们写好我们的 C 语言代码,并如上所述将其编译成一个动态库。
  2. python 代码动态地加载库,例如 libc
  1. path_libc = ctypes.util.find_library("c")
  2. libc = ctypes.CDLL(path_libc)
  3. libc.printf(b"%s\n", b"Using the C printf function from Python ... ")
  1. 我们需要在python中声明C程序的类型是什么:
  1. test_add = mylib.test_add
  2. test_add.argtypes = [ctypes.c_float, ctypes.c_float]
  3. test_add.restype = ctypes.c_float
  4. test_passing_array = mylib.test_passing_array
  5. test_passing_array.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
  6. test_passing_array.restype = None
  1. 标量可以简单传递,数组则需要构建:
  1. data = (ctypes.c_int * Nelements)(*[x for x in range(numel)])