第 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 ...
几个值得注意的细节:
-
OpenFOAM与Pstream的循环依赖。核心库与并行通信库互相需要对方的符号。源码里对 MinGW(Windows 交叉编译)做了特殊处理(src/Allwmake第 33–80 行):先把Pstream编成单个.o(libo目标),再让OpenFOAM链接libPstream.o,最后重新链接。这段注释明确写着Handle Pstream/OpenFOAM cyclic dependency。这是"库分层"并非严格无环的一个真实例证。 -
wmakeLnInclude -u <dir>先于编译执行。OpenFOAM 采用"扁平化头文件"策略:把库内所有.H文件通过符号链接汇集到一个lnInclude/目录。这样库与库之间只需-I.../lnInclude就能找到所有头文件,无需在源码里写深路径。src/Allwmake第 25–26 行、第 111、134–135 行都在做这件事。 -
Allwmake是"分层的":顶层src/Allwmake调用子目录的Allwmake(如thermophysicalModels/Allwmake),子目录再调用更下层。这种递归组织使每个库可以自管理"先编译谁"。 -
可选的第三方依赖用
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 的源码有一条稳定套路:
- 先看
.H的类声明——成员函数名与数据成员就说明了一切。文件顶部注释块给出Description、SourceFiles、See also,非常可靠。 - 再看
.C的构造与New——RTS 的注册行(addToRunTimeSelectionTable)通常在文件中部。 - 追
TypeName与declareRunTimeSelectionTable——确定这个类属于哪张选择表。 Info/FatalError/DebugInfo——Info是标准输出日志(等价std::cout但带前缀),FatalErrorInFunction是异常式终止,DebugInfo受debug开关控制。它们本身就是"运行时行为"的观测点。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/嵌入。