OpenFOAM Sharing Programming 开发编程基础00 基本实现和开发
参考:
- https://github.com/UnnamedMoose/BasicOpenFOAMProgrammingTutorials
- https://www.topcfd.cn/simulation/solve/openfoam/openfoam-program/
- https://www.tfd.chalmers.se/~hani/kurser/OS_CFD/
- https://github.com/ParticulateFlow/OSCCAR-doc/blob/master/openFoamUserManual_PFM.pdf
- https://www.youtube.com/watch?v=KB9HhggUi_E&ab_channel=UCLOpenFOAMWorkshop
- http://dyfluid.com/#
感谢原作者们的无私引路和宝贵工作。
前置:
https://aerosand.cn/categories/CFD/强烈建议在学习过计算流体力学基础以及有限体积法之后再开始本系列学习。至少也要同时开始,不然最后可能很难理解 OpenFOAM 求解器标准算法。
OpenFOAM 是什么呢?引用 wiki 解释如下
OpenFOAM (for “Open-source Field Operation And Manipulation”) is a C++ toolbox for the development of customized numerical solvers, and pre-/post-processing utilities for the solution of continuum mechanics problems, most prominently including computational fluid dynamics (CFD).
我们从简单的 C++ 程序实现开始,简单了解编译原理,通过 makefile 逐渐掌控我们的项目,过渡到了解 OpenFOAM 的 Make 实现方式,然后认识 OpenFOAM 的基本程序,最后逐渐深入 OpenFOAM 的程序开发。
鉴于 OpenFOAM 的使用环境,我们选择在 ubuntu 22.04 系统环境中,基于 OpenFOAM 2306 版本进行开发,方便起见使用 vscode 工具。
OpenFOAM 的安装,vscode 的安装,快捷命令的自定义等讨论参见 OpenFOAM 环境准备 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 (aerosand.cn)
原生实现
我们使用 C++ 写一段简单的 “hello world” 程序。
hello world
对于 OpenFOAM 来说,不管是求解器还是算例,放在任何一个文件夹都可以。放在
$FOAM_RUN
路径下也是为了方便管理。比如,此系列讨论中,我们把项目放在自建文件夹
/userPath/ofsp
之下(/userPath/
即用户自定义的文件夹路径,用户替换成自己的路径),并定义了快捷命令ofsp
快速跳转到该路径下。快捷命令的自定义使用请参考 OpenFOAM 环境准备 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 (aerosand.cn)我们使用如下约定
userPath/
:用户指定路径,该路径下再新建例如userPath/ofsp/
文件夹,ofsp/
下再新建各个项目userProject/
:用户指定项目路径,在userPath/ofsp/userProject/
文件下存放具体的项目文件
例如,新建总项目文件夹(// terminal
表示下面命令需要在终端输入执行)
1 | // terminal |
ofsp
即OpenFoam sharing programming
的缩写
在用户路径下,建立本章节的项目文件夹。
详细终端命令演示如下
1 | // terminal |
通过 vscode 打开项目后,可以使用 ctrl + ~
唤出 vscode 的终端控制台,快捷进行命令操作。
终端中使用命令新建文件
1 | // terminal |
终端中可以使用 tree
命令查看文件树状结构(命令参考 OpenFOAM 常用指令 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 (aerosand.cn))
最后,我们能看到文件结构为
1 | |- ofsp_00_helloWorld_cpp/ |
我们约定在文件表示中,名称后加了
/
的表示文件夹,没有的表示文件。
我们分别写入示例代码,内容如下
类的声明 Aerosand.h
1 | // Aerosand.h |
代码风格:
本系列讨论的代码尽量贴近 OpenFOAM 的代码风格,比如私有成员变量名称尾缀下划线。
类的定义 Aerosand.cpp
1 | // Aerosand.cpp |
主源码 main.cpp
1 | // main.cpp |
虽然我们笼统的把代码到程序的整个过程称为“编译”,实际上,在 Linux 系统下,C++ 程序的编译过程分为四个过程。
1 | 预处理 -> 编译 -> 汇编 -> 链接 |
我们将逐步拆解并实现各个环节。
预处理
将程序代码中包含的 #include
文件插入替换到原文件中,在 Linux 系统下生成中间预处理输出文件(后缀 .i
文件)。
我们在终端里通过命令执行这一过程
1 | // terminal |
- g++ 的
-E
标识指定编译器进行预处理- g++ 的
-o
(小写)标识指定编译器生成文件的名称
结果生成 Aerosand.i
和 main.i
文件。
后缀
.i
表示 intermediate preprocessor output 中间预处理输出
编译
编译器对预处理后的中间预处理输出文件进一步语法分析,在 Linux 系统下生成汇编语言形式的源文件(后缀 .s
文件)。
终端中执行编译过程
1 | // terminal |
g++ 的
-S
标识指定编译器进行编译(大写S
)
结果生成 Aerosand.s
和 main.s
文件。
后缀
.s
(小写)表示 source code written in assembly 汇编语言形式的源文件
汇编
编译器把编译后的汇编语言形式的源文件转换成可执行机器指令,在 Linux 系统下生成目标文件。
终端中执行汇编过程
1 | // termianl |
g++ 的
-c
(小写)标识指定编译器进行汇编
结果生成 Aerosand.o
和 of00_0.o
文件。
后缀
.o
(小写)表示 object file 目标文件
动态库
当项目中有大量“类”的时候,我们希望某些“类”能固定下来提供某种“方法”,这种“方法”就形成一个可以重复使用的“库”(library)。当其他项目使用这个库的时候,库本身无需再次“预处理”,“编译”以及“汇编”,仅仅和这个项目链接即可。
因为静态库开销大,浪费空间,更新维护困难,所以 OpenFOAM 大量使用动态库,我们这里也只以动态库为例。
动态库在程序编译时并不链接到目标代码,而仅仅在程序运行时才被链接载入。不同的程序如果调用相同的库,那么内存里只需要一份该动态库的可分享实例,这样就大大减少了空间浪费。此外,因为动态库仅在程序运行时才被链接载入,所以库的单独维护更新也十分方便。
编译器可以对汇编后的 .o
目标文件进行整理形成动态库,在 Linux 系统下生成 .so
文件。
终端中执行生成动态库命令
1 | // terminal |
- g++ 的
-shared
标识指定生成动态链接库- g++ 的
-fPIC
标识指定创建与地址无关的编译程序,f
即 file,PIC
即 position independent code- 动态库文件以
lib
开头
生成 Aerosand.so
文件,该文件就是可链接的动态库。
后缀
.so
代表着 shared object 共享目标
链接
编译器把所有的目标文件以及库文件都组织成一个可以执行的二进制文件。
使用 echo
命令查看原本动态库链接路径,可以发现并不是项目本地路径。临时指定动态库路径为当前文件夹(不要担心,临时指定不影响 OpenFOAM 动态库路径的环境配置)。
1 | // terminal |
终端中将主源码目标文件和动态库链接,生成可执行文件
1 | // terminal |
- g++ 的
-L
标识紧跟指定的库的路径, 使用-L.
表示动态库在当前路径- g++ 的
-l
标识紧跟指定的库的名称,使用时省略lib
字段
最终生成 main
可执行程序。
注意,上面指定的动态库路径是临时的,如果重启计算机,想要再次运行
main
程序,必须要再次指定动态库路径。
需要知道的是,无论是把新开发的库放在本项目下,或是其他特定路径下,每个项目都可以链接使用这个动态库,只要指定正确的链接路径。这也是动态库“相对独立”“自由链接”的意义所在。
运行
总结整个过程如下
运行该先项目
1 | // terminal |
运行结果为
1 | Hi, OpenFOAM! Here we are. |
make 实现
上一节的编译过程虽然清晰,但是一步一步的执行十分繁琐。
为了简化项目编译,以及兼顾理解编译细节,我们采用 make 工具来管理我们的开发项目。很多人也会使用 cmake 来构建项目,cmake 更加简洁高效,不过本质上也是基于 makefile 的。
我们可以为项目提供 makefile 文件通用的描述所有执行步骤。这样只需要简单执行 makefile 文件就可以编译整个项目,大大方便调试运行。此外,上面项目的所有代码文件都放在一起,十分不方便,所以我们对代码进行分类管理。
项目准备
我们建立新项目 ofsp_00_helloWorld_make
,
1 | // termianl |
文件结构如下:
1 | |- ofsp_00_helloWorld_make/ |
在我们早期开发的很多情况下,开发的库只是某个项目的特定库。所以,我们仍然把包含众多 类
形成的 开发库
作为一个文件夹放进开发项目内。项目根目录下有项目主源码, 项目的 项目 makefile
,以及开发库文件夹。开发库文件夹内有库自己的 库 makefile
。文件结构清晰,层次明确。
因为头文件路径有变化,主源码头文件包含修改为
1 |
其他代码的内容保持不变。
库 makefile
库的 makefile 只负责库的编译,目的是得到动态库,方便主程序链接使用。
makefile 文件将编译过程简化成“编译”和“链接”两部分,不再展开“预处理”,“汇编”的过程。编译过程把源代码编译成 .o
目标文件或者库文件等中间文件,“链接”过程把中间文件链接成最后可执行程序。
makefile 文件的基本格式为
1 | <target>: <support> |
基于前文对 C++ 项目编译原理的过程的讨论,库 makefile 可以非常直白的写成
1 | libAerosand.so: Aerosand.o |
- 顺序重要。先指定动态库的编译,再指定汇编
.o
文件的编译。<support>
部分不包括.h
文件
进入库文件夹,执行 makefile
1 | // terminal |
清理的时候自行删除
.o
文件和.so
文件即可。
可以看到库文件夹下生成了我们需要的 libAerosand.so
文件。
当代码文件繁多的时候,这么直白的写 makefile 效率低,不利于维护,我们优化 makefile 写法到通用形式
1 | # MACRO |
希望读者不要对 makefile 的写法感到担心,需要知道的都写在本文里了,陌生的语法知道可以这么使用即可,不需要花费时间了解原因和原理。
给出必要的解释如下:
第一段定义了一些命令的宏变量,方便第二段命令的书写。注意获取当前文件夹名称
NAME
的写法,获取当前路径下所有源文件SOURCE
的写法。第二段使用宏变量写了编译命令,本质上和原生写法没有什么区别。
第三段自定义了
make clean
命令。注意Linux
系统下常见的类似写法,例如*.so
指代所有的.so
文件。
动态库编译(确认在库路径下)
1 | // terminal |
结果生成了动态库 userPath/ofsp_00_helloWorld_make/Aerosand/libAerosand.so
文件(注意路径)。
项目 makefile
有了上节的经验,我们直接写项目 makefile 文件
1 | # MACRO |
如果主源码没有先行单独的生成目标文件,而是通过
-c
标识把编译和链接放在一句指令内写完,编译后的文件将无法执行,这是因为-c
标识指定的生成文件不能直接运行。
程序编译(注意要离开库文件夹, cd
到本项目根目录下)
1 | // terminal |
注意,makefile 中的 compile
部分临时指定了动态库链接路径。
运行结果如下
1 | Hi, OpenFOAM! Here we are. |
vscode 插件
这里插入一点题外话。
对于一般的 C++ 项目,可以使用 vscode 的插件 C/C++ Project Generator
,操作和备注如下
- 基于 makefile 的多文件项目模版
- 使用
F1
打开快捷命令输入,使用Create C++ project
- 弹出窗口中选择到准备好的空白项目文件夹,并打开
- 在
src/main.cpp
中开发主函数代码 - 在
include/
路径下开发头文件的定义,在src/
路径下开发头文件的实现 - 终端使用命令
make
编译此项目,make run
编译并运行,make clean
清理项目 - 也可以使用
./output/main
直接运行该主程序
也可以使用 vscode 的插件 c cpp cmake project
,操作和备注如下
- 基于
CMakeList
的多文件项目模板 - 使用
F1
打开快捷命令输入,使用CMake Project: Create Project
- 弹出窗口中选择到准备好的空白项目文件夹,并打开
- 在
src/main.cpp
中开发主函数代码 - 在
include/
路径下开发头文件的定义,在src/
路径下开发头文件的实现 - 终端使用命令
cmake build/
进行项目编译 - 终端使用命令
make -C build/
进行项目最后编译 - 终端使用命令
./build/xxx
运行该主程序
wmake 实现
在理解了 C++ 项目的 make
实现方式之后,我们现在可以更加容易的明白 OpenFOAM 提供的 wmake
实现方式。
可以找到 OpenFOAM 的 wmake
文件夹。
1 | // terminal |
大概扫一眼文件构成,简单来说, OpenFOAM 使用 wmake
管理应用,wmake
是基于 make
的一个脚本,但是提供了针对 OpenFOAM 的特性。
OpenFOAM 使用 Make/files
和 Make/options
实现 wmake
的编译管理,下面我们具体使用一下就会明白。
OpenFOAM 约定源文件后缀为 .C
,头文件后缀为 .H
。为了让读者不要对文件架构感到困扰,这里作了不严谨的区分。OpenFOAM “应用层面” 的 .H
文件更多只是为了对主源码按功能进行拆分,方便代码阅读和维护,很多并不是类的头文件。这和 C++ 开发层面的头文件以及 OpenFOAM “源码层面”的头文件(如 $FOAM_SRC/OpenFOAM/dimensionSet.H
)有些不同。
本文下面讨论的都是“源码层面”的头文件,暂无“应用层面”的头文件。
项目准备
OpenFOAM 中,应用(application)包括
- 求解器(solvers)
- 工具(tools)
- 实用程序(utilities)
在本系列讨论中,我们所说的 “项目” 是完整解决某个问题的所有内容,包括
- 求解器
- 调试算例
- 调用库
- 工具
- 实用程序
- 等等
建立应用(application)/userPath/ofsp/ofsp_00_helloWorld_wmake
。进入该文件夹,写入源代码。
1 | // terminal |
先行给出代码内容如下:
Aerosand 库头文件
1 | // Aerosand.H |
Aerosand 库源文件
1 | // Aerosand.C |
应用主源码
1 | // ofsp_00_helloWorld_wmake.C |
- 注意每个代码文件的最后都需要留个一个空白行,否则 OpenFOAM 会警告
parse error
- 类的头文件不再需要相对路径
OpenFOAM 提供了 Make 文件来帮助开发,其中
/Make/files
指定编译需要的所有源文件和目标路径以及目标文件名称/Make/options
指定 application 或者库所需要包含的头文件、链接的其他库文件(包含路径)
类似把 makefile 文件换成了 OpenFOAM 的 Make 文件
硬链接
硬链接并不需要单独编译库文件,而是将库文件直接包含在主应用上,所以这种情况不需要准备库自己的 Make 文件。
代码文件结构如下
1 | |- ofsp_00_helloWorld_wmake/ |
文件 ofsp_00_helloWorld_wmake/Make/files
内容如下
1 | Aerosand/Aerosand.C |
此应用需要“包括”自定义库 Aerosand。
这里用词使用“包括”而不是“链接”,为了强调 Aerosand 并没有生成自己的独立库文件(如静态库
.a
文件或者动态库.so
文件)
文件 ofsp_00_helloWorld_wmake/Make/options
内容如下
1 | EXE_INC = \ |
因为前文临时定义了库的链接路径,这里需要可能重启终端,以恢复 OpenFOAM 的环境配置。
执行编译
1 | // terminal |
终端输出信息有三段,对应着应用编译的三个过程。
第一段是自定义的 Aerosand 库编译得到目标文件 Aerosand.o
(见输出信息的末尾处)
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -IAerosand -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -c Aerosand/Aerosand.C -o Make/linux64GccDPInt32Opt/Aerosand/Aerosand.o |
第二段是主源码编译得到目标文件 of00_2.o
(见输出信息的末尾处)
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -IAerosand -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -c ofsp_00_helloWorld_wmake.C -o Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o |
第三段是链接的过程,链接自定义库或者其他的动态链接,最终生成可执行文件(见输出信息的末尾处)。
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -IAerosand -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -Xlinker --add-needed -Xlinker --no-as-needed Make/linux64GccDPInt32Opt/Aerosand/Aerosand.o Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o -L/usr/lib/openfoam/openfoam2312/platforms/linux64GccDPInt32Opt/lib \ |
编译的过程文件在 ofsp_00_helloWorld_wmake/Make/linux64GccDPInt32Opt/
文件夹下(根据平台可能会有所不同)。编译形成的可执行程序在 $FOAM_USER_APPBIN
文件夹下。
这个可执行程序不需要从任何外部文件读取参数,可以在任何路径下通过终端命令直接运行。
我们可以直接运行这个应用
1 | // terminal |
运行结果如下
1 | Hi, OpenFOAM! Here we are. |
动态库
在实际的开发中,我们还是要使用动态库来保证内存和效率。
我们调整文件结构如下
1 | |- ofsp_00_helloWorld_wmake/ |
注意此时的文件结构,库自己单独拥有一套
库 Make
。
库 Make
为库创建 Make 文件
文件 ofsp_00_helloWorld_wmake/Aerosand/Make/files
内容如下
1 | Aerosand.C |
- 注意库使用
LIB
而不是EXE
- 库的目标路径结尾是
LIBBIN
- 目标文件需要加
lib
因为这个 Aerosand 库的实现不再进一步需要链接其他库,所以 userApp/Aerosand/Make/options
直接空置即可。
根目录下编译这个库
1 | // terminal |
终端输出信息有两段,我们关注每段输出的最后部分
第一段是自定义的 Aerosand 库编译得到目标文件 Aerosand.o
(见输出信息的末尾处)
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -c Aerosand.C -o Make/linux64GccDPInt32Opt/Aerosand.o |
第二段将目标文件 Aerosand.o
编译成动态库 Aerosand.so
(见输出信息的末尾处)
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -shared -Xlinker --add-needed -Xlinker --no-as-needed Make/linux64GccDPInt32Opt/Aerosand.o -L/usr/lib/openfoam/openfoam2312/platforms/linux64GccDPInt32Opt/lib \ |
可以通过命令
cd $FOAM_USER_LIBBIN
确认 Aerosand 库的编译结果文件libAerosand.so
的所在位置。
因为库内的类可能会有多个,甚至还有其他子库,所以库编译后会同时在库的目录下生成 lnInclude
文件夹,lnInclude
包含了该库所有类(/子库)的声明( .H
文件)或者实现 .C文件
的快捷方式,方便后续链接的时候可以提供统一路径。可以参考 OpenFOAM 的 $FOAM_SRC/OpenFOAM
库,可以看到根目录下有 lnInclude
文件夹,其中包含了此库内的所有类(/子库)的快捷方式。
我们在终端使用
find
命令搜索 OpenFOAM 源代码文件的时候,往往能看到相近的路径lnInclude/
下也有一个同名文件,这个就是快捷方式。我们一般选择打开阅读本体。
应用 Make
因为 Aerosand 库已经被编译成了动态库,所以我们需要在应用的 Make/options
文件中为应用提供动态库链接。Make/files
指定的编译不再需要该动态库的源代码。
修改 userApp/Make/files
文件
1 | ofsp_00_helloWorld_wmake.C |
修改 userApp/Make/options
文件
1 | EXE_INC = \ |
标识符号
EXE_INC
指定需要包含的库-I
同样用来指定包含“库.H
”,从lnInclude
文件夹取得路径
EXE_LIB
指定需要链接的库-L
同样用来指定链接“库文件.so
”的路径-l
同样用来指定链接“库文件.so
”的名称(字母是 link 的 l,不是 include 的 i)
我们常见的 OpenFOAM 求解器的
Make/options
中没有-L
指定只有-l
指定,这是因为那些求解器中使用的都是原生库,链接路径已经得到配置,无需进行-L
路径指定,仅进行-l
名称指定即可。如果是用户自定义库,编译结果在其他文件位置,当然也需要进行-L
路径指定。一定要注意路径,路径对于成功编译非常重要。一般指定的路径都是从本地出发的(可以从本地路径出发,定义其他库的绝对路径,见下文演示)。
编译运行
编译应用
1 | // terminal |
此时终端信息分为两段,第一段将主源码编译成目标文件,第二段链接动态库到应用,生成可执行文件。
第一段是主源码编译得到目标文件 ofsp_00_helloWorld_wmake.o
(见输出信息的末尾处)
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -IAerosand/lnInclude -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -c ofsp_00_helloWorld_wmake.C -o Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o |
第二段链接动态库到应用,并生成可执行文件 ofsp_00_helloWorld_wmake
(见输出信息的末尾处两句)
1 | g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3 -DNoRepository -ftemplate-depth-100 -IAerosand/lnInclude -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude -fPIC -Xlinker --add-needed -Xlinker --no-as-needed Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o -L/usr/lib/openfoam/openfoam2312/platforms/linux64GccDPInt32Opt/lib \ |
可以通过
cd $FOAM_USER_APPBIN
确认应用的可执行程序被编译到了哪里。
运行编译生成的应用程序
1 | // terminal |
运行结果如下
1 | Hi, OpenFOAM! Here we are. |
结果和前面各种方式实现的结果相同。
OpenFOAM 类
前面我们已经了解了 C++ 原生实现、make 实现以及 OpenFOAM 的 wmake 实现。在正式进入到 OpenFOAM 实现之前,我们有必要稍微了解一点 OpenFOAM 中的类,以方便使用。
常见的类
OpenFOAM 中有大量的类。我们大概了解一些常见的类型/类:
下面可以在 OpenFOAM: API Guide: OpenFOAM®: Open source CFD : API 中分别查询。
Switch
- OpenFOAM 为了用户使用简单方便,大量使用
Switch
供用户选择。Switch
本质上一种布尔类型。用户可以使用false
,off
,no
,n
,f
表示否定选项,使用true
,on
,yes
,y
,t
表示肯定选项
- OpenFOAM 为了用户使用简单方便,大量使用
label
- 本质上是 OpenFOAM 定义的具有
INT_SIZE
大小的 integer data type
- 本质上是 OpenFOAM 定义的具有
scalar
- 本质上是 OpenFOAM 定义的具有
FLOAT_SIZE
或者DOUBLE_SIZE
大小的 floating-point data type
- 本质上是 OpenFOAM 定义的具有
vector
- 矢量数据结构
- 包含重载后的数学运算方法
tensor
- 张量数据结构
- 包含重载后的数学运算方法
dimensionedScalar/*Vector/*Tensor
- 具有 OpenFOAM 单位系统的量
- 包含名称、单位和取值等成员数据,以及相应的成员方法
scalar*/vector*/tensorField
- 基础类型的列表值
- 描述各种场
- 包含数据和方法
dimensionSet
- OpenFOAM 的单位系统
dimensionSet (const scalar mass, const scalar length, const scalar time, const scalar temperature, const scalar moles, const scalar current=0, const scalar luminousIntensity=0)
[质量, 长度, 时间, 温度, 摩尔, 电流, 光强]
- OpenFOAM 也提供很多单位的组合
$FOAM_SRC/OpenFOAM/dimensionSet/dimensionSets.H
- 以后会深入讨论
tmp<>
- OpenFOAM 经常见到的处理临时数据的特殊类
- 因为 C++ 很多时候的内存都需要手动管理,所以 OpenFOAM 为开发者提供此类
- 无需深究
IOobject
- 提供接入 OpenFOAM 各种数据结构的方法,本身没有成员数据
- 以后会深入讨论
Vector 类
我们拿出 Vector 类看一下 OpenFOAM 对 Vector 类的实现。
更多代码的详细讨论参见
ofsc
系列。
找到 Vector 类,进入文件夹,
1 | // terminal |
阅读 Vector.H
的介绍可知,该类是模板化的 3D Vector,继承自 VectorSpace 并添加了 3 分量的构造、分量元素的接口函数、点积和叉积的方法等。具体到 Vector.H
的代码,可以看到简单的方法在代码中直接实现,复杂方法则大量使用内联函数(inline funtion)以提高代码性能。OpenFOAM 特别提供 VectorI.H
文件来写内联函数的实现。该类没有 .C
文件,因为所有方法都是通过内联函数实现的。其他的文件夹 /bools
, /complex
, /floats
,/ints
,/lists
都是 Vector 类对不同基本类型的 typedef
。
我们大概看一下 Vector.H
代码
陌生的代码细节可以忽略,或者网上查询相关语法。
1 | namespace Foam |
比如,基于 3 分量的构造,其内联函数在 VectorI.H
中实现如下(Vector
后面加后缀 I
用来表示是对内联函数的实现)
1 | template<class Cmpt> |
- 成员数据
v_
和重载的运算符[]
来自父类VectorSpace
- 查看
$FOAM_SRC/primitives/VectorSpace/VectorSpace.H
- 查看
- 传入函数的参数为
vx, vy, vz
,并使用常引用提高性能,保证数据安全 - 函数将 3 分量参数对应传入类的成员数据
- 模板化的函数,类型为
Cmpt
- 如果我们自己写也可以参考这种类的架构
比如,3 分量的接口函数因为简单,就没有内联而直接跟在 Vector.H
对应的声明中直接实现,如下
1 | // Member Functions |
- 对常量数据和非常量数据定义了两套函数,提高性能
- 标识符
noexcept
直接告诉编译器该函数不会发生异常,利于编译优化
比如,标量积和矢量积的声明和实现分别是
声明如下:
1 | // Vector.H |
对应的实现如下:
1 | // VectorI.H |
const
约束输入参数,需要使用常量 vectorconst
约束函数体,函数体内部不能修改类的成员数据- 另外声明
v1
作为本类的成员数据v_
的引用 - 返回
v1
和v2
的计算结果
根据类型 Cmpt
的不同,就实现了针对不同类型的方法。比如 Vector<scalar>
就是 scalar
类型的类实现。比如,在 /Vector/ints/labelVector.H
文件中说明了这一点,如下
1 | typedef Vector<label> labelVector; |
类似的,我们也可以找到 tensor.H
阅读和了解 tensor
类。
上面这些代码介绍主要是为了帮助读者慢慢熟悉 C++ 语言在 OpenFOAM 中的使用,克服对 C++ 语言的陌生和恐惧,便于读者理解下文要写的主源码。暂时不需要花费更多时间去阅读更多 OpenFOAM 的源代码,后续会在
ofsc
系列讨论代码。
项目准备
我们通过一个应用来使用 OpenFOAM 的自有类型/类。
1 | // terminal |
建立文件结构如下
1 | |- userProject/ |
注意,开发库的文件结构与前文稍有不同。
我们在前文已经可以注意到 OpenFOAM 库下一般有多个子库/类。
用户的开发库里可能也会由好几个类构成,开发库拥有自己的 Make 文件,集中管理多个类,比如这里有
class1
和class2
。
开发库
头文件 class1.H
如下
1 |
|
源文件 class1.C
如下
1 |
|
头文件 class2.H
如下
1 |
|
源文件 class2.C
如下
1 |
|
库 Make
为开发库写 Make 文件
文件 ofsp_00_tensor/Aerosand/Make/files
如下
1 | class1/class1.C |
本次开发库的实现没有再用到其他库,所以库文件 userProject/Aerosand/Make/options
空置。
将开发库编译成动态库
1 | // terminal |
顺利编译后,可以在 userProject/Aerosand/lnInclude
内看到类的快捷方式。
主源码
如果我们要使用 tensor 类,不仅要包含它的头文件,还需要包含相关的库。
查找 tensor 库
1 | // terminal |
通过 ls
命令我们可以看到 OpenFOAM
拥有自己的 Make 文件以及 lnInclude
,所以 tensor 类属于 OpenFOAM 库。后续我们需要在应用中包含、链接 OpenFOAM 库。
同样的,我们想要使用 dimensionedTensor
和 tensorField
1 | // terminal |
可以看到这两个类也属于 OpenFOAM
库。
我们包含这些库,也包含自己的开发库
主源码如下
1 |
|
应用 Make
文件 ofsp_00_tensor/Make/files
如下
1 | ofsp_00_tensor.C |
原则上来说,我们应该在 userApp/Make/options
里面链接 OpenFOAM
库。实际上,因为 OpenFOAM
库是最基本的库,所以,所有的应用都默认隐含的自动链接 OpenFOAM
库,无需再显式的写明。
所以,我们只需要在 userApp/Make/options
里面链接用户的开发库。
1 | EXE_INC = \ |
编译运行
1 | // terminal |
运行结果如下
1 | 3.14 * (1 2 3) = (3.14 6.28 9.42) |
fvCFD.H
在实际开发中,我们还需要用到更多的和 FVM 相关的类来离散求解偏微分方程。OpenFOAM 提供 fvCFD.H
,其中包含了大部分和 FVM 相关的头文件,包括 tensor 类等。使用 fvCFD.H
可以大大减少主源码要写的头文件数量。
我们找一下 fvCFD.H
文件
1 | // terminal |
显然 fvCFD.H
不在 OpenFOAM 库
里,而是在 finiteVolume 库
里,所以需要另外在 Make/options
中包含、链接。
我们将上述主源码中的非开发库头文件全部替换成 fvCFD.H
。
此时的 Make/options
文件应修改为(当然开发库链接仍然需要有)
1 | EXE_INC = \ |
finiteVolume
被显式的包含、链接meshTools
补充了finiteVolume
中涉及的其他库
重新编译运行,结果相同。
OpenFOAM 应用
在完全理解了上面关于编译过程讨论的种种,我们使用 OpenFOAM 提供的工具,串一个较为完整的应用开发流程。
项目准备
在用户文件夹下创建应用项目,进入该应用项目
1 | // terminal |
接着可以拷贝一个简单算例用于开发过程的测试(对于该项目来说,仅仅是为了通过算例检查)
1 | // terminal |
准备脚本
为了方便开发,可以创建脚本文件,下面举个例子。
1 | // terminal |
脚本 caserun
主要是负责应用编译成功后,调试算例的运行,暂时写入如下内容
1 | #!/bin/bash |
caseclean
脚本主要是负责清理应用到到编译前状态,如果应用要修改,那么测试算例也要还原到运行前的状态,所以暂时写入如下内容
1 | #!/bin/bash |
库的编译清理也可以类似的创建脚本,这里不多赘述。
我们可以简单的运行这些脚本,如下所示
1 | // terminal |
如果运行提醒 Permission denied
,那就需要给文件权限
1 | // terminal |
说明文件
为了方便后续开发和使用,还应该准备说明文件。
1 | // terminal |
写入需要的开发备忘、运行步骤、注意事项等内容。请不要吝啬时间,务必花费一点点时间把问题写清楚,不然日后转交他人,或者自己重新阅读项目,都会十分痛苦。
项目结构
所以,项目的文件结构如下
1 | |- debug_case/ |
我们准备在本项目中使用前文讨论的开发库,所以该项目路径下没有开发库文件。
主源码
打开主源码 ofsp_00_helloWorld_openfoam.C
1 | /* |
这里约定
/* comments */
写在代码功能块之前,用于正式说明或者介绍此代码功能块// comments
写在具体代码行附近,用于解释、标记或者想法等
代码解释
- OpenFOAM 文件头内容
- 文中均省略处理
#include "fvCFD.H"
- 包含了所有的 FVM 方法,通常必须
#include "setRootCase.H"
- 建立应用的参数列表的类,检查算例文件结构
- 使用
find $FOAM_SRC -name setRootCase.H
去查阅 - 后续会详细讨论
#include "createTime.H"
- 创建
time
类的runTime
对象,需要算例 - 后续会详细讨论
- 创建
#include "creatMesh.H"
- 求解器应用需要该头文件
- 创建
fvMesh
类的mehs
对象 - 后续会详细讨论
Info
- OpenFOAM 提供的输出语法,适配 OpenFOAM 多种类型
nl
- OpenFOAM 提供的换行符,和
endl
类似 - 本系列约定,当存在大段输出的时候,中间换行用
nl
,结尾处用endl
- OpenFOAM 提供的换行符,和
runTime.printExecutionTime(Info)
- 打印时间相关信息
主源码开发
我们在主代码中添加简单的内容
1 |
|
注意,这里演示了如何链接使用其他开发库。
本项目调用的依然是上一节的开发库
Aerosand
(见#include class1.H
)。所以,我们需要妥善处理链接问题。
应用 Make
默认生成的 userApp/Make/options
基础上添加自开发库
1 | EXE_INC = \ |
- 注意书写格式,比如标识符、行尾的
\
换行- 原则上可以一行写完所有语句,虽然有警告,但不是不行
- 为了方便维护,使用换行符
\
来换行,换行符\
前后多加空格无所谓,后面如果有空格会有警告
EXE_INC
- 以
-I
标识开头列出所有需要包含的头文件的路径 $(LIB_SRC)
是环境变量- 可以等同替换为
-I$(FOAM_SRC)/finiteVolume/lnInclude
- 或者直接写成绝对地址
-I/usr/lib/openfoam/openfoam2306/src/finiteVolume/lnInclude
- 开发库的调用是相对地址
../ofsp_00_tensor/Aerosand/lnInclude
- 以
EXE_LIBS
- 小写
-l
标识列出库的名字 - 大写
-L
标识列出库的路径
- 小写
查看默认生成的 userApp/Make/file
,其内容不需要修改。
1 | ofsp_00_helloWorld_openfoam.C |
编译运行
编译并运行该应用
1 | wclean |
- 编译时会警告 mesh 未使用,我们确实没有使用,这不影响
运行结果如下
1 | /*---------------------------------------------------------------------------*\ |
以后的运行结果不再赘述 OpenFOAM 标准输出信息。
小结
到此为止,我们应该清楚明白了 OpenFOAM 应用开发的逻辑和架构。
希望这样的介绍能让读者感受到“清晰”和“连续”。
在实际开发中,开发库可以是用户自定义边界条件,也可以是一些用户工具等等。