AI摘要

摘要:本文介绍Fortran与C互操作中bind(C)接口的使用要点:需满足C互操作规则,使用iso_c_binding类型,以value控制标量值传递,数组用假定形状(*)而非假定形状,并处理列主序及符号名匹配,以建立可移植的ABI边界。

Fortran 数值计算生态中有大量成熟的 BLAS、LAPACK 和 MPI 代码,C 则是操作系统和工程集成中最常见的接口语言。bind(C)(Fortran 2003)为两者提供了标准化的 ABI 边界,但它并不是“给任意 Fortran 过程加一个 C 名字”:过程的参数、类型和数组形状都必须满足 C 互操作规则。

本文以一个矩阵-向量计算为例,说明如何声明可被 C 调用的 Fortran 过程,以及如何从 Fortran 调用 C 函数。示例使用 Fortran 标准的 iso_c_binding,避免依赖编译器对字节数和名称修饰的约定。

1. bind(C) 到底保证什么

subroutine compute_xxx(...) bind(C, name="compute_xxx")
  • bind(C) 使过程采用实现定义但符合 C 互操作要求的 C ABI,并按 C 规则导出符号。
  • name="compute_xxx" 指定链接符号名,名称区分大小写;不写 name 时,绑定标签是 Fortran 过程名的小写形式。
  • bind(C) 不会自动把普通 Fortran 类型变成 C 类型,也不会让假定形状数组(a(:))变成 C 指针。只有满足标准互操作约束的声明才是合法接口。

2. 一个可工作的最小接口

下面的过程由 Fortran 实现、C 调用。数组使用假定大小(*)),因此 C 端只需接收数据指针;mn 使用 value,对应 C 的按值参数。

module c_api
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  implicit none
contains

  subroutine compute_xxx(m, n, a, b, r) bind(C, name="compute_xxx")
    integer(c_int), value, intent(in) :: m, n
    real(c_double), intent(in)  :: a(*) ! m x n,按列主序展开
    real(c_double), intent(in)  :: b(*) ! 长度 n
    real(c_double), intent(out) :: r(*) ! 长度 m
    integer(c_int) :: i, j

    do i = 1, m
      r(i) = 0.0_c_double
      do j = 1, n
        ! Fortran 的 a(i,j) 在一维缓冲区中的下标为 i + (j-1)*m
        r(i) = r(i) + a(i + (j - 1) * m) * b(j)
      end do
    end do
  end subroutine compute_xxx

end module c_api

C 头文件和实现可以写成:

/* c_api.h */
#ifndef C_API_H
#define C_API_H
void compute_xxx(int m, int n, const double *a,
                const double *b, double *r);
#endif
/* main.c */
#include "c_api.h"

int main(void) {
    const int m = 2, n = 3;
    /* Fortran 顺序:第 1 列的两个元素,然后是第 2、3 列 */
    const double a[] = {1, 2, 3, 4, 5, 6};
    const double b[] = {10, 20, 30};
    double r[2];
    compute_xxx(m, n, a, b, r);
    return (r[0] == 220.0 && r[1] == 280.0) ? 0 : 1;
}

这里的 C 声明与 Fortran 声明逐项对应:valueinteger(c_int)int,数组元素是 double,数组参数退化为指针。C++ 调用时应在头文件外包一层 extern "C",防止 C++ 名称修饰。

编译和链接(用 Fortran 驱动链接,以带上 Fortran 运行时):

gfortran -c c_api.f90 -o c_api.o
gcc -c main.c -o main.o
gfortran main.o c_api.o -o app
./app

3. 参数传递:value 是关键开关

bind(C) 下,标量 dummy argument 默认仍按引用传递;只有写出 value 才按 C 的值传递:

Fortran 声明C 侧声明语义
integer(c_int) :: nint *n地址(Fortran 默认方式)
integer(c_int), value :: nint n
real(c_double) :: xdouble *x地址
real(c_double), value :: xdouble x

intent(in)intent(out)intent(inout) 是 Fortran 编译期的读写契约,不会改变 C ABI;C 头文件中的 const 只是帮助 C 编译器检查调用方,不能替代 Fortran 的 intent

4. 类型必须来自 iso_c_binding

不要用 integerreal(8) 猜测 C 类型。默认整型的字节数和 real(8) 的含义并非 Fortran 标准保证。常用映射如下:

C 类型Fortran 类型
intinteger(c_int)
long longinteger(c_long_long)
size_tinteger(c_size_t)
floatreal(c_float)
doublereal(c_double)
_Boollogical(c_bool)
charcharacter(kind=c_char)

