7.2 复杂数据结构跨语言传递


7.2 复杂数据结构跨语言传递

本节摘要:标量与数组能通了,结构体怎么办?Fortran 派生类型跨语言有几个硬约束:不能带指针与 allocatable 组件、字符不能超过定长、布局要对齐。本节讲清 bind(c) 派生类型怎么映射 C 结构体,以及更实用的"不透明句柄"模式——把 Fortran 对象藏在 C 指针后面,跨语言只传句柄,彻底避开布局问题。

结构体跨语言:三件不能带的行李

第 2 章讲过,派生类型可以把物理对象装进代码。但跨语言时,派生类型里有三件"行李"带不过去:指针组件(C 不懂 Fortran 指针)、allocatable 组件(涉及隐式描述符)、变长字符(C 用空字符结尾,规则不同)。bind(c) 的派生类型把这些排除在外——能映射 C 结构体的,只有标量组件、定长数组、定长字符与嵌套的 bind(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 侧接口,这对"接口定了就不想再动"的长期库尤其值钱。

学习目标

阅读完本节,你应当能够:

  1. 说出 bind(c) 派生类型的组件约束
  2. 写一个 bind(c) 派生类型映射 C 结构体,并双向传递
  3. 用 c_ptr 句柄隐藏 Fortran 对象,实现 C 侧的完整操作
  4. 处理结构体对齐与顺序差异

本节要点回顾

  • bind(c) 类型有三不能带:指针、allocatable、变长字符组件
  • 逐字段映射要核对顺序与对齐:c_sizeof 与 offsetof 对表,别赌平台默认
  • 不透明句柄是工程主力:c_loc 装箱、c_f_pointer 解箱,C 侧只见 void 指针
  • 谁创建谁销毁:句柄的生命周期契约要写进接口文档
  • 内部 OOP、外部过程式:对象封装留给 Fortran,对外只暴露句柄加函数
  • ABI 稳定是句柄的隐藏收益:内部字段演进不破坏 C 侧接口

结构能通了,最后一步是把整条链接到 Python——7.3 节。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U