《OpenFOAM 源码深度剖析》系列-第 1 章 OpenFOAM 项目架构与工程体系全景

2 阅读10分钟

第 1 章 OpenFOAM 项目架构与工程体系全景

本系列基于 OpenFOAM 官方仓库(openfoam-master,master 分支,版本名 WM_PROJECT_VERSION=v2606)的一手源码撰写。所有结论均可回溯到具体文件与行号,读者可自行打开源码核对。

1.1 为什么值得读 OpenFOAM 的源码

OpenFOAM(Open Field Operation and Manipulation)是工业界使用最广泛的开源计算流体力学(CFD)软件。它与多数"应用软件"不同:它并不是一个带图形界面的单体程序,而是一套用于离散化与求解偏微分方程(PDE)的 C++ 类库 + 一批可执行求解器/工具。用户既可以把它当作"软件"直接运行,也可以把它当作"框架"来编写自己的求解器。

这种"库 + 应用"的双重身份决定了它的架构特征:

  • 一切皆对象:网格、场、边界条件、离散格式、线性求解器、湍流模型、物性、时间推进策略,全部是运行期可选择的 C++ 对象。
  • 配置驱动:物理与数值选择写在字典(dictionary)文件里,而不是硬编码在程序中。fvSchemes 决定离散格式,fvSolution 决定线性求解器,turbulenceProperties 决定湍流模型。
  • 面向对象 + 泛型编程重度结合:Field<Type>、GeometricField<Type, PatchField, GeoMesh> 这样的多层模板,既保证了向量/张量/标量场的统一实现,又通过 C++ 模板实例化保证了数值性能。
  • 零拷贝哲学:tmp<T>、refPtr<T> 等引用计数/延迟拷贝机制,是整个框架性能的关键。

理解这套框架,最有效的方式是从构建系统出发,逆流而上:先看代码如何被组织与编译,再看基础设施(字典/IO/运行期选择),再看数值底座(基元/维度/场/内存),然后进入网格、有限体积离散、线性代数与并行,最后到达物理建模与求解器。本系列就按这条路线逐层展开。

1.2 顶层目录布局

打开仓库根目录,会看到如下结构(Get-ChildItem 或 ls 即可验证):

openfoam-master/
├── .gitlab/            # GitLab CI 配置
├── applications/       # 可执行程序(求解器、工具)
├── bin/                # 环境脚本(foamRun、foamEtcFile、foamCleanPath 等)
├── doc/                # 构建文档、Doxygen 配置
├── etc/                # 环境配置(bashrc、config.sh、controlDict、aliases)
├── etc-mingw/          # Windows/MinGW 交叉编译专用配置
├── modules/            # 官方维护的扩展模块(git submodule 形式)
├── plugins/            # 社区维护的插件
├── src/                # 所有库源码(核心)
├── tutorials/          # 算例(字典文件的"活文档")
├── wmake/              # 自研构建系统
├── Allwmake            # 顶层构建入口
├── Allwmake-modules
├── Allwmake-plugins
├── README.md
└── META-INFO/          # 版本元信息

这个布局本身就是一份架构地图:

目录职责关键点
src/全部库的 C++ 源码分层构建,依赖顺序严格
applications/可执行程序求解器与工具,链接 libOpenFOAM 等
wmake/构建系统替代 CMake 的自研多平台 Make 包装器
etc/运行环境与全局配置bashrc 定义 WM_* 变量
bin/Shell 工具脚本foamRun、foamEtcFile、foamCleanPath
modules/ plugins/扩展以 Allwmake 参与统一构建
tutorials/算例每个算例是一组字典文件

一个常被忽视却极其重要的设计是:src/ 与 applications/ 的物理分离。库只提供能力,应用只负责"选择能力 + 组织算法"。因此,我们读求解器代码(如 applications/solvers/incompressible/icoFoam/icoFoam.C)时,会发现它异常简短——因为它几乎所有的重活都委托给了库。

1.3 src/ 的库分层与依赖顺序

src/Allwmake(顶层库构建脚本)是理解库分层的最佳入口。它逐条 wmake 各子目录,顺序即依赖顺序。整理其核心片段(src/Allwmake 第 25–175 行):

# 基础库
wmakeLnInclude -u OpenFOAM
wmakeLnInclude -u OSspecific/"${WM_OSTYPE:-POSIX}"
OSspecific/"${WM_OSTYPE:-POSIX}"/Allwmake $targetType $*
Pstream/Allwmake $targetType $*
wmake $targetType OpenFOAM            # 核心库 libOpenFOAM

# 网格基础设施
wmake $targetType fileFormats
wmake $targetType surfMesh
wmake $targetType meshTools

# 有限体积与有限面积
wmake $targetType finiteArea
wmake $targetType finiteVolume
wmake $targetType fused/finiteVolume

# 网格生成/动网格
wmake $targetType mesh/blockMesh
wmake $targetType mesh/extrudeModel
wmake $targetType dynamicMesh
wmake $targetType genericPatchFields

# 并行分解与重编号
wmake $targetType parallel/decompose/decompositionMethods
renumber/Allwmake $targetType $*
parallel/Allwmake $targetType $*

