本节摘要:标量与数组能通了,结构体怎么办?Fortran 派生类型跨语言有几个硬约束:不能带指针与 allocatable 组件、字符不能超过定长、布局要对齐。本节讲清 bind(c) 派生类型怎么映射 C 结构体,以及更实用的"不透明句柄"模式——把 Fortran 对象藏在 C 指针后面,跨语言只传句柄,彻底避开布局问题。
第 2 章讲过,派生类型可以把物理对象装进代码。但跨语言时,派生类型里有三件"行李"带不过去:指针组件(C 不懂 Fortran 指针)、allocatable 组件(涉及隐式描述符)、变长字符(C 用空字符结尾,规则不同)。bind(c) 的派生类型把这些排除在外——能映射 C 结构体的,只有标量组件、定长数组、定长字符与嵌套的 bind(c) 类型。
module struct_interop_mod use iso_c_binding, only: c_int, c_double implicit none ! Fortran 侧声明一个与 C 结构体对齐的类型 type, bind(c) :: point_t real(c_double) :: x real(c_double) :: y integer(c_int) :: id end type point_t end module struct_interop_mod program pass_struct use struct_interop_mod, only: point_t use iso_c_binding, only: c_double, c_int implicit none type(point_t) :: p p%x = 3.0_c_double p%y = 4.0_c_double p%id = 7_c_int ! 把这个类型的实例传给 C 侧的某个函数(接口声明见 7.1 的模式) write(*,*) 'point ready:', p%x, p%y, p%id end program pass_struct
C 侧对应的结构体:
typedef struct { double x; double y; int id; } point_t;
注意两点:一、组件顺序按声明排列,C 侧字段顺序必须一致;二、对齐规则由编译器决定,两边的默认对齐通常一致(double 对齐 8 字节),但跨平台前要核对——可以用 C 的 offsetof 和 Fortran 的 c_sizeof 对比验证。
结构体逐字段映射的痛点在于:字段一变,两边都要改;Fortran 内部的私有状态(allocatable 数组、派生类型嵌套)根本没法映射。工程上更常用的是不透明句柄(opaque handle)模式:Fortran 侧创建对象,返回一个 c_ptr 指向它;C 侧把这个指针当"身份证"存着,每次操作再传回 Fortran 侧解开。
module handle_mod use iso_c_binding, only: c_ptr, c_loc, c_f_pointer use iso_fortran_env, only: real64 implicit none type :: solver_handle_t ! 内部对象,C 侧看不到 real(real64), allocatable :: a(:,:) real(real64), allocatable :: b(:) real(real64), allocatable :: x(:) end type solver_handle_t contains function solver_create(n) result(h) bind(c, name="solver_create") integer, value :: n type(c_ptr) :: h type(solver_handle_t), pointer :: p allocate(p) allocate(p%a(n,n)); allocate(p%b(n)); allocate(p%x(n)) p%a = 0.0_real64; p%b = 0.0_real64; p%x = 0.0_real64 h = c_loc(p) ! 把对象地址装箱成 c_ptr end function solver_create subroutine solver_solve(h) bind(c, name="solver_solve") type(c_ptr), value :: h type(solver_handle_t), pointer :: p call c_f_pointer(h, p) ! 按 c_ptr 解回 Fortran 指针 ! 在 p 上做求解…… p%x = p%b end subroutine solver_solve subroutine solver_destroy(h) bind(c, name="solver_destroy") type(c_ptr), value :: h type(solver_handle_t), pointer :: p call c_f_pointer(h, p) deallocate(p) ! 谁创建谁销毁 end subroutine solver_destroy end module handle_mod
C 侧只看到三个函数和一把 void*:create 返回句柄,solve 消费句柄,destroy 释放。内部是 allocatable 数组还是指针、有没有嵌套类型,C 侧一概不知。这是面向对象(第 5 章)跨语言的正确姿势:内部 OOP 随便用,对外只暴露过程式接口加句柄。
逐字段映射时,最隐蔽的坑是对齐:C 编译器会在结构体里插 padding,Fortran 的 bind(c) 类型也按同样规则对齐,但两者"恰好一致"依赖平台。跨平台代码,别赌默认一致。两个核对手段:C 侧用 offsetof 宏打印每个字段偏移,Fortran 侧用 c_sizeof 与组件偏移对比;或者干脆都用 8 字节对齐的字段类型,把 padding 风险压到最低。另一个高频坑是字符:Fortran 定长字符在 bind(c) 里按固定长度映射,C 侧要按定长缓冲处理并手动补空字符结尾,别用 strlen 直接量 Fortran 传过来的字符数组。
两种方案的分界线是"数据结构稳不稳定"。结构体字段固定、跨语言传递频繁、且需要 C 侧直接访问字段——用 bind(c) 逐字段映射。对象内部状态复杂、字段可能演进、C 侧只做"调用操作"——用不透明句柄。科研项目里 90% 的场景是后者:C 或 Python 只想调用 Fortran 的求解能力,不需要掀开看内部。句柄模式的额外收益是 ABI 稳定:内部字段怎么改都不破坏 C 侧接口,这对"接口定了就不想再动"的长期库尤其值钱。
阅读完本节,你应当能够:
结构能通了,最后一步是把整条链接到 Python——7.3 节。