如何为 Fortran 派生类型自动生成对应的 C struct?
可以编写专用生成脚本。对于示例中的 bind(c) 类型,按成员声明顺序生成 C 结构体,并将 real(c_float) 映射为 C 的 float。为了便于长期维护,可以选定一份类型定义作为唯一来源,再自动生成 C 声明和库所需的注册信息。
是否需要 f2c,取决于是否还有转换 Fortran 程序的需求。对于这里的类型声明,可以先从一个范围明确的小型生成器入手。
先保证 Fortran 类型能够与 C 互操作
示例中的 c_float 应来自 iso_c_binding。完整声明可以写成:
module coordinate_types
use, intrinsic :: iso_c_binding, only: c_float
implicit none
type, bind(c) :: coordinatesfloat
real(c_float) :: x
real(c_float) :: y
end type coordinatesfloat
end module coordinate_types
对应的 C 头文件为:
#ifndef COORDINATE_TYPES_H
#define COORDINATE_TYPES_H
typedef struct coordinatesfloat {
float x;
float y;
} coordinatesfloat;
#endif
bind(c) 为符合互操作要求的派生类型提供与 C 结构体对应的布局规则,但不会自动生成 C 头文件。要使两者对应,成员类型和顺序必须一致,两侧还要使用兼容的目标平台和编译配置。
不要给 C 结构体额外添加 packed 属性或改变对齐规则,也不要把所有 Fortran real 都直接转换为 float。示例可以这样映射,是因为成员明确使用了 real(c_float)。
自动化可以从受限的生成器开始
如果必须以已有 Fortran 源码作为类型定义的来源,可以让脚本只处理明确支持的声明:
- 找出带有
bind(c)的派生类型。 - 按声明顺序读取成员名称和类型。
- 根据显式映射表生成 C 成员。
- 同时生成类型注册所需的元数据。
- 遇到不支持的语法立即报错,要求人工处理。
第一版可以只支持问题中的 real(c_float) 标量成员,等确有需求时再添加其他互操作类型和数组。
不建议用几个正则表达式尝试覆盖任意 Fortran 源码。续行、注释、同一行声明多个成员、预处理和 kind 名称的间接引用,都可能让简单匹配产生错误结果。输入格式固定时,小脚本可以胜任;输入来自任意项目时,需要能理解相关 Fortran 语法的解析过程。
不要对未知类型使用默认映射。生成失败比生成一个能够编译、却会错误解释内存的结构体更容易排查。
新增类型较多时,可以使用共享定义
如果能够调整开发流程,可以用一份独立描述同时生成 Fortran 和 C 声明。例如:
{
"name": "coordinatesfloat",
"fields": [
{ "name": "x", "type": "float32" },
{ "name": "y", "type": "float32" }
]
}
这里的 float32 是生成器自行约定的类型标识,生成规则将它分别映射到 real(c_float) 和 C 的 float。这种描述没有通用标准,需要在项目中明确其含义。
这种方式省去了从完整 Fortran 源码中提取类型的复杂度,代价是修改类型时需要先修改共享定义,还要将生成步骤接入构建流程。已有大量 Fortran 类型时,从源码提取通常更容易逐步接入。
类型注册接口应由库实际需要的信息决定
如果库只需要识别一个类型并按整体大小处理内存,注册信息可能只需类型名称和 sizeof。如果库还要访问成员,则可以增加成员名称、类型和偏移。
下面是一种自行设计的 C 元数据结构示例,并非现有库提供的 API:
#include <stddef.h>
#include "coordinate_types.h"
enum field_kind {
FIELD_FLOAT
};
struct field_desc {
const char *name;
enum field_kind kind;
size_t offset;
size_t size;
};
struct type_desc {
const char *name;
size_t size;
size_t field_count;
const struct field_desc *fields;
};
static const struct field_desc coordinatesfloat_fields[] = {
{
"x", FIELD_FLOAT,
offsetof(coordinatesfloat, x), sizeof(float)
},
{
"y", FIELD_FLOAT,
offsetof(coordinatesfloat, y), sizeof(float)
}
};
static const struct type_desc coordinatesfloat_desc = {
"coordinatesfloat",
sizeof(coordinatesfloat),
sizeof(coordinatesfloat_fields) /
sizeof(coordinatesfloat_fields[0]),
coordinatesfloat_fields
};
库的注册入口可以接收 const struct type_desc *,检查名称是否重复后保存描述信息。如果保存的是指针,需要保证描述及其成员表的生命周期足够长;示例使用静态对象,满足这一要求。
这些描述也应由生成器输出。结构体大小和成员偏移交给 C 编译器通过 sizeof、offsetof 计算,不要在脚本中按成员大小简单累加,因为结构体可能包含填充字节。
类型声明一致,还需要明确内存约定
生成对应的 struct 只解决了部分内存布局问题。库还需要约定:
- C 端拿到的是实际数据地址,还是需要另外处理的数组描述符。
- 若传入数组,元素数量、维度和步长如何提供。
- Fortran 分配的内存在 C 使用期间是否始终有效。
- 由哪一侧负责释放或重新分配内存。
不要直接对 Fortran 分配的对象调用 C 的 free()。通常应由 Fortran 侧提供与分配方式配套的释放入口。
对于当前示例,可以先实现一个受限生成器,从 bind(c) 类型声明生成 C 头文件与注册表,并通过一个小型互操作示例确认 C、Fortran 两侧能够正确读写 x 和 y。扩展到数组、嵌套类型或更复杂成员时,再分别补充生成规则和内存约定。
备注:内容仅供参考。