# ... 物理建模
thermophysicalModels/Allwmake $targetType $*
TurbulenceModels/Allwmake $targetType $*
wmake $targetType combustionModels
...

由此可以画出 OpenFOAM 的库依赖有向图(自底向上):

                    applications/ (solvers / utilities)
                                │
   ┌────────────────────────────┼─────────────────────────────┐
   │                            │                             │
combustionModels   TurbulenceModels   thermophysicalModels ...  functionObjects
   │                            │                             │
   └────────────┬───────────────┴──────────────┬──────────────┘
                │                              │
           finiteVolume  ◄──────────────►  fused (算子融合)
                │
        meshTools / dynamicMesh / surfMesh
                │
        lduPrimitiveMeshAssembly (有限体积到 LDU 的桥)
                │
            OpenFOAM (核心库)  ◄──── filesFormats / OSspecific
                │
             Pstream (并行通信)
                │
            global / primitives / memory ...

几个值得注意的细节:

  1. OpenFOAM 与 Pstream 的循环依赖。核心库与并行通信库互相需要对方的符号。源码里对 MinGW(Windows 交叉编译)做了特殊处理(src/Allwmake 第 33–80 行):先把 Pstream 编成单个 .o(libo 目标),再让 OpenFOAM 链接 libPstream.o,最后重新链接。这段注释明确写着 Handle Pstream/OpenFOAM cyclic dependency。这是"库分层"并非严格无环的一个真实例证。

  2. wmakeLnInclude -u <dir> 先于编译执行。OpenFOAM 采用"扁平化头文件"策略:把库内所有 .H 文件通过符号链接汇集到一个 lnInclude/ 目录。这样库与库之间只需 -I.../lnInclude 就能找到所有头文件,无需在源码里写深路径。src/Allwmake 第 25–26 行、第 111、134–135 行都在做这件事。

  3. Allwmake 是"分层的":顶层 src/Allwmake 调用子目录的 Allwmake(如 thermophysicalModels/Allwmake),子目录再调用更下层。这种递归组织使每个库可以自管理"先编译谁"。

  4. 可选的第三方依赖用 have_* 探测。wmake/scripts/ 下有一组 have_scotch、have_metis、have_parmetis(在别处)、have_hypre、have_cgal、have_boost、have_fftw、have_adios2 等脚本,构建时探测系统是否具备这些库,具备则启用对应功能(如区域分解可用 SCOTCH/METIS)。

1.3.1 核心库 src/OpenFOAM 的内部结构

src/OpenFOAM/ 是整个框架的"操作系统",其子目录(ls src/OpenFOAM)揭示了基础设施的全貌:

algorithms/         # 图算法、索引、排序
compat/             # 兼容层
containers/         # List / UList / SubList / DynamicList / HashTable / DLList ...
db/                 # 字典 + IO + 对象注册表 + Time (数据库层)
dimensionedTypes/   # 带物理维度的量
dimensionSet/       # 七基本量维度系统
expressions/        # 表达式解析(string->表达式)
expressionTemplates/# 表达式模板(算子融合的编译期基础)
fields/             # Field / FieldField 模板体系
global/             # 全局函数、Foam 命名空间初始化、version
include/            # 宏、常量的集中定义 (.H),被全局包含
interpolations/     # 插值基元(面向标量/向量)
matrices/           # lduMatrix 及线性求解器
memory/             # autoPtr / tmp / refPtr / refCount
meshes/             # polyMesh / primitiveMesh / 几何计算
orientedType/       # 有向性标记(wall/symmetry 等)
parallel/           # 并行分解、通信原语
primitives/         # scalar/label/vector/tensor/symmTensor...

其中 db/(database,数据库层)是 OpenFOAM 的灵魂——它不仅管 IO,还管对象注册表(objectRegistry)与运行期选择(RTS)。这一层将用整整两章(第 3、4 章)展开。

1.4 C++ 技术选型:为什么"像现在这样"

OpenFOAM 是 C++ 老派工程风格的集大成者。它没有用 CRTP、没有用 std::function 大量替换虚函数、也没有用现代 Ranges,而是坚持一套自洽的技术栈:

1.4.1 运行期选择(RTS)取代"配置硬编码"

物理/数值模型的种类在编译期已知(都是 C++ 类),但在运行期才能确定用哪一个(由字典决定)。OpenFOAM 用一套宏实现的"抽象工厂 + 字符串注册表"解决:

  • 每个模型类在头文件里用 declareRunTimeSelectionTable 声明一张"构造表";
  • 在源文件里 addToRunTimeSelectionTable(Base, Derived, word) 把派生类注册进去;
  • 基类通过 Base::New(dict) 按字典中的名字查表构造。

这套机制贯穿全书:边界条件、湍流模型、离散格式、线性求解器、物性、函数对象,全部由此驱动。

1.4.2 表达式模板(Expression Templates)与算子融合

Field<Type> + Field<Type> 如果天真实现会产生大量临时对象。OpenFOAM 用表达式模板在编译期把 a = b + c*d 折叠成单次遍历、无中间临时的表达式树;并在 src/fused/ 里进一步引入 fusedMultiplyAdd 三目融合,用于有限体积与湍流的高频内层循环。这部分在第 3 章(核心数据类型)与第 7 章(离散算子)中详述。

