尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C与Fortran互操作实战:类型映射、ISO C Binding与符号表检查

C与Fortran互操作实战:类型映射、ISO C Binding与符号表检查 简介这是一份用于C和C语言与Fortran语言之间代码互转的实用工具包主要面向科学计算、高性能计算以及需要维护混合语言项目的开发人员目的是解决跨语言调用、历史代码改造和功能迁移过程中遇到的对接难题。资源压缩包共整理有8个文件整体大小仅1.38MB其中包含2个可直接运行的可执行程序、2个Fortran源文件、1个C语言示例文件以及fd和fi两类接口描述文件用户既能双击exe体验自动转换又可以打开f90和c文件学习转换细节。目前这份工具包已被352人学习使用。该工具能够自动分析C或C源代码生成对应的Fortran接口并且妥善处理指针、数组、结构体、函数声明等复杂的类型映射关系在Fortran转C的方向上也会考虑到数组下标起点不同、内联输入输出和动态内存管理差异尽量生成可直接使用的C代码。借助这份工具开发者可以明显减少手工改写代码的工作量同时借助配套示例深入理解两门语言在调用约定与数据类型上的区别提升跨语言开发效率。1. c2f.zip 与 C/C/Fortran 互相转换的切入点拿到一个以c2f.zip命名的压缩包里面包含的往往不是编译器而是一堆脚本、替换规则和示例工程。它们的目标只有一个把 C 或 C 的源码搬到 Fortran 世界或者反向搬回。表面看起来这只是把语法换一套关键字实际做起来却会撞上内存布局、符号修饰、数组排列和字符串约定这些“看不见的墙”。你花一天改完语法又可能花三天调链接错误或数值不一致。这篇文章不依赖任何具体的 c2f.zip 内容只谈这类转换中最常见、最可靠的落地路径先用 C 和 Fortran 的数据模型差异建立正确预期再用手写映射加编译器检查跑通最小例子最后用符号表和随机数值比对验证结果可靠。它适合维护过老 Fortran 数值库、想把 C 算法嵌入 Fortran 主程序、或者反过来暴露 Fortran 子程序给 C/C 列表的工程师。2. C 与 Fortran 的数据模型差异从类型表到调用约定的三个必查点2.1 类型位宽与指针语义的对应关系在动手写转换代码之前先把两种语言的类型表铺开。Fortran 里integer默认可能是 4 字节也可能因为编译选项变成 8 字节C 里int通常也是 4 字节但标准只保证不小于 2。直接硬编码会埋雷。常见做法是明确指定integer(kind4)、real(kind8)这类 kind 值与 C 的int32_t、double对齐。C 类型Fortran 类型gfortran位宽说明charcharacter*1/character1 字节C 字符串以\0结尾Fortran 按长度处理shortinteger*22 字节int/int32_tinteger*44 字节long longinteger*88 字节floatreal*44 字节doublereal*88 字节char*character(len*), intent(in) :: s动态转换重点指针更麻烦。C 里的int*可能指向数组首元素也可能指向单个标量Fortran 中常用dimension(:)假定形状数组来表达“一段连续内存”。如果 C 的指针和长度参数分离是常态Fortran 里则要把长度一并传入或使用size()。比如 C 语句void fill(int* a, int n)Fortran 端写一个子程序时最好写成subroutine fill(a, n) bind(C)其中a声明为integer(c_int), intent(out), dimension(*) :: a。标量指针就用type(c_ptr)再在内部转成c_f_pointer。! Fortran 端接收 C 指针的骨架 module ptr_helper use iso_c_binding contains subroutine pass_ptr(cp, n) bind(C, namepass_ptr) type(c_ptr), value :: cp integer(c_int), value :: n integer(c_int), pointer :: arr(:) call c_f_pointer(cp, arr, [n]) ! arr(1) 到 arr(n) 可以像正常数组一样使用 end subroutine end module这里type(c_ptr)是在iso_c_binding模块里定义的对应 C 的void*。c_f_pointer把 C 指针装载成 Fortran 指针数组。值得注意的是value属性它让 C 传入的指针值不再变成 Fortran 端的引用参数避免多取一层地址。参数说明[n]指定 Fortran 端 shape只有这样才能对数组做边界检查否则只能按dimension(*)原始访问越界风险高。2.2 数组顺序、字符串和结构体的存储布局C 数组按行优先存储arr[2][3]先存第 0 行再存第 1 行Fortran 数组按列优先arr(2,3)先存第 0 列再存第 1 列。这个差异直接决定了“二维数组按线性内存转换”时元素顺序完全不同。如果你在 C 端按双重循环for(i) for(j) a[i][j]转换到 Fortran 应该写成do j1,m; do i1,n; a(i,j)...外层循环放第二维才会让访问顺序贴合原内存布局。字符串的处理是另一个高发事故点。C 以\0结尾没有长度元数据Fortran 字符串隐式携带长度即使你不传编译器也会在后面塞一个长度参数取决于调用约定。因此 C 调用 Fortran 子程序时如果子程序参数包含character*20这类定长字符C 端必须预留足够空间并在调用后手动补\0。建议转换时统一走bind(C)的character(kindc_char)数组不用 Fortran 原生字符类型避免隐式长度参数参与符号签名导出与导入双方都更可控。结构体的对齐也要检查。C 编译器默认对齐到 4 或 8Fortran 的type默认顺序布局可以用!GCC$ ATTRIBUTES ALIGN或c_loc相关特性对齐但最稳妥的办法是不在接口处直接传结构体而是传type(c_ptr)在 Fortran 内部用c_f_pointer把结构体当作字节数组逐个字段解析。这也是 c2f 这类工具最常见的处理方式转换后保留 C 结构体定义并在 Fortran 侧生成对应的type(, bind(C))声明如type, bind(C) :: c_point real(c_double) :: x real(c_double) :: y end typebind(C)让编译器按 C 的布局规则处理字段顺序和对齐避免 Fortran 默认规则插进填充字节。很多转换后的程序段错误根源就在这里。2.3 调用约定中的符号修饰与绑定方式C 源码编译出的目标文件函数符号名与源码名一致如sum。Fortran 编译出的函数符号名受编译器影响gfortran 默认会在外部符号名后加下划线sum_ifort 默认不加下划线。C 则还有名字修饰void foo()在 Itanium ABI 下会被修饰为_Z3foov。这三者混编直接链接会找不到符号。为了绕开这些问题业界有一个早已标准化的接口iso_c_binding的bind(C)属性。它告诉 Fortran 编译器这个子程序按 C 命名规则导出不额外加下划线参数按 C 调用约定传递。反过来C 或 C 中声明 Fortran 函数时推荐用extern C包一层再按编译器实际符号手动写上_后缀。比如 gfortran 编译的add_C 端声明extern C double add_(double* a, double* b);这里如果不加extern CC 编译器会把add_二次修饰链接同样失败。所以当手头的 c2f.zip 只提供替换脚本而没提供接口包装时我建议重点检查生成的.h或.f90接口模块是否带上bind(C)。没有这一层后面所有的编译命令都是空的。subroutine add(a, b, result) bind(C, nameadd) use iso_c_binding real(c_double), value :: a, b real(c_double), intent(out) :: result result a b end subroutine注意value属性让实参按值传递C 端可以直接double r add(1.0, 2.0)不传指针。这个特性在转换过程中经常被遗漏因为传统 Fortran 子程序参数默认都是引用传递不加value时 C 端必须传地址。3. 手写 C→Fortran 转换参数表、数组与字符串处理的最小可运行命令3.1 从 C 函数到 Fortran 模块的映射步骤假设待转换的 C 函数是double calc_sum(const double* x, int n) { double s 0.0; for (int i 0; i n; i) s x[i] * 2.0; return s; }写 Fortran 时最直接的方式不是写成一个函数而是子程序加输出参数以保持与 C 的参数数组一致module conv_example use iso_c_binding contains subroutine calc_sum(x, n, result) bind(C, namecalc_sum) real(c_double), intent(in) :: x(*) integer(c_int), intent(in), value :: n real(c_double), intent(out) :: result integer :: i result 0.0d0 do i 1, n result result x(i) * 2.0d0 end do end subroutine end module代码逻辑说明x(*)是假定长度数组只接收地址长度由n控制。intent(in)与 C 侧const double*对应intent(out)对应输出参数。循环内的2.0d0是为了避免 Fortran 把整型 2 转成默认实型再计算C 端2.0是双精度这里显式用d0后缀保证乘法在双精度域内完成结果与 C 端逐位一致。参数说明value属性使n按值传递C 调用时calc_sum(arr, 3)的 3 直接进栈如果不写valueFortran 会尝试取 3 的地址造成运行时 segfault。这是所有 C/Fortran 互操作代码中最高频的错误。3.2 用 gfortran gcc 在本地跑通混合编译的最小命令现在需要让上面的模块与 C 测试程序链接。一个合理的流程是写 C 端测试文件main.c。写 Fortran 模块文件conv_example.f90。分别编译成.o再链接。# 编译 Fortran 模块生成 .mod 和 .o gfortran -c conv_example.f90 -o conv_example.o # 编译 C 测试程序注意用 C99 或 C11 gcc -c main.c -o main.o -stdc11 # 链接C 端调用 Fortran 必须用 gfortran 做链接驱动 gfortran main.o conv_example.o -o test_calcC 端测试程序#include stdio.h void calc_sum(const double* x, int n, double* result); int main(void) { double x[3] {1.0, 2.0, 3.0}; double out 0.0; calc_sum(x, 3, out); printf(result%f\n, out); return 0; }逻辑说明链接顺序放在最后因为 C 的运行时与 Fortran 运行时并不完全一致gfortran会帮我们追加 Fortran 运行时库包括libgfortran。如果用gcc来做链接会报一堆_gfortran_*未定义。gcc只编译.c文件不负责 Fortran 运行时的初始化。参数说明-stdc11只是为了得到更野蛮的检查gfortran -c产生的.mod文件是给 Fortran 其他模块用的不是链接必需的链接只需要.o。如果模块里用了bind(C)且name指定了calc_sum那目标文件中的符号就叫calc_sum不会出现calc_sum_这样main.c里的声明才能对上。3.3 数组维度和长度参数的处理两个必调参数C 数组在多维时最外层数组在参数传递中会退化为指针转换到 Fortran 要注意两件事尺寸参数和外循环顺序。最常见的调用是// C 二维数组按行传递 void mat_row_sum(const double* m, int rows, int cols, double* rowsum);转换后的 Fortran 子程序subroutine mat_row_sum(m, rows, cols, rowsum) bind(C, namemat_row_sum) use iso_c_binding real(c_double), intent(in) :: m(rows, cols) integer(c_int), intent(in), value :: rows, cols real(c_double), intent(out) :: rowsum(rows) integer :: i, j, idx do i 1, rows rowsum(i) 0.0d0 do j 1, cols idx (j - 1) * rows i ! 列优先索引转换 rowsum(i) rowsum(i) m(i, j) end do end do end subroutine逻辑说明因为 Fortran 数组m(rows, cols)的存储顺序是列优先m(i,j)在内存中的位置是(j-1)*rowsi手动索引idx同样结果。这里显式写出 idx 是为了提醒转换代码时不要按 C 的行优先双重循环硬搬否则会出现访问错位运行结果在数值上差之千里。参数说明m(rows, cols)是显式形状数组它需要rows和cols在参数表中先给出来。这也是为什么要按值传rows和cols而且顺序必须在m之后Fortran 要求显式形状数组的边界参数提前出现所以参数表顺序是m, rows, cols。如果你的 c2f.zip 转换脚本只会把int n放在数组后面那一编译就会报错这是手写转换的第一个踩坑点。4. Fortran→C 反向转换与 c2f 包的典型命令组合4.1 避开手工重写的自动预处理思路反向转换的场景通常是老 Fortran 77 子程序要提供 C 接口或者是纯 Fortran 算法要被 C 调用。此时不一定要把代码全部改成 C更常见的是保留 Fortran 实现套一层 C 接口。c2f 这个名字虽然字面是 C→Fortran但很多实际包也会包含反向脚本比如用sed把integer替换成int把subroutine替换成void——这种机械替换只能应付 70% 的纯计算代码遇到字符串和派生类型就完蛋。更稳的自动路线是先用 gfortran 的预处理能力跑一遍宏再用工具做还原。比如.F90文件中的#define和宏函数在编译时会先展开这样 Fortran 代码里也可以写 C 风格的条件编译。转换到 C 时把这些宏留在代码里作为 C 端的#define并配合-cpp选项处理。# 用 -cpp 展开 Fortran 中的宏同时生成 .i 文件用于查看 gfortran -cpp -E legacy_code.F90 -o legacy_code.i但逆向转换的最实用方法并不是让机器把 Fortran 源码翻译成 C 语法而是把 Fortran 源文件本身当作 C 函数的子程序来链接。下面这段就是一个直接可用的 Fortran 子程序subroutine old_calc(a, n, out) bind(C, nameold_calc) use iso_c_binding integer(c_int), intent(in), value :: n real(c_double), intent(in) :: a(n) real(c_double), intent(out) :: out integer :: i out 0.0d0 do i 1, n out out sqrt(a(i)) end do end subroutine把它编译成目标文件C 端声明void old_calc(const double* a, int n, double* out);即可调用完全不需要改 Fortran 代码。参数说明bind(C, nameold_calc)让符号名不加下划线value让n直接传值。a(n)是显式形状数组用n说明长度C 端传入的数组必须至少有 n 个元素否则a(n)的边界检查不会作用在 C 调用的数据上可能越界读。4.2 用 ISO C Binding 暴露 Fortran 子程序给 C如果你手头是一个独立的 Fortran 模块并且希望 C 调用时不依赖任何 Fortran 语法知识建议在模块里定义一组包装子程序。下面是一个完整的接口模块module c_interface_wrap use iso_c_binding implicit none contains subroutine c_sum_array(ptr, len, result_out) bind(C, namec_sum_array) type(c_ptr), value :: ptr integer(c_int), value :: len real(c_double), intent(out) :: result_out real(c_double), pointer :: farr(:) real(c_double) :: s integer :: i call c_f_pointer(ptr, farr, [len]) s 0.0d0 do i 1, len s s farr(i) end do result_out s end subroutine end moduleC 端调用#include stdio.h void c_sum_array(const double* arr, int len, double* out); int main() { double a[4] {1,2,3,4}; double res; c_sum_array(a, 4, res); printf(%lf\n, res); return 0; }逻辑说明type(c_ptr)对应 C 的void*或const void*value告诉 Fortran 不要把这个指针当作 Fortran 引用参数而是直接取指针值。c_f_pointer是 Fortran 2003 标准内置子程序它把一个 C 地址转换成带 shape 的 Fortran 指针数组[len]指定形状为一维长度 len。注意farr是一个pointer属性变量不能用自动数组替代否则缺少数组描述符信息。参数说明c_sum_array的name没有加下划线所以 C 端nm查到的符号就是c_sum_array。C 端传入的数组必须是连续的不能像 C vector 那样交换内部布局否则 Fortran 读到的是错误的连续块。4.3 c2f.zip 类包常见的三个文件布局虽然我无法确定某个具体c2f.zip的内容但根据这类转换工具包的普遍形态它们通常会包含三个部分一个映射表一个生成脚本和一两个示例工程。映射表写清 C 类型到 Fortran kind 的对应关系生成脚本负责扫描.c文件中的函数原型并输出 Fortran 接口。示例工程则用来验证链接结果。文件/目录作用需要关注的坑type_map.txt维护double→real(c_double)等映射不含结构体对齐策略c2f.py/c2f.sh解析函数签名生成.f90不会处理宏展开#define会让解析崩掉examples/arrays演示数组参数传递默认按列优先生成但 C 源可能是行优先使用这类包时我建议把生成后的.f90文件用 gfortran 编译一遍再对照生成前的 C 符号表跑nm。如果符号名带了下划线而包脚本预期不含下划线立即检查是否用了bind(C)。5. 验证转换结果的 3 个技巧数值等价、内存布局与符号表检查5.1 用-Wall -Wconversion检查整型和浮点的静默损失Fortran 转 C 时最隐蔽的坑是整数除法。C 中1/2得 0Fortran 中1/2同样得 0但当你把 C 的double x 1/2;照搬到 Fortran 的real(8) :: x; x 1/2Fortran 侧先算整数除法再转换成浮点结果也是 0。这类问题编译器不会报错只能在边界处显式写.0。验证时用gfortran -c converted.f90 -Wall -Wconversion -Werrorconversion 21 | grep -E conversion|overflow-Wconversion会提示浮点数到整数的隐式转换和精度损失。配合-Werrorconversion可以把转换风险直接转化为编译错误强制修掉。5.2 用nm和readelf检查生成的符号名链接阶段失败大多因为符号名不匹配。跑一个最小的编译后检查gfortran -c conv.f90 -o conv.o nm conv.o | grep -i calc\|sum nm -C conv.o # 如果是 C 混乱名称查看修正后符号 objdump -d conv.o | grep call.*calc # 查看调用目标检查重点bind(C)生成的无修饰符号直接显示为calc_sum无bind的老式 Fortran 会显示calc_sum_C 端如果漏了extern CC 名字修饰符号会像_Z9calc_sumPKdi。看见这类符号就知道该去补extern C了。但注意nm命令本身属于工具检查不涉及任何系统漏洞。5.3 用随机输入矩阵对比两次运行的输出数值正确性验证不能只看链接成功。用相同种子生成随机输入分别在转换前后的两个可执行文件里运行然后逐位对比。我常用一个最小脚本验证#!/bin/bash # 转换前后各跑一次并输出二进制结果再比较 ./calc_original input.bin out_orig.bin ./calc_converted input.bin out_conv.bin cmp -l out_orig.bin out_conv.bin | head -20input.bin可以用下面的 Python 代码生成固定尺寸双精度数组import struct, random random.seed(42) with open(input.bin, wb) as f: for _ in range(16): f.write(struct.pack(d, random.uniform(-100, 100)))若cmp -l输出为空说明双精度 bit 完全一致。如果只有少数尾部位差很可能是中间计算中用了不同的优化级别如-O2下的 FMA 指令。要消除这种差异可以在 Fortran 侧把-ffp-contractoff关闭浮点收缩再与 C 侧在不开启 FMA 的条件下比较才能把“算法转换问题”与“编译器浮点优化”分开。把这一项加进比较脚本比单纯看数值接近更能定位真实误差来源。本文还有配套的精品资源点击获取
返回列表