AI摘要
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 端只需接收数据指针;m 和 n 使用 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_apiC 头文件和实现可以写成:
/* 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 声明逐项对应:value 的 integer(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
./app3. 参数传递:value 是关键开关
bind(C) 下,标量 dummy argument 默认仍按引用传递;只有写出 value 才按 C 的值传递:
| Fortran 声明 | C 侧声明 | 语义 |
|---|---|---|
integer(c_int) :: n | int *n | 地址(Fortran 默认方式) |
integer(c_int), value :: n | int n | 值 |
real(c_double) :: x | double *x | 地址 |
real(c_double), value :: x | double x | 值 |
intent(in)、intent(out) 和 intent(inout) 是 Fortran 编译期的读写契约,不会改变 C ABI;C 头文件中的 const 只是帮助 C 编译器检查调用方,不能替代 Fortran 的 intent。
4. 类型必须来自 iso_c_binding
不要用 integer、real(8) 猜测 C 类型。默认整型的字节数和 real(8) 的含义并非 Fortran 标准保证。常用映射如下:
| C 类型 | Fortran 类型 |
|---|---|
int | integer(c_int) |
long long | integer(c_long_long) |
size_t | integer(c_size_t) |
float | real(c_float) |
double | real(c_double) |
_Bool | logical(c_bool) |
char | character(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. 常见错误排查
- C 端把默认引用参数写成值参数:Fortran 未写
value时,C 侧应使用指针。 - 使用
integer/real(8):改为integer(c_int)/real(c_double),并在两端共享头文件或接口定义。 - 把
a(:,:)当成double *传递:改为a(*)/a(ld,*),或按 Fortran 2018 使用 CFI 描述符方案。 - 忽略列主序:确认下标公式、leading dimension 和数据是否连续。
- 链接失败:检查
name=与 C 符号完全一致;最终链接通常用gfortran/ifx驱动,以包含 Fortran 运行时;C++ 则检查extern "C"。 - 误以为
intent能约束 C:intent只约束 Fortran 编译器,C 代码仍需自行遵守只读/写入约定。
结语
设计 Fortran-C 接口时,可以把 bind(C) 看作一份 ABI 合同:用 iso_c_binding 明确类型,用 value 明确标量传递方式,用 (*) 或显式形状数组表达裸缓冲区,并把长度、leading dimension 和字符串终止符作为普通参数传递。遵守这些规则后,BLAS/LAPACK 封装、遗留 Fortran 库服务化以及 C/C++ 工程集成都可以建立在可移植、可诊断的接口之上。