1.4.3 引用计数与延迟拷贝:tmp<T>

tmp<T> 是 OpenFOAM 里最常见的"看似智能指针、实则零拷贝开关"的类型。它表达"这个临时对象可以被复用(reuse)也可以被共享(ref)"。掌握 tmp 的 ref()/reuse()/ptr() 语义,是读懂 fvm::/fvc:: 算子签名的前提。

1.4.4 命名约定

OpenFOAM 有一套极其稳定的命名约定,读懂它就能"望文生义":

约定含义例子
.H / .C头文件 / 实现文件分离dictionary.H / dictionary.C
TypeName("xxx")类名宏,用于 RTS 与类型名打印TypeName("kEpsilon")
namespace Foam所有类都在 Foam 命名空间Foam::fvMesh
xxxFwd.H前向声明的轻量头文件volFieldsFwd.H
I / O 前缀输入 / 输出Istream / Ostream
Ptr 后缀智能指针管理autoPtr、faceFluxCorrectionPtr_
_ 后缀类的数据成员(私有)psi_、diag_、boundary_
camelCase 方法名公有接口方法correctBoundaryConditions()
ALL_CAPS 宏宏常量MUST_READ、AUTO_WRITE

1.4.5 全局库初始化与 #include 的"隐式依赖"

OpenFOAM 的程序启动时会自动初始化全局对象。这来自 src/OpenFOAM/global/ 与 src/OpenFOAM/include/,其中 src/OpenFOAM/include/OpenFOAM.H 之类的头文件被广泛包含。这类"全局注入"是 OpenFOAM 便捷性(开箱即用)与可读性(隐式依赖)的双刃剑。

1.5 编码约定与"看代码的方法论"

读 OpenFOAM 的源码有一条稳定套路:

  1. 先看 .H 的类声明——成员函数名与数据成员就说明了一切。文件顶部注释块给出 Description、SourceFiles、See also,非常可靠。
  2. 再看 .C 的构造与 New——RTS 的注册行(addToRunTimeSelectionTable)通常在文件中部。
  3. 追 TypeName 与 declareRunTimeSelectionTable——确定这个类属于哪张选择表。
  4. Info / FatalError / DebugInfo——Info 是标准输出日志(等价 std::cout 但带前缀),FatalErrorInFunction 是异常式终止,DebugInfo 受 debug 开关控制。它们本身就是"运行时行为"的观测点。
  5. dimensionCheck 与 dimensionedScalar——凡是物理量,必带维度,一旦量纲不匹配会直接报错。

1.6 版本与编译信息:构建元数据

OpenFOAM 把构建元数据嵌进每个可执行文件。README 中说明,blockMesh -help 会打印:

Using: OpenFOAM-com (2012) - visit www.openfoam.com
Build: b830beb5ea-20210429 (patch=210414)
Arch:  LSB;label=32;scalar=64

这背后是一套脚本:wmake/scripts/wmake-build-info、bin/foamEtcFile(提供 -show-api、-show-patch)、以及 wmake -build-info 子命令。在尚未编译时,也能查询当前 api/patch/branch。本仓库为 master 分支,WM_PROJECT_VERSION=v2606,可在 etc/bashrc 第 58 行确认。

编译信息还写进源码目录的 META-INFO/,并在 src/OpenFOAM/global/ 中生成 buildInfo/version 相关常量。当你在日志里看到 Arch: LSB;label=32;scalar=64 时,它意味着:label 是 32 位(int),scalar 是 64 位(double),这直接对应 etc/bashrc 中的 WM_LABEL_SIZE=32 与 WM_PRECISION_OPTION=DP。

1.7 三层"世界观"总结

把 1.2–1.4 浓缩成一张表,OpenFOAM 的工程哲学可以概括为三层世界:

层代表问题它解决
运行环境层etc/ bin/ wmake/让同一份源码在不同平台/编译器下构建、运行
基础设施层src/OpenFOAM/db、memory、fields字典、IO、对象注册表、RTS、场与内存
数值方法论层src/finiteVolume、src/OpenFOAM/matrices有限体积离散、线性代数、并行
物理应用层src/TurbulenceModels、thermophysicalModels、applications具体物理模型与求解器

下一章将深入第二层的入口——构建系统 wmake,看它是如何把这一整套分层在数十种平台组合下可靠地编译出来的。


本章要点回顾

  • OpenFOAM = 库(提供能力) + 应用(选择与编排能力),src/ 与 applications/ 物理分离。
  • src/Allwmake 的构建顺序即依赖顺序;wmakeLnInclude 用符号链接实现"扁平头文件"。
  • 核心技术栈:运行期选择(RTS)、表达式模板、tmp<T> 零拷贝、维度系统。
  • 稳定的命名约定(TypeName、_ 后缀、I/O 前缀、Fwd.H)让代码"望文生义"。
  • 版本与编译元数据通过 wmake -build-info、foamEtcFile、META-INFO/ 嵌入。