两端还必须对齐 signedness、结构体布局、指针宽度和编译器 ABI。结构体应使用 type, bind(C) :: ...,指针则通常通过 type(c_ptr)c_loc/c_f_pointer 处理。

5. 数组:形状、描述符与内存布局

5.1 C 互操作数组的合法形状

若希望 C 端直接用 double * 接收数据,数组 dummy argument 应使用显式形状或假定大小(最后一维可为 *),例如 a(*)a(ld, *)。在 Fortran 2003/2008 的传统互操作模型中,假定形状数组 a(:)a(:,:) 不是合法的 C 互操作 dummy argument

Fortran 2018(吸收了 TS 29113)扩展了互操作能力:假定形状、假定秩、allocatable 或 pointer 等参数可通过标准的 CFI_cdesc_t C 描述符表示。此时 C 端形参是描述符指针,而不是 double *;这不能简化为“bind(C) 自动解包成裸指针”。

如果 Fortran 内部希望使用 a(:,:),可增加一个普通 Fortran 包装过程,在包装层检查形状后调用上面的 C 互操作过程;C 边界仍保持 a(*)a(ld,*)

5.2 列主序与步长

Fortran 按列主序存储二维数组。若 a 表示 m x n 的连续矩阵,C 中元素 a(i,j)(Fortran 从 1 开始)对应:

double value = a[(j - 1) * m + (i - 1)];

不要把它误写成 C 行主序的 a[i * n + j]。若数组可能是切片或非连续区域,还必须在接口中传递 leading dimension/stride,或在一侧先复制成连续缓冲区。

6. 从 Fortran 调用 C:接口块怎么写

当 C 函数没有 Fortran 实现时,Fortran 需要一个显式接口来描述它。接口可以放在模块中并通过 use 复用:

module c_decls
  use, intrinsic :: iso_c_binding, only: c_int, c_double
  implicit none
  interface
    subroutine c_scale(n, alpha, x) bind(C, name="c_scale")
      import :: c_int, c_double
      integer(c_int), value, intent(in) :: n
      real(c_double), value, intent(in) :: alpha
      real(c_double), intent(inout) :: x(*)
    end subroutine c_scale
  end interface
end module c_decls

此时 C 原型是:

void c_scale(int n, double alpha, double *x);

模块过程本身不需要再写 interface;只有外部过程(例如 C 函数)或没有显式接口的过程才需要接口块。接口块的作用是让编译器检查类型、intent 和互操作属性,并生成正确的调用代码,而不是“把任意假定形状数组转换成指针”。

7. 字符串、函数和可选参数的边界

  • bind(C) 的字符 dummy argument 通常是长度为 1 的 character(kind=c_char);C 字符串应声明为 character(kind=c_char) :: s(*),并显式传递以 c_null_char 结尾的缓冲区。不会有编译器私自添加的隐藏长度参数。
  • C 函数返回值应使用 function ... result(...)`bind(C),结果类型必须是 C 互操作类型;复杂结果通常改为 subroutine`,通过输出参数返回。
  • optional、假定秩、可分配数组等高级特性需要按照 Fortran 2018/TS 29113 的 C 描述符规则设计。若目标是稳定的 C ABI,优先使用普通标量、(*) 数组和显式长度参数。

8. 常见错误排查

  1. C 端把默认引用参数写成值参数:Fortran 未写 value 时,C 侧应使用指针。
  2. 使用 integer/real(8):改为 integer(c_int)/real(c_double),并在两端共享头文件或接口定义。
  3. a(:,:) 当成 double * 传递:改为 a(*)/a(ld,*),或按 Fortran 2018 使用 CFI 描述符方案。
  4. 忽略列主序:确认下标公式、leading dimension 和数据是否连续。
  5. 链接失败:检查 name= 与 C 符号完全一致;最终链接通常用 gfortran/ifx 驱动,以包含 Fortran 运行时;C++ 则检查 extern "C"
  6. 误以为 intent 能约束 Cintent 只约束 Fortran 编译器,C 代码仍需自行遵守只读/写入约定。

结语

设计 Fortran-C 接口时,可以把 bind(C) 看作一份 ABI 合同:用 iso_c_binding 明确类型,用 value 明确标量传递方式,用 (*) 或显式形状数组表达裸缓冲区,并把长度、leading dimension 和字符串终止符作为普通参数传递。遵守这些规则后,BLAS/LAPACK 封装、遗留 Fortran 库服务化以及 C/C++ 工程集成都可以建立在可移植、可诊断的接口之上。

投币支持一下吧
END