PAYATHON 2026

如何为 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 源码作为类型定义的来源,可以让脚本只处理明确支持的声明:

  1. 找出带有 bind(c) 的派生类型。
  2. 按声明顺序读取成员名称和类型。
  3. 根据显式映射表生成 C 成员。
  4. 同时生成类型注册所需的元数据。
  5. 遇到不支持的语法立即报错,要求人工处理。

第一版可以只支持问题中的 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。扩展到数组、嵌套类型或更复杂成员时,再分别补充生成规则和内存约定。

备注:内容仅供参考。