OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试

0 阅读43分钟

OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试

第二季收官章。前面十五节讲的都是"物理、数值、网格、框架"——它们决定算得对不对。本节讲的是"工程面":跨平台构建(尤其是 Windows)、性能剖析与优化、数据交换与可视化、模块扩展、测试与回归。这些内容不改变控制方程,却决定了一份 OpenFOAM 源码能不能在你的机器上编译出来、跑得快、能被 ParaView/EnSight 读懂、能不能被自动化验证。换言之,本章是"从源码到可用工具"的最后一公里。

本章所有路径、行号与代码摘录均来自本仓库(OpenFOAM master,WM_PROJECT_VERSION=2606,C++17)的真实文件,已逐个 Read 核对。


1. 导言与全景:OpenFOAM 的工程工具链地图

1.1 一条完整的工程闭环

一个 OpenFOAM 开发者的日常工作,可以抽象成一条闭环:

写代码  ──▶ wmake(S2-15、第一季第 2 章)
调试    ──▶ Info / FatalError / DebugInfo + applications/test
性能    ──▶ profiling(controlDict)→ 定位瓶颈 → OpenMP / SPDP / 算法改造
并行    ──▶ decomposePar → 运行 → reconstructPar
后处理  ──▶ functionObjects / foamToVTK / foamToEnsight / postProcess
跨平台  ──▶ MinGW 交叉编译(.def / libo / LINKEXE_STUB)
扩展    ──▶ libs(...) / modules(adios、visualization、external-solver)/ plugins
验证    ──▶ applications/test 单元自检 + tutorials 端到端回归 + META-INFO 版本校验

本节逐环拆解,每一环都落到具体文件、具体行。

1.2 三条主线与三类"看不见的抽象"

本章覆盖三条主线,每条都对应一套"看不见的抽象层":

主线目标关键抽象层代表文件
跨平台构建Linux 上编出能跑在 Windows 的 exe/dll规则链(rules)+ 符号导出层(.def)src/Allwmake、wmake/makefiles/general、wmake/rules/*Mingw
性能剖析找到"时间花在哪"计时树(profiling)src/OpenFOAM/global/profiling/*
数据交换与外部工具/后处理器互通格式层(fileFormats)+ 转换层(conversion)src/fileFormats/*、src/conversion/*、applications/utilities/postProcessing/*

补充两条辅线:modules 扩展(modules/)与测试体系(applications/test/、tutorials/、META-INFO/)。

1.3 为什么"工程面"值得单独一章

前十五节反复出现的三层抽象(运行期选择 RTS、字典驱动、模板 + 零拷贝)解决的是"程序内部如何可配置、可扩展"。而工程面解决的是"程序外部如何被构建、被测量、被交换、被验证"。二者合起来,才构成一个完整的 OpenFOAM:

  • 没有跨平台构建,源码只能活在 Linux;
  • 没有 profiling,优化只能靠猜;
  • 没有数据交换,结果只活在 OpenFOAM 自己的目录里;
  • 没有测试,改动就没有安全网。

2. Windows / MinGW 交叉编译

OpenFOAM 原生面向 Linux/Unix。所谓"Windows 支持",在 OpenFOAM 里其实有两种完全不同的含义,必须严格区分:

  1. runtime-only(运行期支持):在 Windows(或 MSYS/MinGW shell)里能运行 Linux 上编出来的东西——这依赖 Windows 的 WSL/POSIX 子系统,源码层面只提供 OSspecific/MSwindows 适配层;
  2. cross-compilation(交叉编译):在 Linux 主机 上用 mingw-w64 工具链编出原生 Windows 的 .exe / .dll——这是真正的"跨平台构建",也是本节重点。

etc/config.sh/settings 对这两种情况做了清晰的编码。

2.1 平台识别:etc/config.sh/settings 的 MINGW 分支

etc/config.sh/settings(第 92–108 行):

# With mingw cross-compiled : presume x86_64 (unless aarch64)
MINGW* | MSYS*)
    export WM_COMPILER_LIB_ARCH=64  # Use lib64 (consistent with build platform)
    WM_ARCH=win64
    case "$(uname -m)" in
    (aarch64 | arm64)
        WM_ARCH=winARM64
        ;;
    esac
    # Default compiler should be 'Mingw'
    case "$WM_COMPILER" in
    (Clang* | Gcc*)
        WM_COMPILER=Mingw
        ;;
    esac
    echo "openfoam: windows support ($WM_ARCH) is runtime only" 1>&2
    ;;

逐行解读:

  • 第 93 行 MINGW* | MSYS*:当 uname -s 返回以 MINGW 或 MSYS 开头的系统名时进入(典型是 MSYS2 的 bash)。这是一条 OS 名称模式匹配,WM_ARCH 初值在第 23 行由 uname -s 得到。
  • 第 94 行:强制 WM_COMPILER_LIB_ARCH=64,让库目录后缀与构建平台一致(lib64)。
  • 第 95 行:把架构名改成 win64——注意这里 WM_ARCH=win64 而不是 linux64,字符串里已经写明"目标是 Windows"。
  • 第 96–100 行:若 uname -m 为 aarch64/arm64,则改成 winARM64,为 ARM 版 Windows 做准备。
  • 第 102–106 行:把 Clang*/Gcc* 统一改写为 Mingw,因为在这个 shell 里默认编译器族应当是 MinGW。
  • 第 107 行:打印 windows support ($WM_ARCH) is runtime only——这行提示是本分支存在的核心原因:此分支本身描述的是"运行期环境",真正的交叉编译由 etc-mingw/ 另行配置。

2.2 WM_OPTIONS:平台字符串如何编码"为谁构建"

etc/config.sh/settings(第 127–134 行):

# wmake configuration
export WM_DIR="$WM_PROJECT_DIR/wmake"
export WM_LABEL_OPTION="Int$WM_LABEL_SIZE"
export WM_OPTIONS="$WM_ARCH$WM_COMPILER$WM_PRECISION_OPTION$WM_LABEL_OPTION$WM_COMPILE_OPTION"

# Base executables/libraries
export FOAM_APPBIN="$WM_PROJECT_DIR/platforms/$WM_OPTIONS/bin"
export FOAM_LIBBIN="$WM_PROJECT_DIR/platforms/$WM_OPTIONS/lib"
  • 第 129 行:WM_LABEL_OPTION=Int32 或 Int64(取决于 WM_LABEL_SIZE)。
  • 第 130 行:WM_OPTIONS 由五段拼接:{ARCH}{COMPILER}{PRECISION}{LABEL}{OPTION}。交叉编译时典型值为 linux64MingwDPInt32Opt——注意第一个字段是 linux64 而非 win64,因为宿主(编译发生地)是 Linux,WM_ARCH 仍是 linux64;目标平台由 WM_COMPILER=Mingw 这段编码。
  • 第 133–134 行:所有产物落到 platforms/$WM_OPTIONS/{bin,lib}。这解释了为什么"同一份源码可以同时存在 linux64GccDPInt32Opt 与 linux64MingwDPInt32Opt 两套产物"——平台目录天然隔离不同工具链的产物。

关键结论:WM_OPTIONS 是一串自描述的平台标识。看到 linux64Mingw... 就知道:宿主 Linux、目标 Windows、编译器 MinGW。这个字符串贯穿 wmake 的规则选择、目录命名与链接参数。

2.3 WM_OSTYPE=MSwindows:把 OS 类型也交给编译器决定

etc/config.sh/settings(第 221–226 行):

# Non-POSIX systems and cross-compiling
case "$WM_COMPILER" in
(Mingw*)
    export WM_OSTYPE=MSwindows
    ;;
esac
  • 第 222–226 行:只要编译器是 Mingw*,就把 WM_OSTYPE 设为 MSwindows。这是"OS 差异"被编译工具链决定而非被 uname 决定的证据。
  • WM_OSTYPE 随后被 src/Allwmake 用来选择 OSspecific/MSwindows 或 OSspecific/POSIX 目录,也被 wmake/makefiles/general(第 231 行 findstring windows,$(WM_OSTYPE))用来切换 libso 的链接分支。

2.4 etc-mingw/:交叉编译专用配置层

etc-mingw/ 目录注入的是"用于交叉编译的偏好"。etc-mingw/prefs.sh(第 1–27 行)全文很短,但信息密度很高:

# Preferences for mingw cross-compilation

export FOAM_CONFIG_ETC="etc-mingw"

export WM_COMPILER=Mingw
export WM_MPLIB=msmpi-10.0

unset WM_COMPILE_CONTROL

# No zlib available:
# export WM_COMPILE_CONTROL="~libz"

# No mpi available:
# WM_MPLIB=none

# [mingw] generally wish to avoid debug symbols from bloating the sizes
case "$FOAM_EXTRA_CXXFLAGS" in
(*-g*)
    FOAM_EXTRA_CXXFLAGS="$(echo "$FOAM_EXTRA_CXXFLAGS" | sed 's/-g[0-9]*//')"
    echo "Removed -g from FOAM_EXTRA_CXXFLAGS ($WM_COMPILER)" 1>&2
    ;;
esac

逐行/逐段解读:

  • 第 4 行 FOAM_CONFIG_ETC="etc-mingw":把配置搜索目录从默认 etc 切到 etc-mingw。于是同名的 config.sh/* 文件(如 FFTW、CGAL、scotch)会被 etc-mingw/config.sh/ 下的版本覆盖。
  • 第 6 行:显式声明 WM_COMPILER=Mingw。
  • 第 7 行:WM_MPLIB=msmpi-10.0——Windows 下用微软的 MS-MPI,而不是 OpenMPI。对应规则 wmake/rules/linux64Mingw/mplibMSMPI。
  • 第 9 行:unset WM_COMPILE_CONTROL——清空编译控制串,避免宿主环境的 +openmp/~float 等泄漏到交叉编译。
  • 第 20–25 行:如果 FOAM_EXTRA_CXXFLAGS 里含 -g(调试符号),用 sed 's/-g[0-9]*//' 剥掉,并打一条提示。Windows 上 DLL/EXE 带调试符号会显著膨胀体积,所以交叉编译默认去调试符号。这是很具体的工程取舍。

etc-mingw/config.sh/ 下有 3 个覆盖文件:

  • FFTW.system(第 22–23 行):fftw_version=fftw-system;export FFTW_ARCH_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw——用宿主 mingw 提供的 Windows 版 FFTW。
  • CGAL.none(第 22–26 行):boost_version=boost-none、cgal_version=CGAL-none,路径设成 /$boost_version、/$cgal_version 这样故意不存在的路径,从机制上强制禁用 Boost/CGAL。
  • scotch(第 35–36 行):SCOTCH_VERSION=scotch_6.1.0;SCOTCH_ARCH_PATH 拼进 $WM_ARCH$WM_COMPILER$WM_PRECISION_OPTION$WM_LABEL_OPTION 前缀(与 WM_OPTIONS 同构),定位交叉编译对应的 ThirdParty 平台目录。

小结其余覆盖:FFTW 用 FFTW.system 指向宿主的 /usr/x86_64-w64-mingw32/sys-root/mingw;CGAL/Boost 用 CGAL.none 把路径指向不存在处以机制性禁用;SCOTCH 用 6.1.0 版本且路径含 Mingw 前缀以分区;MPI 切到 WM_MPLIB=msmpi-10.0;unset WM_COMPILE_CONTROL 避免宿主开关泄漏;-g 被 sed 剥离以减小 DLL/EXE 体积。

2.5 OSspecific/MSwindows:OS 差异适配层

src/OSspecific/ 下有两套并列实现:POSIX 与 MSwindows。src/Allwmake(第 25–31 行)在构建时选择其一:

wmakeLnInclude -u OpenFOAM
wmakeLnInclude -u OSspecific/"${WM_OSTYPE:-POSIX}"

# Update version info (as required)
OpenFOAM/Alltouch -check 2>/dev/null

OSspecific/"${WM_OSTYPE:-POSIX}"/Allwmake $targetType $*
  • 第 26 行:OSspecific/MSwindows 或 OSspecific/POSIX——上层代码只认 Foam:: 接口,平台差异由这一薄层消化。
  • 第 31 行:调用该层自己的 Allwmake。

MSwindows 的组成见 src/OSspecific/MSwindows/Make/files:MSwindows.C;cpuInfo/cpuInfo.C、memInfo/memInfo.C;memory/MemoryPool.cxx;signals/{sigFpe,sigInt,sigQuit,sigSegv,sigStopAtWriteNow,sigWriteNow,timer}.cxx;fileStat/fileStat.C;fileMonitor/fileMonitor.C(注释 /* Without inotify */);printStack/dummyPrintStack.C;LIB = $(FOAM_LIBBIN)/libOSspecific。

MSwindows 与 POSIX 功能语义一致、实现不同:CPU 信息由 cpuInfo.C(populate() 为空,核数退化为 std::thread::hardware_concurrency());内存信息 memInfo.C(Windows API);MemoryPool.cxx 平台版;信号为同名 .cxx(Windows 异常/信号模型);文件状态 fileStat.C(替代 POSIX 时间戳);文件监视 fileMonitor.C(#ifdef _WIN32 强制 #undef FOAM_USE_INOTIFY);栈回溯用 dummyPrintStack.C 空实现(Windows 上没有真实栈回溯);计时 cpuTime.H 平台版;链接由 Make/options 的 EXE_INC=$(COMP_FLAGS)/LIBO_LIBS=$(LINK_FLAGS) 注入 -DFOAM_USE_WINDOWS_PSAPI 与 -lpsapi。

MSwindows/Allwmake(第 1–29 行) 是这一层的构建入口:

targetType=libo                                     # Preferred library type
. ${WM_PROJECT_DIR:?}/wmake/scripts/AllwmakeParseArguments $*

if [ -f /usr/include/FlexLexer.h ] && [ ! -f FlexLexer.h ]
then
    ln -sf /usr/include/FlexLexer.h FlexLexer.h     # 交叉编译就地补链接
    if [ -d lnInclude ]; then (cd lnInclude && ln -sf ../FlexLexer.h .); fi
fi

COMP_FLAGS="-DFOAM_USE_WINDOWS_PSAPI"
LINK_FLAGS="-lpsapi"

wmake -no-openmp $targetType
  • 第 3 行 targetType=libo:OSspecific 层默认编成单个 .o,避免 DLL 导出复杂性与不必要的 OpenMP。
  • 第 8–18 行:交叉编译时把宿主 /usr/include/FlexLexer.h 软链进当前目录(并在 lnInclude/ 再链一次)——"交叉编译环境差异补丁"的典型手法。
  • 第 22–23 行:-DFOAM_USE_WINDOWS_PSAPI + -lpsapi(Windows 进程状态 API,供 memInfo 读内存)。
  • 第 27 行:wmake -no-openmp libo。

2.6 难题一:Pstream 与 OpenFOAM 的循环依赖 → libo

src/Allwmake(第 33–80 行)是整章最关键的脚本,它把"跨平台难题"写成了工程方案:

case "$WM_ARCH/$WM_COMPILER" in
(*ARM64/Mingw*)
    # Handle Pstream/OpenFOAM cyclic dependency
    # - compile Pstream as single .o object
    # - compile OpenFOAM and link against libPstream.o
    Pstream/Allwmake libo  #<- dummy and mpi
    FOAM_LINK_DUMMY_PSTREAM=libo wmake $targetType OpenFOAM

    # Extra handling to ensure that export symbols are not pruned
    special_def_file="${FOAM_LIBBIN:?}/dummy/libPstream.o.def"
    if [ -f "$special_def_file" ]
    then
        # Relink [dummy] libPstream as dll with full export
        FOAM_EXTRA_CXXFLAGS="$FOAM_EXTRA_CXXFLAGS -Wl,$special_def_file" \
        WM_MPLIB=dummy Pstream/Allwmake $targetType $*
    fi

    # --- 对 MPI 版 libPstream 做同样处理(.def 优先取 FOAM_MPI_LIBBIN)---
    # ... Pstream/Allwmake-mpi $targetType $*

    # Force relink of libOpenFOAM against libPstream.{dll,so}
    OpenFOAM/Alltouch 2>/dev/null
    ;;
(*/Mingw*)
    # Handle Pstream/OpenFOAM cyclic dependency
    WM_MPLIB=dummy Pstream/Allwmake libo
    FOAM_LINK_DUMMY_PSTREAM=libo wmake $targetType OpenFOAM
    OpenFOAM/Alltouch 2>/dev/null
    ;;
esac

逐行解读:

  • 第 33 行 case "$WM_ARCH/$WM_COMPILER":以"架构/编译器"二元组分支——ARM64+Mingw 走第一支,普通 x86_64 Mingw 走第二支。
  • 第 35–37 行注释把问题说透:Handle Pstream/OpenFOAM cyclic dependency。libOpenFOAM 依赖 libPstream 的通信符号;libPstream 又用 OpenFOAM 的类型/工具。ELF 上可用 --start-group 或"默认导出全部符号"糊过去,但 Windows DLL 下这条链会成环且符号被裁。
  • 第 38 行 Pstream/Allwmake libo:把 Pstream 编成单个 .o(libo 目标),.o 是无条件可链接的对象,绕开"共享库导入/导出"这层。
  • 第 39 行:FOAM_LINK_DUMMY_PSTREAM=libo wmake ... OpenFOAM——用环境变量告知"用 dummy Pstream 的 libo"。libo 一词同时传递"目标类型"与"链接来源"。
  • 第 42–48 行:定位 ${FOAM_LIBBIN}/dummy/libPstream.o.def,若存在则用 -Wl,<def> 把它传给链接器,重链 dummy 版 Pstream 为全导出 dll(注释 Relink [dummy] libPstream as dll with full export)。
  • 第 50–65 行:对 MPI 版 Pstream 做同样处理(.def 优先取 ${FOAM_MPI_LIBBIN},退化取 ${FOAM_LIBBIN}/$FOAM_MPI)。
  • 第 67–68 / 77–78 行 OpenFOAM/Alltouch:强制 libOpenFOAM 重新链接(touch 源文件使 wmake 认定需重建),确保最终链接到重链后的 Pstream。

2.7 难题二:Windows DLL 必须显式导出符号 → .def

Linux 上 g++ -shared 默认导出所有非隐藏符号;Windows 的 DLL 默认只导出被显式标注的符号。因此 MinGW 交链必须生成 .def(模块定义文件),里面列出要导出的 mangled 符号名。

先生成 .def 的是链接器本身。wmake/rules/linux64Mingw/c++(第 34–47 行)里的 LINKLIBSO:

LINKLIBSO   = $(CC) $(c++FLAGS) -shared \
    -Wl,--output-def=$(LIB).def \
    -Wl,--out-implib=$(LIB).dll.a \
    -Wl,--no-undefined \
    -Wl,--enable-runtime-pseudo-reloc \
    -Wl,--enable-auto-import \
    -Wl,--enable-auto-image-base \
    -Wl,--strip-all

LINKEXE     = $(CC) $(c++FLAGS) \
    -static-libgcc -static-libstdc++ \
    -Wl,--enable-auto-import \
    -Wl,--strip-all \
    -Wl,--force-exe-suffix

逐行解读:

  • 第 34–35 行:-shared 生成共享库,--output-def=$(LIB).def 让链接器顺带写出一个 .def;--out-implib=$(LIB).dll.a 同时产出导入库。
  • 第 38 行:--no-undefined 要求链接期无未定义符号;注释(第 31–32 行)说明它与 --enable-runtime-pseudo-reloc 不能一起"宽容"未定义符号——必须把所有符号凑齐,这与".def 显式导出"是配套的严格策略。
  • 第 40–41 行:--enable-auto-import/--enable-auto-image-base 是 MinGW 兼容开关;--strip-all 去符号表。
  • 第 43–47 行:可执行文件额外 -static-libgcc -static-libstdc++(静态链接 libgcc/libstdc++,免于在 Windows 上再分发这两个 DLL),--force-exe-suffix 强制 .exe。

但链接器自动写出的 .def 未必包含所有需要导出的 C++ mangled 符号(模板实例、被优化掉的引用等)。于是 OpenFOAM 主动用 llvm-nm 抽取符号,补一份 .def。这就是 wmake/makefiles/general(第 113–125 行)的宏:

# For llvm-mingw, export symbols are needed in a few places.
# Filters the output of 'nm' xxx [BDRT] _ZFoam...
#
# Called with (input-file, output-file)
define LLVM_MINGW_EXPORT_DEF
    @if test -f $(1); then \
        echo "symbols: $(2)"; \
        echo 'EXPORTS' >| $(2); \
        llvm-nm --defined-only --extern-only $(1) \
            | awk '$$2 ~ /[BDRT]/ && $$3 ~ /^_Z/ {print "    " $$3}' \
            | sort -u >> $(2); \
    fi
endef

逐行解读:

  • 第 117–118 行:定义 make 宏 LLVM_MINGW_EXPORT_DEF,入参是"输入文件" ${1}、输出 .def 文件 ${2}。
  • 第 119 行 @if test -f $(1):输入文件存在才做。
  • 第 121 行 echo 'EXPORTS' >| $(2):>| 是 make 转义后的 >(强制覆盖写),第一行写入 DEF 文件必需的 EXPORTS 关键字。
  • 第 122–123 行:llvm-nm --defined-only --extern-only $(1) 列出该 .o 中已定义且外部的符号;awk '$$2 ~ /[BDRT]/ && $$3 ~ /^_Z/ {print " " $$3}':$$2 是 nm 输出的符号段/类型列,要求匹配 [BDRT](B=BSS、D=数据、R=只读数据、T=代码文本段,即"需要导出"的几类);$$3 是符号名列,要求以 _Z 开头——这是 Itanium C++ ABI 的 mangled 名前缀(_Z 后跟函数/变量编码)。满足两条件者,前面缩进四个空格写入 .def。
  • 第 124 行:sort -u 去重排序。
  • 第 125 行 >> $(2):追加到 .def。

这就是".def 导出符号的生成流程":llvm-nm 抽 _Z 外部定义符号 → awk 按段类型过滤 → sort -u → 写进 EXPORTS 块。它把"人不愿手列几百个 mangled 名"这件事自动化了。

机制链 A(完整推导):MinGW 下 .def 从 .o 到 lld-link

源文件 .C/.cxx ──(x86_64-w64-mingw32-g++ -c, -Wa,-mbig-obj)──▶ .o(COFF,含 _Z 符号)
      │
      ├─(wmake libo 目标)──▶ libX.o ──(LLVM_MINGW_EXPORT_DEF)──▶ libX.o.def(EXPORTS 块)
      │
      └─(wmake libso 目标, LINKLIBSO)──▶
              x86_64-w64-mingw32-g++ -shared
                -Wl,--output-def=libX.def          ← 链接器自写一份 .def
                -Wl,--out-implib=libX.dll.a        ← 导入库
                -Wl,--no-undefined ...             ← 严格检查
                ──▶ libX.dll (+ libX.def + libX.dll.a)

重链(当需要"全导出"时,如 Pstream):
      FOAM_EXTRA_CXXFLAGS="-Wl,libX.o.def"
        ──▶ 链接器按 .def 的 EXPORTS 强制导出全部列名符号

ARM64 交叉编译的 libo 特有分支(wmake/makefiles/general 第 273–278 行):
      ifneq (,$(findstring linuxARM64Mingw,$(WM_OPTIONS)))
          $E lld-link -lib -out:$(LIB).o $(OBJECTS) $(LIBO_LIBS)
          $(call LLVM_MINGW_EXPORT_DEF,$(LIB).o,$(LIB).o.def)
      else
          $E $(LD) -r -o $(LIB).o $(OBJECTS) $(LIBO_LIBS)
      endif

2.8 libo 目标:单对象库如何打破循环依赖

wmake/makefiles/general 对三种库目标做了对照实现。下面把 libso/lib/libo 三段并排给出(第 226–278 行),再逐段解释。

(a)libso:共享库(第 228–243 行)。分 Windows / 非 Windows 两支:

ifneq (,$(findstring windows,$(WM_OSTYPE)))
$(LIB)$(EXT_SO): $(OBJECTS)
    $E $(LINKLIBSO) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
        $(PROJECT_LIBS) $(LIB_LIBS) $(GLIB_LIBS) -o $(LIB)$(EXT_SO)
else
$(LIB)$(EXT_SO): $(OBJECTS)
    $E $(LINKLIBSO) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
        $(LIB_LIBS) $(GLIB_LIBS) -o $(LIB)$(EXT_SO)
endif
  • 第 231 行:findstring windows,$(WM_OSTYPE)——仅 Windows 下才在链接命令加入 $(PROJECT_LIBS)(-lOpenFOAM -ldl)。Linux 分支不加,因为 ELF 允许未解析符号延后到最终可执行文件;Windows DLL 必须链接期解析,所以要显式带上。这是"同一 Makefile 按 OS 分叉链接命令"的直接证据。

(b)lib:静态归档(第 245–257 行):用 $(AR) $(ARFLAGS) $(LIB).a $(OBJECTS) 打包,再 $(RANLIB) 建索引。传统 Unix 静态链接,Windows 交叉编译一般不用。

(c)libo:单对象库(第 259–278 行)

# libo : single object lib
# Note: llvm-mingw expects lld-link with '-lib', '-out:...' flags.
# It is useful to create an .def export file from the output object
# (currently done manually)

.PHONY: libo
libo: $(LIB).o | silent

$(LIB).o: $(OBJECTS)
    @$(WM_SCRIPTS)/makeTargetDir $(LIB)
    @rm -f $(LIB).o
    $(call QUIET_MESSAGE,ld,$(LIB).o)
ifneq (,$(findstring linuxARM64Mingw,$(WM_OPTIONS)))
    $E lld-link -lib -out:$(LIB).o $(OBJECTS) $(LIBO_LIBS)
    $(call LLVM_MINGW_EXPORT_DEF,$(LIB).o,$(LIB).o.def)
else
    $E $(LD) -r -o $(LIB).o $(OBJECTS) $(LIBO_LIBS)
endif
  • 第 260–264 行注释点明了两件事:llvm-mingw 需要 lld-link -lib -out:... 参数形态;并且从输出对象生成 .def 是"currently done manually"(当前靠上述宏半自动完成)。
  • 第 269 行目标 $(LIB).o:由所有 .o 合成一个大 .o。
  • 第 273 行:linuxARM64Mingw 走 lld-link -lib -out:$(LIB).o(LLVM 工具链的"静默合并对象"模式),随后立刻调 LLVM_MINGW_EXPORT_DEF 产出 .def。
  • 第 277 行:其它平台走 $(LD) -r -o $(LIB).o——即 ld -r(relocatable 链接,把多个 .o 合并成一个可再链接的 .o,符号仍保留、无重定位丢失)。这一步是"打破循环依赖"的技术核心:合并成一个 .o 后,链接它时不再需要它反过来解析共享库符号。

机制链 B(完整推导):libo 如何打破 Pstream/OpenFOAM 循环依赖

问题:libOpenFOAM 需要 libPstream 的通信符号
      libPstream 又需要 libOpenFOAM 的类型/工具
      → 在 DLL 模型下,二者互为"必须链接期解析"的依赖,成环。

解法(src/Allwmake 第 38–39 / 74–75 行):
  1) Pstream/Allwmake libo
        → 不产 .dll,而产 libPstream.o(单对象,ld -r / lld-link -lib 合并)
  2) FOAM_LINK_DUMMY_PSTREAM=libo wmake OpenFOAM
        → libOpenFOAM 链接时把 libPstream.o 直接吃进去
        → .o 里符号已"定义",不再触发"反过来需要 dll 导入"的成环解析
  3) (ARM64 分支) 生成 libPstream.o.def → 用 -Wl,<def> 重链出全导出 dll
  4) OpenFOAM/Alltouch → 强制 libOpenFOAM 重链到最终的 Pstream 产物

一句话:ELF 上"循环依赖"可以靠 --start-group 或"默认导出全部符号"糊过去;Windows DLL 上这两条都不成立,只能"把一侧压成单对象 .o 直接吞进去 + 用 .def 显式导出"。这就是 OpenFOAM 跨平台构建比"换个编译器"复杂的根本原因。

2.9 难题三:纯串行构建的符号 stub → LINKEXE_STUB

第三种"符号供应"问题是纯串行构建:不需要任何 MPI,但代码仍引用 Pstream 符号。OpenFOAM 用"空实现 stub 库"解决。wmake/rules/General/Gcc/link-c++(第 1–34 行)关键段:

LINKEXE     = $(CC) $(c++FLAGS)
undefine LINKEXE_STUB

# Use gold linker
ifneq (,$(findstring +gold,$(WM_COMPILE_CONTROL)))
    LINKEXE    += -fuse-ld=gold
    LINKEXE_STUB := true
else
    LINKLIBSO  += -Xlinker --add-needed -Xlinker --no-as-needed
    LINKEXE    += -Xlinker --add-needed -Xlinker --no-as-needed
endif

# Link dummy stub to resolve libPstream symbols used by libOpenFOAM
ifneq (,$(LINKEXE_STUB))
ifneq (,$(findstring -lOpenFOAM,$(PROJECT_LIBS)))
    LINKEXE += -L$(FOAM_LIBBIN)/dummy -lPstream
endif
endif

逐行解读:

  • 第 8 行 undefine LINKEXE_STUB:默认未定义该开关(先清空,避免继承)。
  • 第 13–18 行:若编译控制含 +gold,用 gold 链接器并打开 LINKEXE_STUB := true。
  • 第 20–24 行:否则用默认 ld,加 -Xlinker --add-needed --no-as-needed——保证"看起来没被引用"的库也进 DT_NEEDED(防止符号被裁);此分支不设 LINKEXE_STUB。
  • 第 29–33 行:当 LINKEXE_STUB 打开且 PROJECT_LIBS 含 -lOpenFOAM 时,给可执行文件链接追加 -L$(FOAM_LIBBIN)/dummy -lPstream,即链接 dummy stub 来"喂饱" libOpenFOAM 用到的 Pstream 符号。

三种"符号供应"问题归纳(对比表):

场景症状OpenFOAM 解法关键文件/行
ARM64 Mingw:Pstream↔OpenFOAM 成环DLL 导入导出互锁libo 单对象 + .def 全导出重链src/Allwmake 33–69
x86_64 Mingw:同上同上libo 单对象 + 重链src/Allwmake 70–79
纯串行构建缺 Pstream 符号链接期 undefineddummy stub(libPstream)wmake/rules/General/Gcc/link-c++ 28–33
需要全导出的 MinGW dll符号被裁LLVM_MINGW_EXPORT_DEF 生成 .defwmake/makefiles/general 113–125

3. MinGW 的两条规则链与 wmake 规则体系

3.1 规则目录的两级结构

wmake/rules/ 采用两级命名:

  • 架构级:rules/<arch><compiler>/,例如 linux64Mingw/、linuxARM64Mingw/、linux64Gcc/;
  • 编译器族级:rules/General/<family>/,例如 rules/General/Gcc/、rules/General/Nvidia/;
  • 公共级:rules/General/ 与 rules/General/common/。

选择逻辑在 wmake/rules/General/general(第 17–21、52–57 行):

ARCHITECTURE_RULES = $(WM_DIR)/rules/$(WM_ARCH)
COMPILER_FAMILY = $(shell echo "$(WM_COMPILER)" | sed -e 's/[-+.0-9~].*//')
DEFAULT_RULES   = $(ARCHITECTURE_RULES)$(COMPILER_FAMILY)
RULES           = $(ARCHITECTURE_RULES)$(WM_COMPILER)
include $(DEFAULT_RULES)/general
sinclude $(DEFAULT_RULES)/override
ifneq ("$(COMPILER_FAMILY)","$(WM_COMPILER)")
sinclude $(RULES)/general
sinclude $(RULES)/c++
endif

逐行解读:

  • 第 17 行:ARCHITECTURE_RULES = wmake/rules/$(WM_ARCH)。注意此处用 ${WM_ARCH}(如 linux64),不含编译器。
  • 第 18 行:COMPILER_FAMILY 用 sed -e 's/[-+.0-9~].*//' 把 WM_COMPILER 里从第一个 -/+/./数字/~ 起的后缀全部截掉。于是 Mingw → Mingw,Clang-14 → Clang,Gcc-11 → Gcc。这就是"编译家族"的提取规则。
  • 第 19 行:DEFAULT_RULES = rules/<arch><family>,如 rules/linux64Mingw。
  • 第 20 行:RULES = rules/<arch><compiler>,带完整编译器名(含版本后缀)。
  • 第 52 行:先 include $(DEFAULT_RULES)/general——必定存在,提供编译器族通用设置。
  • 第 53 行:sinclude .../override——可选覆盖文件。
  • 第 54–57 行:当"家族"与"完整编译器名"不同时(例如 Mingw vs Gcc-11 这种带版本后缀的情形),再额外 sinclude $(RULES)/general 与 $(RULES)/c++,作为编译器版本专属的覆盖。

这一条链解释了 wmake/rules/linux64Mingw/ 下为何既有一批文件、又要 include $(GENERAL_RULES)/Gcc/c++。

3.2 wmake/rules/linux64Mingw/ 目录内容逐项

先列出该目录下实际存在的文件(本仓库真实内容):

文件作用
general交叉工具链三件套:CPP/LD/AR/RANLIB + PROJECT_LIBS + 后缀
c / c++C/C++ 编译命令与架构标志
cOpt/cDebug/cProfC 的 Opt/Debug/Prof 优化项
c++Opt/c++Debug/c++ProfC++ 的 Opt/Debug/Prof 优化项
mplibMSMPIMS-MPI 的 include/lib 设置

逐文件读。

wmake/rules/linux64Mingw/general(第 1–19 行,全文)

# Need -D_WIN32 -D__MINGW32__ if host cpp is used
#CPP     = /lib/cpp -traditional-cpp -D_WIN32 -D__MINGW32__ $(GFLAGS)

CPP     = x86_64-w64-mingw32-cpp -traditional-cpp $(GFLAGS)
LD      = x86_64-w64-mingw32-ld
AR      = x86_64-w64-mingw32-ar
RANLIB  = x86_64-w64-mingw32-ranlib

PROJECT_LIBS = -l$(WM_PROJECT) \
    -L$(FOAM_LIBBIN)/dummy -lPstream

include $(GENERAL_RULES)/standard
include $(DEFAULT_RULES)/c
include $(DEFAULT_RULES)/c++

EXT_SO  = .dll
EXT_EXE = .exe

逐行解读:

  • 第 2–4 行:若用宿主 cpp 需补 -D_WIN32 -D__MINGW32__,但最终采用第 5 行 mingw 自带 cpp,其预定义宏天然是目标平台的。
  • 第 5 行:x86_64-w64-mingw32-cpp -traditional-cpp——-traditional-cpp 让预处理更像 K&R 预处理器(wmake 依赖生成依赖此行为)。
  • 第 6–8 行:LD/AR/RANLIB 全换成 x86_64-w64-mingw32-*——"交叉"的字面体现:宿主 ld/ar 不能处理 Windows COFF 目标。
  • 第 9–10 行:PROJECT_LIBS 默认就把 dummy Pstream 加进来,与 LINKEXE_STUB 同目的(Windows 下倾向无条件带上)。
  • 第 17–18 行:EXT_SO=.dll、EXT_EXE=.exe——输出后缀改 Windows 约定。

wmake/rules/linux64Mingw/c++(第 6–22 行关键段)

include $(GENERAL_RULES)/Gcc/c++

CC         := x86_64-w64-mingw32-g++ -std=gnu++17
c++ARCH    := -m64 -pthread

ifneq (,$(strip $(WM_COMPILE_OPTION)))
    sinclude $(DEFAULT_RULES)/c++$(WM_COMPILE_OPTION)
endif

c++FLAGS    = \
    $(c++ARCH) $(GFLAGS) $(c++WARN) $(c++OPT) $(c++DBUG) $(ptFLAGS) \
    $(FOAM_EXTRA_CXXFLAGS) $(LIB_HEADER_DIRS)

c++FLAGS   += -Wa,-mbig-obj

逐行解读:

  • 第 6 行:include $(GENERAL_RULES)/Gcc/c++——复用 Gcc 家族设置(见 3.3)。
  • 第 8 行:x86_64-w64-mingw32-g++ -std=gnu++17。用 gnu++17 而非 c++17(第 4 行注释),因为 c++17 会定义 __STRICT_ANSI__,屏蔽 GCC 扩展与某些 MinGW 头行为。
  • 第 9 行:-m64 -pthread(64 位 + posix 线程模型)。
  • 第 11–13 行:按编译选项 sinclude 对应优化文件(Opt → linux64Mingw/c++Opt)。
  • 第 22 行:-Wa,-mbig-obj——注释(第 19–21 行):某些文件(如 rhoReactionThermos)超过传统 16 位 COFF section 编号上限,须用大对象格式。这是 x86_64-w64-mingw32 特有的"补丁式编译标志"。

各优化档位文件(全文极短):cOpt(cDBUG=-DNDEBUG; cOPT=-O3)、c++Opt(c++DBUG=-DNDEBUG; c++OPT=-O3; ROUNDING_MATH=-frounding-math)、cDebug(-gstabs3 -DFULLDEBUG; -O1 -finline-functions)、c++Debug(-gstabs3 -DFULLDEBUG; -O0 -fdefault-inline)、cProf/c++Prof(-gstabs3 -pg; -O2)。

注意 -gstabs3:目标是 Windows,调试信息用 stabs(MinGW gas 支持)而非 DWARF;-pg 是 gprof 插桩。二者都是"交叉编译特有的调试/剖析符号格式"。

wmake/rules/linux64Mingw/mplibMSMPI(第 1–3 行):PFLAGS=-DMPICH_SKIP_MPICXX -DOMPI_SKIP_MPICXX(屏蔽第三方 MPI 的 C++ 接口)、PINC=-I$(MPI_ARCH_PATH)/include、PLIBS=-L$(MPI_ARCH_PATH)/lib -L$(MPI_ARCH_PATH)/lib/x64 -lmsmpi(MS-MPI 的 lib/lib/x64 布局,链接 -lmsmpi)。

3.3 Gcc 家族与 common 规则:跨平台共用的那一半

wmake/rules/General/Gcc/c++(第 1–29 行)关键段:

SUFFIXES   += .C .cc .cpp .cxx
CC         := g++$(COMPILER_VERSION) -std=c++17
c++OPT     := -O2
ptFLAGS    := -DNoRepository -ftemplate-depth=1000

c++WARN     = \
    -Wall -Wextra -Wold-style-cast \
    -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof \
    -Wno-attributes -Wno-unknown-pragmas

c++LESSWARN = -Wno-old-style-cast -Wno-unused-local-typedefs -fpermissive

sinclude $(GENERAL_RULES)/common/c++$(WM_COMPILE_OPTION)
  • 第 4 行 SUFFIXES += .C .cc .cpp .cxx:make 后缀规则;.C 大写是 OpenFOAM 传统 C++ 后缀。
  • 第 6 行:g++$(COMPILER_VERSION) -std=c++17。$(COMPILER_VERSION) 由 rules/General/general 第 29–33 行从 WM_COMPILE_CONTROL 的 version= 提取(如 -11)。
  • 第 10–11 行:-DNoRepository 关闭模板仓库链接(OpenFOAM 用 lnInclude 实例化);-ftemplate-depth=1000 放宽模板递归深度。
  • 第 13–19 行:默认警告集;-Wno-* 是有意放行的(OpenFOAM 代码风格需要)。
  • 第 21–25 行:c++LESSWARN 用于 flex++/CGAL 等生成或第三方代码。
  • 第 28 行:sinclude .../common/c++$(WM_COMPILE_OPTION)——引入公共优化档。

wmake/rules/General/common/ 的档位文件:common/c++Opt(c++DBUG:=、c++OPT:=-O3)、common/c++Debug(-g -DFULLDEBUG; -O0)、common/cDebug(同)。

覆盖顺序:linux64Mingw/c++Opt(带 ROUNDING_MATH)与 General/common/c++Opt 同时被 sinclude,Mingw 目录那份更具体、优先——体现 OpenFOAM 规则体系"越具体越优先"的分层思想。

3.4 MinGW 规则链对比表

层级POSIX/Linux(linux64Gcc)MinGW 交叉(linux64Mingw)
预处理 / 编译系统 cpp;g++ -std=c++17x86_64-w64-mingw32-cpp -traditional-cpp;x86_64-w64-mingw32-g++ -std=gnu++17
架构 / 对象无-m64 -pthread;-Wa,-mbig-obj
链接器 / 后缀ld;.so / 空x86_64-w64-mingw32-ld(ARM64 用 lld-link);.dll / .exe
链接命令-shared 默认全导出libso -shared --output-def/--out-implib --no-undefined ...;exe 加 -static-libgcc -static-libstdc++ --force-exe-suffix
符号导出自动.def(链接器 + LLVM_MINGW_EXPORT_DEF)
循环依赖--start-group 可解libo 单对象
调试 / 剖析DWARF;-pg-gstabs3;-gstabs3 -pg(Prof 档)
MPI / 优化 / 链接库OpenMPI;-O3;-lOpenFOAM -ldlMS-MPI(-lmsmpi);-O3 + -frounding-math;-lOpenFOAM -ldl + dummy/-lPstream

4. 三个机制链汇总(交叉编译部分)

为便于回顾,交叉编译的三条机制链概述:A(.def 导出) = 从含 _Z 符号的 .o,经 --output-def 自写、LLVM_MINGW_EXPORT_DEF(llvm-nm+awk [BDRT]+awk ^_Z+sort -u)、-Wl,<def> 重链,得到全导出 .dll;B(libo 破环) = Pstream 源文件经 wmake libo(ld -r / lld-link -lib 合并为单 .o)后由 OpenFOAM 直接吞入,消除循环依赖;C(stub 供符号) = 纯串行构建下经 LINKEXE_STUB=true(gold 时)或 PROJECT_LIBS 默认带的 -L$(FOAM_LIBBIN)/dummy -lPstream 供符号。

本节核心结论:MinGW 交叉编译的 90% 工作量不在"换编译器",而在"换符号模型"——Windows DLL 的显式导出(.def)与链接期严格解析(--no-undefined),迫使 OpenFOAM 用"单对象 libo + .def 全导出 + dummy stub"三件套重写链接流程。

补充一条交叉编译环境搭建清单(模板 T6,可直接照做):

# 0) 在 Linux 宿主安装 mingw-w64 交叉工具链(Debian/Ubuntu 示例)
sudo apt install mingw-w64               # 提供 x86_64-w64-mingw32-{gcc,g++,cpp,ld,ar,ranlib}

# 1) 使用 etc-mingw 下的偏好(FOAM_CONFIG_ETC=etc-mingw)
export FOAM_CONFIG_ETC=etc-mingw

# 2) 指定编译器族为 Mingw、MPI 为 MS-MPI(prefs.sh 已设默认)
export WM_COMPILER=Mingw
export WM_MPLIB=msmpi-10.0

# 3) 确认 WM_OSTYPE 变为 MSwindows(由 settings 依据 WM_COMPILER 自动设置)
source $WM_PROJECT_DIR/etc/bashrc        # 或对应 shell 的启动脚本
echo "WM_OPTIONS=$WM_OPTIONS"            # 期望形如 linux64MingwDPInt32Opt
echo "WM_OSTYPE=$WM_OSTYPE"              # 期望 MSwindows

# 4) 构建(顶层 Allwmake 会自动走到 src/Allwmake 的 MinGW case)
./Allwmake                               # 或:./Allwmake -j -s

# 5) 检查产物后缀:应为 .exe/.dll
ls $FOAM_LIBBIN | head                   # 期望 libOpenFOAM.dll、dummy/libPstream.o(.def)
ls $FOAM_APPBIN | head                   # 期望 *.exe

逐项说明表:

步骤命令要点校验点常见失败
0装 mingw-w64which x86_64-w64-mingw32-g++ 有输出未装工具链 → wmake 找不到 CC
1FOAM_CONFIG_ETC=etc-mingw配置取自 etc-mingw/config.sh/*忘设 → 用到宿主 FFTW/CGAL
2WM_COMPILER=MingwWM_COMPILER 为 Mingw设成 Gcc → 走错规则目录
3source etc/bashrcWM_OPTIONS 含 Mingw、WM_OSTYPE=MSwindows环境未生效
4./Allwmake进入 src/Allwmake 的 */Mingw* 分支循环依赖未解 → 链接 undefined
5看产物后缀 .exe/.dll、存在 .def符号被裁 → 运行期 DLL 加载失败

5. 性能剖析 profiling:开启方式与数据结构

5.1 一句话开启:controlDict 里的 profiling {}

src/OpenFOAM/global/profiling/profiling.H 的类注释(第 30–48 行)就是使用说明:

Description
    Code profiling.

    This is typically activated from within system/controlDict as follows
    (defaults shown):
    \code
        profiling
        {
            active      true;
            cpuInfo     false;
            memInfo     false;
            sysInfo     false;
        }
    \endcode
    or simply using all defaults:
    \code
        profiling
        {}
    \endcode

逐行解读:

  • 第 33 行:明确"activate from within system/controlDict"——入口就是字典,无需重编译、无需 #ifdef。
  • 第 35–42 行:给出带默认值的完整写法:active=true、cpuInfo=false、memInfo=false、sysInfo=false。注意默认只开 active,不开 cpuInfo/memInfo/sysInfo(后三者开销更大或信息更"静态")。
  • 第 44–48 行:profiling {} 即"全用默认"。

模板 T1:system/controlDict 的 profiling 片段(可直接粘贴)

profiling
{
    active      true;   // 总开关:profiling::initialize() 是否构造单例
    cpuInfo     true;   // 输出 CPU 型号/核数(cpuInfo 对象)
    memInfo     true;   // 输出内存 hwm/rss/size/free(memInfo 对象)
    sysInfo     true;   // 输出系统信息(profilingSysInfo)
}

开关项对比表(C4):

开关默认含义(含输出位置)
activetrue总开关;为 false 时 setMonitoring 走"跳过"分支(决定是否打印整个 profiling {} 块)
cpuInfofalse是否实例化 cpuInfo(厂商/型号/核数),一次性探测,块尾写 cpuInfo {}
memInfofalse是否实例化 memInfo 并在每次 New 时更新峰值(每计时点一次),块尾写 memInfo {}
sysInfofalse是否实例化 profilingSysInfo(主机名等),块尾写 sysInfo {};关时仍写 host <名字> 保底

5.2 Time 如何初始化 profiling:setMonitoring

入口在 src/OpenFOAM/db/Time/Time.C 的 Time::setMonitoring(第 350–397 行):

void Foam::Time::setMonitoring(const bool forceProfiling)
{
    const dictionary* profilingDict = controlDict_.findDict("profiling");
    if (!profilingDict)
    {
        // ... or from etc/controlDict
        profilingDict = debug::controlDict().findDict("profiling");
    }

    if (forceProfiling)
    {
        profiling::initialize(IOobject("profiling", timeName(), "uniform", *this,
            IOobjectOption::NO_READ, IOobjectOption::AUTO_WRITE,
            IOobjectOption::REGISTER), *this);
    }
    else if
    (
        profilingDict
     && profilingDict->getOrDefault("active", true)
    )
    {
        profiling::initialize(*profilingDict,
            IOobject("profiling", timeName(), "uniform", *this,
                IOobjectOption::NO_READ, IOobjectOption::AUTO_WRITE,
                IOobjectOption::REGISTER), *this);
    }
}

逐行解读:

  • 第 352 行:先在本算例的 controlDict_ 里找 profiling 子字典。
  • 第 353–357 行:找不到时回退到 etc/controlDict(全局 debug::controlDict())——故可"全局开启"或"按算例开启"。
  • 第 362–378 行:forceProfiling 分支(来自命令行 -profiling),走"全开"重载(cpuInfo/memInfo/sysInfo 全开)。
  • 第 379–397 行:否则检查 profilingDict 存在且 active 不为 false,才调用带字典的重载——逐项读取开关。
  • IOobject 名为 "profiling"、instance 为 "uniform":报告写到 <time>/uniform/profiling(与时间步无关)。NO_READ/AUTO_WRITE/REGISTER 说明它是"只写不读、自动落盘、登记到注册表"。

命令行侧在 Time.C 第 560–561 行:setMonitoring(args.found("profiling"));(-profiling = force profiling, ignore controlDict entry),以及第 692 行 setMonitoring(); // for profiling etc。

开启 profiling 有两条路:① controlDict(或 etc/controlDict)加 profiling {};② 命令行 -profiling 强制全开。二者最终都落到 profiling::initialize(...)。

5.3 profiling 类的静态单例与核心数据成员

profiling.H(第 82–137 行)的类骨架(只列数据成员):

class profiling : public IOdictionary
{
public:
    typedef profilingInformation Information;
    typedef profilingTrigger Trigger;
    static int allowed;                                   // 是否允许 profiling
private:
    static std::unique_ptr<profiling> singleton_;         // 全进程唯一实例
    const Time& owner_;                                   // 持有者
    PtrDynList<Information> pool_;                        // 计时器池(所有权)
    DynamicList<DynamicList<Information*>> children_;     // 父子关系表
    DynamicList<Information*> stack_;                     // LIFO 活动栈
    DynamicList<clockValue> times_;                       // 平行时钟栈
    std::unique_ptr<sysInfo> sysInfo_;                    // 可选
    std::unique_ptr<cpuInfo> cpuInfo_;                    // 可选
    std::unique_ptr<memInfo> memInfo_;                    // 可选
};

逐个数据成员解读("计时树数据结构"的核心):

  • 第 84 行:继承 IOdictionary——所以能 write() 落盘为字典(报告就是 <time>/uniform/profiling),这正是 profilingSummary 能用 IOdictionary::readContents 读它的原因。
  • 第 97 行 static int allowed:由 debug::infoSwitch("allowProfiling", 1) 初始化——即使代码里到处 addProfiling,只要 allowed==0 就全部空转。
  • 第 109 行 singleton_:全进程唯一实例(注释 "Only one global object is possible")。
  • 第 115 行 owner_:持有者 Time,用于 stop 时判断是否同一个 Time。
  • 第 118 行 pool_:所有计时节点 new 后入池,统一内存管理。对象所有权层。
  • 第 121 行 children_:children_[i] 是"id 为 i 的计时器"的所有子指针列表。负责调用树拓扑(复用判断、写出结构都靠它)。
  • 第 124 行 stack_:LIFO 活动栈,进入计时 push_back、退出 pop_back。
  • 第 127 行 times_:与 stack_ 平行的时钟栈。两者必须严格等长、同进同出,这是 writeData 能正确算活动项耗时的前提。
  • 第 130/133/136 行:三个可选信息对象,由 controlDict 的 sysInfo/cpuInfo/memInfo 决定是否 new。

关系示意:pool_ 是全部节点(所有权);children_ 是父子边;stack_/times_ 是当前活动链及其时刻。profiling.H 第 190–223 行还声明受保护的 beginTimer/endTimer 原语、initialize 两个重载、stop(owner)、New(descr)(对外核心入口)与 unstack(info)(结束已知节点计时并校验栈顶一致)。

5.4 allowed 与单例的生命周期

profiling.C(第 39–41 行):

int Foam::profiling::allowed(Foam::debug::infoSwitch("allowProfiling", 1));
std::unique_ptr<Foam::profiling> Foam::profiling::singleton_(nullptr);
  • 第 39 行:allowed 用 debug::infoSwitch("allowProfiling", 1) 初始化——即可以被运行期 InfoSwitches{ allowProfiling 0; } 或环境变量关闭。默认 1(允许)。
  • 第 41 行:singleton_ 初值为 nullptr(未初始化)。

active()/disable()/initialize/stop(第 115–182 行):

bool Foam::profiling::active() noexcept { return allowed && singleton_; }
void Foam::profiling::disable() noexcept { allowed = 0; }
void Foam::profiling::initialize(const IOobject& ioObj, const Time& owner)
{
    if (allowed && !singleton_) { singleton_.reset(new profiling(ioObj, owner)); }
}
void Foam::profiling::stop(const Time& owner)
{
    if (singleton_ && &owner == &(singleton_->owner_)) { singleton_.reset(nullptr); }
}
  • 第 115–118 行:active() 同时要求 allowed 真且单例存在——所有计时原始语第一道门。
  • 第 121–124 行:disable() 直接 allowed=0(profilingSummary 开篇就调它)。
  • 第 149–159 行:initialize 条件 allowed && !singleton_——已存在单例不再重建(单例语义)。
  • 第 176–182 行:stop(owner) 只在"当前单例的 owner 就是传入 owner"时才销毁,避免误关别的 Time。另有两个转发接口 print(os)→writeData、writeNow()→regIOobject::write()(主动落盘)。

生命周期:Time 构造 → setMonitoring → profiling::initialize(建单例)→ 各处 New(...) 压栈出栈 → 单例销毁,但报告已通过 AUTO_WRITE 落盘到 <time>/uniform/profiling。


6. profiling 计时树:机制链与报告解读

6.1 顶层重置与子节点查找/新建:create()

profiling.C(第 46–88 行)。第一个重载是"顶层重置":pool_/children_/stack_/times_ 全部 clear(),再 new Information(默认构造把 description_="application::main"、parent_=this、id_=0,根节点描述固定为 application::main、父指针自指表示顶层),入池并把 children_ 扩到同长。第二个重载是"按 (父, 描述) 找或建":

Foam::profilingInformation* Foam::profiling::create
(
    profilingInformation *parent,
    const std::string& descr
)
{
    const label parentId = parent->id();

    for (Information* child : children_[parentId])
    {
        if (descr == child->description())
        {
            return child;  // Found existing
        }
    }

    Information* info = new Information(parent, descr, pool_.size());

    pool_.push_back(info);
    children_.resize(pool_.size());
    children_.back().clear();  // safety
    children_[parentId].push_back(info);

    return info;
}

逐行:

  • 第 70 行:取父节点 id。
  • 第 72–78 行:线性扫描 children_[parentId],已有同 description 的子节点则直接复用(第 76 行 Found existing)。这是"同一代码位置反复 addProfiling 会复用同一计时器、累加次数"的关键。
  • 第 80 行:新节点 id 取当前 pool_.size()(自增唯一 id)。
  • 第 82–85 行:入池、扩 children_、挂到父的 children 列表。

6.2 压栈与出栈:beginTimer/endTimer

profiling.C(第 91–110 行):

void Foam::profiling::beginTimer(profilingInformation *info)
{
    stack_.push_back(info);
    times_.push_back(clockValue::now());
    info->setActive(true);              // Mark as on stack
}

Foam::profilingInformation* Foam::profiling::endTimer()
{
    Information *info = stack_.back();
    clockValue clockval = times_.back();
    stack_.pop_back();
    times_.pop_back();

    info->update(clockval.elapsed());   // Update elapsed time
    info->setActive(false);             // Mark as off stack

    return info;
}

逐行解读:

  • beginTimer:
    • 第 93 行:stack_.push_back(info)——把节点压入活动栈。
    • 第 94 行:times_.push_back(clockValue::now())——记录进入时刻。clockValue 是一个"可相减得到 elapsed 的时钟值"类型。
    • 第 95 行:info->setActive(true)——标记"在栈上"。
  • endTimer:
    • 第 101–104 行:从 stack_/times_ 各自弹出栈顶(必须成对)。
    • 第 106 行:info->update(clockval.elapsed())——用进入时刻算出 elapsed,交给节点累加。
    • 第 107 行:setActive(false)——标记"离开栈"。
    • 第 109 行:返回该节点指针(供调用方比较)。

这就是计时树的两条原语:beginTimer = 压栈 + 记时 + 置 active;endTimer = 出栈 + 算差 + 置 inactive。profiling::New 与 unstack 都建立在它们之上。

6.3 对外入口 New 与 unstack

profiling.C(第 185–230 行):

Foam::profilingInformation* Foam::profiling::New(const std::string& descr)
{
    Information *info = nullptr;
    if (active())
    {
        Information *parent = singleton_->stack_.back();
        info = singleton_->create(parent, descr);
        singleton_->beginTimer(info);
        if (singleton_->memInfo_)
        {
            singleton_->memInfo_->update();
            info->maxMem_ = Foam::max(info->maxMem_, singleton_->memInfo_->size());
        }
    }
    return info;
}

void Foam::profiling::unstack(const profilingInformation *info)
{
    if (active() && info)
    {
        Information *top = singleton_->endTimer();
        if (info->id() != top->id())
        {
            FatalErrorInFunction << "Profiling information to unstack has different id than"
                << " the top of the profiling stack" << nl << ... << abort(FatalError);
        }
    }
}

逐行解读:

  • New 第 189 行:if (active())——门口检查,未启用直接返回 nullptr。
  • 第 191 行:parent = stack_.back()——当前栈顶就是新节点的父。这就是"调用树"的建立方式:计时纵深即代码调用纵深。
  • 第 193–194 行:create(parent, descr) 找或建 → beginTimer(info) 压栈开始计时。
  • 第 196–205 行:若 memInfo_ 在,刷新内存并更新该节点 maxMem_。每次 New 都更新一次内存峰值,成本与计时点数成正比——故 memInfo 默认关闭。
  • unstack 第 216 行:endTimer() 弹出栈顶。
  • 第 218–228 行:若传入 info 与栈顶 id 不一致,直接 FatalError 中止并打印两者 id 与描述。这保证"每个 addProfiling 的 RAII 对象析构时弹出的正是它自己压的那个节点"——不匹配就是漏配对了 addProfiling/endProfiling。

6.4 节点数据模型:profilingInformation

profilingInformation.H(第 61–85 行)私有数据:

    //- Unique id to identify it
    const label id_;
    //- What this timer does
    const std::string description_;
    //- Pointer to the parent object (or self for top-level)
    profilingInformation* parent_;
    //- Number of times this was called
    long calls_;
    //- Total time spent
    scalar totalTime_;
    //- Time spent in children
    scalar childTime_;
    //- Max memory usage on call.
    mutable int maxMem_;
    //- Is this information active or passive (ie, on the stack)?
    mutable bool active_;

逐项:

  • id_(const):唯一标识,等于入池时的 pool_.size()。
  • description_(const std::string):计时段描述,如 "application::main" 或用户 addProfiling 给的串。
  • parent_:父节点指针;顶层自指。
  • calls_:被调次数。
  • totalTime_:累计总时间。
  • childTime_:子节点花费的时间(用于"自身净时间 = totalTime - childTime")。
  • maxMem_:调用期间内存峰值(memInfo 开时有效)。
  • active_:是否在栈上。

累加逻辑在 profilingInformation.C(第 68–77 行):

void Foam::profilingInformation::update(const scalar elapsed)
{
    ++calls_;
    totalTime_ += elapsed;

    if (id_ != parent_->id())
    {
        parent_->childTime_ += elapsed;
    }
}
  • 第 70 行:++calls_——次数 +1。
  • 第 71 行:totalTime_ += elapsed——总时间累加。
  • 第 73–76 行:若不是顶层(id_ != parent_->id()),把 elapsed 同时累加到父的 childTime_。这正是"父的 childTime 汇总所有子耗时"的落点;顶层自指被排除,避免自我累加。

机制链 C(完整推导):一次 addProfiling 在计时树里发生了什么

addProfiling(Name, "描述") 展开为:
    ::Foam::profilingTrigger profilingTriggerFor##Name(__VA_ARGS__)
        │
        ├─ profilingTrigger 构造:possible()? 若 profiling::active() 则 enter(name)
        │       │
        │       └─ enter(name) → profiling::New(name)
        │                 ├─ 若 active():
        │                 │     parent = stack_.back()           ← 父=当前栈顶
        │                 │     info   = create(parent, name)     ← 池中找/建
        │                 │       └─ 命中 children_[parent] 则复用;否则 new 并入池
        │                 │     beginTimer(info)                  ← stack_.push_back + times_.push_back(now)
        │                 └─ 若 memInfo_ 开:刷新并更新 info->maxMem_
        │
        └─ 作用域结束时 ~profilingTrigger() → stop() → profiling::unstack(ptr_)
                  ├─ 若 active() && ptr_:
                  │     top = endTimer()                         ← pop stack_/times_、info->update(elapsed)
                  │     if (ptr_->id() != top->id()) → FatalError  ← 配对校验
                  └─ 结束

对应宏与类在 profilingTrigger.H:

  • 第 157–158 行:#define addProfiling(Name, ...) ::Foam::profilingTrigger profilingTriggerFor##Name(__VA_ARGS__)——用 ## 把 Name 拼进变量名(profilingTriggerForName),于是同一作用域内每个 Name 唯一。RAII:作用域结束自动析构。
  • 第 162 行:#define endProfiling(Name) profilingTriggerFor##Name.stop()——可以手动提前结束。
  • 第 167–173 行:addProfilingInFunction(Name),GCC 用 __PRETTY_FUNCTION__,其它用 __func__——描述自动取函数名。

profilingTrigger 的模板构造(第 119–129 行)与 trigger(第 88–94 行):

    template<class... StringArgs>
    explicit profilingTrigger(StringArgs&&... description) : ptr_(nullptr)
    {
        if (possible())
        {
            // Delay string concatenation until actually needed
            trigger(std::forward<StringArgs>(description)...);
        }
    }
    ...
    template<class... StringArgs>
    void trigger(std::string name, StringArgs&&... args)
    {
        string_concat(name, std::forward<StringArgs>(args)...);
        enter(name);
    }
  • 第 124 行:if (possible())——先判断 profiling 是否可用,再拼字符串。注释 "Delay string concatenation until actually needed" 直接说明:profiling 关时不付字符串拼接代价。这是"零开销"设计的体现。
  • trigger 把可分片描述拼成完整串后交给 enter。

6.5 报告结构:writeData() 如何打印计时树

profiling.C(第 308–378 行)。先用函数内静态缓冲 elapsed 记录"当前仍在栈上的各项从进入到现在的耗时":

bool Foam::profiling::writeData(Ostream& os) const
{
    static DynamicList<scalar> elapsed;
    const clockValue now(clockValue::now());
    const label nstack = stack_.size();

    elapsed.resize(nstack+1);   // extend for last entry, which has no child.
    for (label stacki=0; stacki < nstack; ++stacki)
    {
        elapsed[stacki] = (now - times_[stacki]);
    }
    elapsed.back() = 0;

    os.beginBlock("profiling");

    // Active items
    for (label stacki=0; stacki < nstack; ++stacki)
    {
        if (stacki) os << nl;   // Extra line between entries
        stack_[stacki]->write(os, true, elapsed[stacki], elapsed[stacki+1]);
    }

    // Non-active items
    for (const Information& info : pool_)
    {
        if (!info.active()) { os << nl; info.write(os); }
    }

    os.endBlock();

    if (sysInfo_) { os << nl; sysInfo_->writeEntry("sysInfo", os); }
    else          { os << nl; os.writeEntry("host", Foam::hostName()); }
    if (cpuInfo_) { os << nl; cpuInfo_->writeEntry("cpuInfo", os); }
    if (memInfo_) { os << nl; memInfo_->update(); os << nl; memInfo_->writeEntry("memInfo", os); }
    return os.good();
}

逐行:

  • 第 310 行:static DynamicList<scalar> elapsed——函数内静态缓冲,避免每次分配。
  • 第 314 行:nstack = stack_.size()——此刻还嵌套在多少层计时段里。
  • 第 318–322 行:对每个在栈节点算 elapsed[i] = now - times_[i];最后一格置 0。
  • 第 326–338 行:先写"活动项"(仍在栈上),info.write(os, true, elapsed[i], elapsed[i+1]):offset=true 表示"次数补 1",并把 elapsed[i] 当额外 elapsed、elapsed[i+1](子项耗时)当 childTimes。
  • 第 340–348 行:再写"非活动项"(已出栈、已累计完的)。
  • 第 352–375 行:按开关写 sysInfo{}(关时至少写 host <名字> 保底)、cpuInfo{}、memInfo{}(落盘前 update() 一次)。

节点自身写出在 profilingInformation.C(第 88–110 行):块名 trigger<id>,依次写 id、parentId(仅非顶层)、description、calls(活动项 +1)、totalTime、childTime、maxMem(仅非 0)、active(yes/no)。

6.6 报告怎么读

报告是一份 OpenFOAM 字典,形如:

profiling
{
    trigger0 { id 0; description application::main; calls 1; totalTime 12.34; childTime 12.30; active no; }
    trigger1 { id 1; parentId 0; description lduMatrix::solve; calls 567;
               totalTime 8.21; childTime 0.05; maxMem 123456; active no; }
    ...
}
host myhost
cpuInfo { ... }
memInfo { ... }

解读四步法:

  1. 看 totalTime - childTime(自身净时间)排序。占比最高的几项就是真正的瓶颈;只看 totalTime 会被"父节点包含子节点"误导。
  2. 看 calls:次数异常多(百万级)而单次极短的项,往往是可被合并/向量化/减少调用的热点(如逐面循环里的某个函数)。
  3. 看调用树(parentId 链):沿 parentId 回溯,能找到"最外层耗时入口"(如某求解器的 solve),再逐层下钻。
  4. 看 maxMem:内存峰值高的节点提示"这里一次性分配了大对象",可能是 tmp/Field 未复用之处。

常见瓶颈排序(经验):线性求解器(PCG/GAMG/PBiCGStab)> 梯度/插值(grad/interpolate)> I/O(读写场/网格)> 边界条件与 correctBoundaryConditions。

注意:profiling 本身有开销(每次 New 至少两次 DynamicList 的 push_back,memInfo 开时还有一次系统调用);别在生产运行里一直开——先用它定位,再关掉跑正式算例。


7. profiling 进阶:profilingSummary 多进程汇总与优化清单

7.1 并行算例的 profiling 分散在各 processor 目录

并行运行时,每个 processorN/ 各自写自己的 <time>/uniform/profiling。要得到"全局视角",OpenFOAM 提供 profilingSummary,位于 applications/utilities/postProcessing/miscellaneous/profilingSummary/。

profilingSummary.cxx(第 62–66 行)定义了要汇总的字段:

const HashTable<wordList> processing
{
    { "memInfo", { "hwm", "rss", "size", "free" } },
    { "profiling", { "calls", "totalTime", "childTime", "maxMem" } },
};
  • 第 63 行:memInfo 汇总 hwm/rss/size/free;第 64 行:profiling 汇总 calls/totalTime/childTime/maxMem。不含 description/id/active(它们是标识而非数值)。

第 83–86 行先主动关闭 profiling:profiling::disable();(→ allowed=0),虽本工具不触发计时,但显式关掉以免混淆信息。

核心汇总逻辑(第 356–378 行):

for (const word& tag : tags)
{
    const auto& values = stats(tag);
    if (values.size() == nProcs)
    {
        MinMax<scalar> limits(values);
        scalar avg = Foam::sum(values) / nProcs;
        if (limits.min() < limits.max())
        {
            outSubDict.set(tag,
                FixedList<scalar, 3>{ limits.max(), avg, limits.min() });
        }
    }
}
  • 第 361 行:只有凑齐 nProcs 个值才汇总(缺一个就 continue,见第 321–324 行)。
  • 第 363–364 行:MinMax<scalar> 求极值、avg = sum / nProcs。
  • 第 366–376 行:仅当 min<max(进程间不一致)时才写 (max avg min) 三元组;全同则保留单值(注释:"If the values are identical for all processes, only a single value is written.")。

第 382–427 行还做按主机的内存高水位汇总:优先读 memInfo.hwm(退化 peak),主机名优先从顶层 host 取(退化 sysInfo.host),按 host 归并 mem_hwm 并求和,写进 memInfo.hwm_total.<host>——因为同一主机上多个 rank 共享物理内存,按主机求和才反映真实占用。

7.2 性能优化的六类手段(结合两季)

结合两季,OpenFOAM 的主要优化手段有:tmp 的 reuse(第一季第 6 章,跨算子零拷贝、复用同一块内存)、表达式模板 + fused(第一季第 6 章,编译期把 a+b*c 折叠成一个循环、零临时、FMA 融合)、LDU 的 CSR 变体(第一季第 11 章,Amul 由按面循环改为按单元循环以改善缓存)、OpenMP(WM_COMPILE_CONTROL=+openmp + #pragma omp,跨核并行)、PrecisionAdaptor/SPDP(第一季第 11 章 solveScalar,存储单精度、求解双精度以省内存带宽)、连续内存的 Field(第一季第 5 章)与 GAMG 缓存聚合(S2-07 cacheAgglomeration_,复用粗化结果)。

并行(MPI)与共享内存(OpenMP)是两套独立加速:前者跨节点、后者跨核。大算例通常是"MPI 分解 + 每进程 OpenMP"。OpenMP 开关在 wmake/makefiles/general(第 72–79 行,+openmp 时把 LINK_OPENMP 加进 SYS_LIBS)与 rules/General/general(第 59–66 行);~openmp(禁用)优先于 +openmp(注释 "disable has higher precedence")。

7.3 -profiling 与 addProfiling 的组合用法

#include "profiling.H"
void myAlgorithm(...)
{
    { addProfiling(myAlgo, "myAlgorithm"); /* ... */ }   // 作用域自动结束(推荐)
    addProfiling(loopBody, "myAlgorithm::loopBody");     // 或手动结束
    for (...) { /* ... */ }
    endProfiling(loopBody);
}

addProfiling(Name, "...") 的 RAII 对象在作用域结束时析构并 unstack;endProfiling(Name) 可提前结束(变量名必须是 profilingTriggerFor<Name>);addProfilingInFunction(Name) 让描述=函数名(GCC 得完整签名);命令行 -profiling 忽略 active 强制全开(cpuInfo/memInfo/sysInfo 也开,开销更大)。

7.4 profiling 排障要点

  • 报告没生成:profiling.active 为 false,或全局 allowProfiling 0 关了 allowed;用 -profiling 可快速判定。
  • 报告只有 trigger0:用户代码没有 addProfiling,只有内建 Stage——不是错误。
  • 并行下只在 processor0 有:属正常,各进程各写一份;要看全局用 profilingSummary。

8. 数据交换:fileFormats 全景

8.1 库的组成:src/fileFormats/Make/files

src/fileFormats/Make/files(第 1–83 行)是一份"支持格式总清单"。按目录归类如下:

行段目录内容/作用
1–2common/库级公共设施(fileFormats.C、manifoldCellsMeshObject)
4–6colours/colourTable(s)、colourTools:可视化配色
8–31ensight/EnSight 读写全套:file/{ensightCase,ensightCaseOptions,ensightFile,ensightGeoFile,ensightWriterCaching}、mesh/ensightMesh(+Options)、output/ensightOutput、part/(第 19 行用 make 变量 part = ensight/part)、read/ensightReadFile、type/ensightPTraits
32–35—abaqus/ABAQUSCore、nastran/NASCore、obj/OBJstream、fire/FIRECore
37–43gltf/glTF 2.0 全套:Object/Accessor/BufferView/Mesh/Scene/SceneWriter/Animation
45json/JSONformatter
47–52starcd/+stl/STAR-CD 与 STL(三种 ASCII 解析器,见第 10 节)
54–57`vtk/basefile`foamVtkCore、类型 traits、foamVtkFileWriter、foamVtkSeriesWriter、foamVtkVtmWriter
60–67vtk/format/formatter 家族:Ascii/Base64/AppendBase64/AppendRaw/Base64Layer/LegacyAscii/LegacyRaw
69–70vtk/output/foamVtkOutput、foamVtkOutputOptions
72–74vtk/part/foamVtkMeshMaps、foamVtuCells、foamVtuSizing
76–80`vtk/readwrite/`vtkUnstructuredReader + line/poly/vertex/surface writer
83—LIB = $(FOAM_LIBBIN)/libfileFormats

src/fileFormats/Make/options(第 1–4 行)极简:

EXE_INC =

LIB_LIBS = \
    -lOpenFOAM
  • 说明 libfileFormats 只依赖 libOpenFOAM,不依赖 finiteVolume——这正是它能被 foamToVTK/foamToEnsight(依赖 finiteVolume)复用的原因:格式层与物理层解耦。

8.2 格式对比表(C1)

维度EnSightVTK(XML/vtu)glTFSTLJSON
读/写读写读写写读写写
二进制有(C Binary)有(base64/raw append)有(bufferView)有(binary)否
时间序列.case 归档.vtm.series动画(Animation)无无
并行分区每 patch/zone 一个 part每 rank 一个 piece单场景无无
顶点/单元布局单元+顶点Points/Cells/OffsetsAccessor/BufferView三角面键值
典型消费端EnSight/CFD-PostParaView浏览器/three.js切片/3D 打印后处理脚本

9. VTK 家族与 base64/append 数据布局

9.1 formatter 家族:把"格式"做成策略对象

VTK 的 formatType 由多个 formatter 类实现,wmake/rules... 不,由 src/fileFormats/Make/files 第 60–67 行可见其家族:

  • foamVtkAsciiFormatter:XML + 内联 ASCII。
  • foamVtkBase64Formatter:XML + 内联 base64。
  • foamVtkAppendBase64Formatter:XML + append 模式,数据追加到 AppendedData,base64 编码。
  • foamVtkAppendRawFormatter:XML + append,raw 二进制。
  • foamVtkLegacyAsciiFormatter / foamVtkLegacyRawFormatter:旧版 VTK(legacy)。
  • foamVtkBase64Layer:base64 编码层(被上面几个 formatter 复用)。

src/fileFormats/vtk/output/foamVtkOutputOptions.H 把这些格式的开关语义写得很清楚(第 131–150 行):

    //- Toggle ASCII mode on/off.
    //  In XML append mode, this switches between base64 and raw binary.
    //  In XML inline mode, this switches between ASCII and base64.
    //  In legacy mode, this switches between ASCII and binary.
    //  \return outputOptions for chaining
    outputOptions& ascii(bool on) noexcept;

    //- Toggle append mode on/off.
    outputOptions& append(bool on) noexcept;

    //- Toggle legacy mode on/off.
    outputOptions& legacy(bool on) noexcept;

逐行解读:

  • 第 131–135 行:ascii(bool) 的语义随上下文变化——XML+append 时是 base64↔raw;XML+inline 时是 ASCII↔base64;legacy 时是 ASCII↔binary。同一个布尔开关映射到三套格式分支。
  • 第 140–142 行:append(bool) 切换"数据放哪里"——内联(inline)还是追加(append)。
  • 第 144–146 行:legacy(bool) 切换新旧 VTK 格式。

foamToVTK 的 -ascii/-legacy 命令行选项正是直接驱动这些开关(见第 12 节)。VTK 输出格式对比表(C7):

模式legacyascii结果
XML inline ASCIIfalsetrue纯文本 XML,人可读、体积大
XML inline base64falsefalseXML + 内联 base64
XML append base64falsetrueAppendedData 段 base64
XML append rawfalsefalseAppendedData 段 raw 二进制
Legacy ASCIItruetrue旧版 .vtk 文本
Legacy binarytruefalse旧版 .vtk 二进制

9.2 base64 编码层:3 字节 → 4 字符

base64Layer.H(第 105–127 行)定义编码长度与接口:encodedLength(len) = 4*((len+2)/3)(第 111–114 行)——每 3 字节出 4 字符、向上取整,是 RFC 4648 标准长度公式的整数写法;write(s,n) 编码输出、reset() 重开一段、close() 补 = 收尾。

实现细节在 base64Layer.C。字母表(第 34–44 行)用 64 个字符 A-Za-z0-9+/。四个编码函数的位切分(第 50–72 行)——encode0/1/2/3 分别取三字节的:

  • encode0:group_[0] 的高 6 位((group_[0] & 0xFC) >> 2)。
  • encode1:group_[0] 的低 2 位(&0x03 <<4)作高、group_[1] 的高 4 位(&0xF0 >>4)作低,拼成第二个索引。
  • encode2:group_[1] 的低 4 位(&0x0F <<2)与 group_[2] 的高 2 位(&0xC0 >>6)拼成第三个。
  • encode3:group_[2] 的低 6 位(&0x3F)作第四个。

add_char(第 75–91 行)把字符塞进 group_[groupLen_++];攒满 3 字节就编成 4 字符、一次写 4 字节、计数清零。close(第 136–172 行)处理尾部:剩 1 字节补 =表(X X = =),剩 2 字节补一个 =(X X X =),否则不写。

这正是 VTK base64 输出的字节级实现:foamVtkBase64Layer 只是把它包成 VTK 语义。

9.3 foamVtkBase64Layer:加一层 VTK 语义

src/fileFormats/vtk/format/foamVtkBase64Layer.cxx(第 73–118 行)里,writeSize 先把 uint64_t numbytes 以 sizeof(uint64_t) 裸字节写入(VTK append 模式要求每段数据带一个 8 字节长度前缀,这就是"append 数据布局"的第一步);write(uint8_t) 直接进 base64 层;write(label)/write(float) 按各自 sizeof 原样写入。唯一特殊的是 write(double)(第 100–118 行):

void Foam::vtk::foamVtkBase64Layer::write(const double val)
{
    // Limit range of double to float conversion
    if (val >= std::numeric_limits<float>::max())
    {
        write(std::numeric_limits<float>::max());
    }
    else if (val <= std::numeric_limits<float>::lowest())
    {
        write(std::numeric_limits<float>::lowest());
    }
    else
    {
        float copy(val);
        write(copy);
    }
}

逐行:

  • 第 105–108 行:值超过 float 最大 → 写 float 最大值(钳位)。
  • 第 109–112 行:低于 float 最小 → 写最小值。
  • 第 114–117 行:否则 float copy(val) 把 double 压成 float。
  • 结论:VTK 输出统一用 32 位浮点。这是"体积/精度"的显式取舍,也解释了 foamToVTK legacy 分支为何要检查 sizeof(float)!=4。

9.4 append 模式的偏移累加:offset()

foamVtkAppendBase64Formatter.cxx(第 74–83 行):

uint64_t Foam::vtk::appendBase64Formatter::offset(const uint64_t numbytes)
{
    uint64_t prev = offset_;

    if (formatter::npos != numbytes)
    {
        offset_ += this->encodedLength(sizeof(uint64_t) + numbytes);
    }
    return prev;
}

逐行:

  • 第 76 行:记住当前偏移。
  • 第 78–81 行:若 numbytes 非 npos(即"要追加一段数据"),则把偏移前进 encodedLength(8 + numbytes)——8 是长度前缀 uint64 的字节数,encodedLength 是 base64 展开后的字符数。这就是"每个 <DataArray> 在 AppendedData 里的偏移量"如何被准确算出来。
  • 第 82 行:返回追加前的偏移(VTK 头里 <DataArray offset="N"> 用的就是这个值)。

机制链 D(完整推导):VTK append-base64 数据布局

写入一个 DataArray(如某个 scalar 场的 32 位浮点数组):
  1) formatter::writeHeader → 记 <DataArray ... offset="prev">   ← 取 offset() 返回值
  2) 数据体写入 AppendedData 流:
       writeSize(uint64 numbytes)              ← 8 字节长度前缀(base64 前是裸字节)
       write(float/label/double→float) ...     ← 元素逐个进 base64Layer::add
       base64Layer::add_char:每攒 3 字节出 4 字符
       close():尾部不足补 '='
  3) offset_ += encodedLength(8 + numbytes)     ← 为下一个 DataArray 预留偏移
  4) 文件尾写入 AppendedData 整段(一次 base64 大块)
ParaView 读取时:按 <DataArray offset> 定位到 AppendedData 内的字节位置,
                 先读 8 字节长度,再读 numbytes 数据,base64 解码还原 float。

9.5 VTK formatter 的 XML 标签栈

foamVtkFormatter.cxx 维护一个"当前打开的 XML 标签栈"来生成合法 XML(第 76–155 行)。openTagImpl:若已在标签内再开标签 → 警告并拒绝(inTag_ 状态机),否则缩进、写 <tag、push_back 入栈、置 inTag_=true。closeTag(isEmpty):isEmpty=true 写自闭合 <tag />(弹栈),否则写 >。endTag(tagName):

Foam::vtk::formatter& Foam::vtk::formatter::endTag(const word& tagName)
{
    const word curr(std::move(xmlTags_.back()));
    xmlTags_.pop_back();
    indent();
    if (inTag_) { WarningInFunction << "adding xml endTag '" << curr << "' but already in another tag!" << endl; }
    if (!tagName.empty() && tagName != curr)
    {
        WarningInFunction << "expecting to end xml tag '" << tagName
            << "' but found '" << curr << "' instead" << endl;
    }
    os_ << "</" << curr << '>' << nl;
    inTag_ = false;
    return *this;
}
  • 第 127–128 行:弹栈取栈顶标签名并缩进。
  • 第 131–136 行:已在另一标签内则警告。
  • 第 142–148 行:校验传入标签名与栈顶是否一致——不符则警告(挑出配错的开闭标签)。
  • 第 150–152 行:写 </tag>、复位状态。

设计要点:用运行时标签栈保证 XML 合法性。所有 VTK 写出(Points/Cells/DataArray/Piece)都经过这几个原语,任何不匹配都会在 stderr 报警,把"手写 XML 易错"转化为"栈状态检查"。

9.6 polyMesh → VTK cell 的映射与并行偏移

src/fileFormats/vtk/part/foamVtuCells.cxx 顶部有个匿名命名空间工具 parCoordinatedOffsets(第 47–87 行),专解"并行下 VTK 的 begin/end offsets 拼接":并行走特殊逻辑(第 55 行);len = size-1(第 57 行);find_first(len > 0) 找到第一个有非空数据的 rank 作 leader(第 60–64 行);leader 返回完整 offsets(含起始 0 offset,第 65–69 行);非 leader 但有数据 → 只返回 slice(1)(丢掉起始 offset,因为它就是 leader 的末端,重复写会让 offsets 出现重复值,第 70–74 行);无数据的 rank → 返回空(不贡献 offset,第 75–79 行);串行直接复制(第 82–86 行)。

注释(第 40–46 行)把动机写透:"From the first viable rank: use the begin/end offsets; Other ranks: use their end offsets only"。VTK 的 <DataArray> 用 offsets 数组描述变长 cell;并行时各 rank 各写一段,若不协调就会有重复的边界 offset。 同类文件里 vtuCells 的构造(第 94–137 行)还按输出类型(LEGACY/HDF/XML,由 outputOptions 的 legacy()/is_hdf() 决定)选分支——同一个 polyMesh,按输出格式选不同 VTK cell 编码。

机制链 E(VTK 侧):polyMesh(任意多面体)→ foamVtuSizing/foamVtuCells 把 cell 映射成 VTK 类型(六面体→HEXAHEDRON、四面体→TETRA、棱柱/棱锥→WEDGE/PYRAMID、多面体→分解为 tets/pyramids 或 POLYHEDRON),生成 Points/connectivity/offsets → 并行用 parCoordinatedOffsets 协调 → formatter 按 formatType 编码(inline 写在 <DataArray> 内,append 写长度前缀+数据到 AppendedData 并累加 offset_)→ foamVtkFileWriter/foamVtkSeriesWriter/foamVtmWriter 落盘为 .vtu/.vtk 或 .vtm+.vtm.series。


10. STL 三解析器与其它格式

10.1 STL:同一格式的三个词法分析器

src/fileFormats/stl/STLReader.C 顶部有个优化开关决定用哪个解析器(第 39–42 行):

int Foam::fileFormats::STLReader::parserType
(
    Foam::debug::optimisationSwitch("fileFormats::stl", 0)
);
  • 用一个 optimisationSwitch("fileFormats::stl", 0) 决定 parser 类型(默认 0)。

选择逻辑在 readASCII(第 65–81 行):

bool Foam::fileFormats::STLReader::readASCII(const fileName& filename)
{
    // No runtime selection of parser (only via optimisationSwitch)
    // this is something that is infrequently changed.
    if (parserType == 1)
    {
        return readAsciiRagel(filename);
    }
    else if (parserType == 2)
    {
        return readAsciiManual(filename);
    }
    return readAsciiFlex(filename);
}

逐行:

  • 第 72–75 行:parserType == 1 → Ragel(STLAsciiParseRagel.cc,由 .rl 生成)。
  • 第 76–79 行:parserType == 2 → 手写(STLAsciiParseManual.C)。
  • 第 80 行:默认 → Flex(STLAsciiParseFlex.L,由 flex 生成)。
  • 注释(第 70–71 行):"No runtime selection of parser"——只在 optimisationSwitch 里调,属"不常改"的东西。

STLAsciiParseFlex.L(第 32–33 行)用 %option 声明 flex 类:

%option prefix="yySTL"
%option yyclass="yySTLFlexLexer"
  • 第 32 行:把生成符号前缀设为 yySTL(避免与其它 flex 文件冲突)。
  • 第 33 行:把扫描器类设为 yySTLFlexLexer(C++ 类封装)。

该文件还有针对 C++17 的兼容补丁(第 45–49 行):

// Flex may use register, which is deprecated and incompatible with C++17
#pragma clang diagnostic ignored "-Wdeprecated-register"
// A 'nothing' define to effectively remove from code as well (issue #3337)
#undef  register
#define register
  • 第 46 行:屏蔽 clang 的 -Wdeprecated-register。
  • 第 48–49 行:把 register 关键字宏定义为空,从机制上消除 C++17 里已弃用的 register。注释注明 issue #3337。

STL 三解析器对比表(C6):

解析器文件生成方式前缀/类取舍
Flexstl/STLAsciiParseFlex.Lflex 生成 C++ 扫描器yySTL / yySTLFlexLexer默认;规则式,可维护
Ragelstl/STLAsciiParseRagel.cc由 .rl 生成—parserType=1;状态机
手写stl/STLAsciiParseManual.C人工编码—parserType=2;零依赖、可读

设计意图:OpenFOAM 保留三种实现做 A/B——既是历史遗留(从手工到 Ragel 再到 Flex),也是"同一问题不同技术的活教材"。

10.2 EnSight:.case 归档与分区组织

ensightCase.H(第 76–117 行)里能看到 EnSight 的组织方式:两个固定名 dataDirName="data"、geometryName="geometry"(EnSight 的"case 文件 + data 目录 + geometry 文件"三段式);os_ 只在 master 上打开(注释 "Output stream (master only)",并行时 case 文件只由主进程写);caseName_ 带 .case 结尾;timeIndex_/timeValue_ 属 timeset 1;timesUsed_ 记录用过的 index/value;geomTimes_ 记录几何写出的时间索引,特殊值 -1 表示静态几何(注释 "The special index '-1' is used for static geometry")。

foamToEnsight 的 -index <start> 选项(见第 13 节)正是改写 data/######## 的编号起点。

10.3 其余格式

glTF(gltf/)是一整套 glTF 2.0 对象(foamGltfObject 基类、Accessor、BufferView、Mesh、Scene、SceneWriter、Animation 时间序列),与规范的"属性→缓冲"层级对应;JSON(json/JSONformatter)供脚本消费;OBJ(obj/OBJstream)主要写出几何;colours(colours/colourTable(s)/colourTools)用于可视化配色;ABAQUS(abaqus/ABAQUSCore)、NASTRAN(nastran/NASCore)是结构分析格式(主要导出网格);FIRE(fire/FIRECore)、STAR-CD(starcd/STARCDCore)是其它 CFD 网格格式。src/conversion/Make/files 里同时出现这些 Core 的读写器,说明格式核心在 fileFormats、转换算法在 conversion。


11. conversion 库与转换工具

11.1 src/conversion/ 的层次

src/conversion/Allwmake(第 1–11 行):

#!/bin/sh
cd "${0%/*}" || exit                                # Run from this directory
targetType=libso                                    # Preferred library type
. ${WM_PROJECT_DIR:?}/wmake/scripts/AllwmakeParseArguments

wmake $targetType

ccm/Allwmake

#------------------------------------------------------------------------------

逐行:

  • 第 3 行:默认目标 libso。
  • 第 8 行:wmake libso 编本目录 libconversion。
  • 第 10 行:再进入 ccm/(AVL FIRE/CCM 需要额外的第三方库,单独构建)。

src/conversion/ 下的子目录与作用:common/(读/写网格的公共骨架:reader/meshReader.* 的 createPolyCells/createPolyBoundary/calcPointCells、writer/meshWriter.*、tables/{cellTable,boundaryRegion},读入抽象网格后统一生成 polyMesh);ccm/(reader/ccmReader*、writer/ccmWriter*、common/ccmBase,AVL FIRE/CCM 读写);ensight/(output/ensightOutputVolField.txx 等,把场转 EnSight);fire/(FIREMeshReader/Writer、checkFireEdges);starcd/(STARCDMeshReader/Writer);polyDualMesh/(生成对偶网格:cell 重心连成新 cell、顶点变新 cell);vtk/(output/foamVtkInternalWriter、foamVtkPatchWriter、foamVtkSurfaceMeshWriter 等 + adaptor/foamVtkVtuAdaptor,foamToVTK 的核心写出器)。

关键点:common/reader/meshReader 是"外部网格 → polyMesh"的统一抽象层。 每种外部格式(FIRE/STAR-CD/CCM)实现自己的 reader,最终都调用 createPolyCells/createPolyBoundary 生成 polyMesh——这解释了 *ToFoam 系列为何能统一。

11.2 上层转换工具(applications/utilities)

src/conversion/Allwmake 编完后,上层工具(applications/utilities)才有依赖可用的库:网格转换(mesh/conversion/)含 gmshToFoam、fluent3DMeshToFoam、star4ToFoam、cfx4ToFoam、netgenNeutralToFoam、ideasUnvToFoam、ensightToFoam、kivaToFoam、plot3dToFoam、fireToFoam、tetgenToFoam、ccm/ccmToFoam、foamToStarMesh、foamToFireMesh、foamToSurface、foamToCcm、vtkUnstructuredToFoam 等;数据转换(postProcessing/dataConversion/)含 foamToVTK、foamToEnsight、foamToGMV、foamDataToFluent、foamToTetDualMesh、smapToFoam、objToVTK 等;并行后处理(parallelProcessing/)含 reconstructPar、reconstructParMesh、redistributePar、decomposePar。

依赖关系上:foamToVTK/foamToEnsight 都 -lfileFormats、-lconversion、-lfiniteVolume——格式库 + 转换库 + 物理库三者组合,这正是"数据交换"的组件拼图。


12. foamToVTK 逐段解读

12.1 Make/options 与 Make/files

foamToVTK/Make/options(第 1–16 行):include 覆盖 finiteArea/finiteVolume/fileFormats/meshTools/conversion/lagrangian/basic/lagrangian/intermediate 的 lnInclude;EXE_LIBS 链接 -lfiniteArea -lfiniteVolume -lfileFormats -lmeshTools -lconversion -llagrangianIntermediate -lgenericPatchFields——说明该工具能处理体积场、面积场、拉格朗日粒子、子集网格。

foamToVTK/Make/files(第 1–3 行):唯一源文件 foamToVTK.cxx,EXE = $(FOAM_APPBIN)/foamToVTK(装到项目 appbin)。

12.2 输出选项解析:getOutputOptions

foamToVTK.cxx(第 227–259 行):

vtk::outputOptions getOutputOptions(const argList& args)
{
    vtk::outputOptions opts;                 // 默认:内联 ASCII XML
    if (args.found("legacy"))
    {
        opts.legacy(true);
        if (!args.found("ascii"))
        {
            if constexpr (sizeof(float) != 4 || sizeof(label) != 4)
            {
                opts.ascii(true);
                WarningInFunction << "Using ASCII rather than legacy binary VTK ..." << endl;
            }
            else { opts.ascii(false); }
        }
    }
    else { opts.ascii(args.found("ascii")); }
    return opts;
}

逐行解读:

  • 第 230 行:默认就是"内联 ASCII XML"(注释)。
  • 第 232–234 行:-legacy → opts.legacy(true)。
  • 第 236 行:若没同时给 -ascii(想要 legacy binary),先做编译期尺寸检查 if constexpr (sizeof(float)!=4 || sizeof(label)!=4)。
  • 第 240–245 行:尺寸不符(如 label 是 64 位)时强制退回 ASCII并警告——legacy binary VTK 硬编码 4 字节 float/label,不符会写出坏文件。这是"用 if constexpr 做编译期防错"的漂亮用法。
  • 第 252–257 行:非 legacy 时 ascii 由 -ascii 决定(XML 下 ASCII↔base64)。

12.3 主流程:时间循环 + 多区 + 网格代理

主函数关键段落(第 510–520 行配置、第 728–760 行目录管理、第 777–945 行时间循环):

    const bool decomposePoly = args.found("poly-decomp");
    const bool doBoundary    = !args.found("no-boundary");
    const bool doInternal    = !args.found("no-internal");
    const bool doLagrangian  = !args.found("no-lagrangian");
    const bool doFiniteArea  = !args.found("no-finite-area");
    const bool doEdgeFields  = args.found("edgeFields");
    const bool doSurfaceFields = args.found("surfaceFields");
    const bool oneBoundary   = args.found("one-boundary") && doBoundary;
    const bool nearCellValue = args.found("nearCellValue") && doBoundary;
    const vtk::outputOptions writeOpts = getOutputOptions(args);
  • 第 510–517 行:把一堆布尔选项解包,统一采用 !args.found("...") 的"否定命名"(no-boundary → doBoundary)。
  • 第 517 行:oneBoundary、nearCellValue 需同时 doBoundary 才有意义(短路逻辑)。
  • 第 520 行:调 getOutputOptions。

输出目录管理(第 728–760 行):默认 <globalPath>/VTK;-name 可改目录名(绝对或相对),outputDir.clean() 去掉多余的 ..;只有 master 建目录,逐 region 建 VTK/<regionName>/,-overwrite 时先 rmDir。并行时非 master 不碰文件系统。

时间循环(第 777–945 行,节选关键):

forAll(timeDirs, timei)
{
    runTime.setTime(timeDirs[timei], timei);
    const word timeDesc = "_" + Foam::name(runTime.timeIndex());
    const scalar timeValue = runTime.value();

    vtk::vtmWriter vtmMultiRegion;      // 多区 VTM 累加

    forAll(regionNames, regioni)
    {
        auto& meshProxy = meshProxies[regioni];
        auto& vtuMeshCells = vtuMappings[regioni];
        polyMesh::readUpdateState meshState = meshProxy.readUpdate();
        if (meshState == polyMesh::TOPO_CHANGE || meshState == polyMesh::TOPO_PATCH_CHANGE)
        { vtuMeshCells.clear(); }       // 触发 vtk cell 重映射
        ...
    }

    if (UPstream::master() && regionNames.size() > 1)
    {
        fileName outputName(outputDir/outputBaseName + "-regions" + timeDesc + ".vtm");
        vtmMultiRegion.setTime(timeValue); vtmMultiRegion.write(outputName);
        fileName seriesName(vtk::seriesWriter::base(outputName));
        vtk::seriesWriter& series = vtkSeries(seriesName);
        if (series.empty()) { series.load(seriesName, true, timeValue); }
        series.append(timeValue, outputName); series.write(seriesName);
    }
}
  • 第 781–782 行:timeDesc = "_<timeIndex>"、timeValue。用 timeIndex 而非 timeName 作后缀——与 VTM/系列命名一致。
  • 第 788 行:多区用 vtk::vtmWriter 汇总。
  • 第 807–819 行:meshProxy.readUpdate() 检查网格变化;拓扑或 patch 变化时 vtuMeshCells.clear(),强制重生成 VTK cell 映射(注释 "Trigger change for vtk cells too")。
  • 第 916–940 行:多区时 master 写一个 *.vtm(多块索引)并维护 *.vtm.series。第 933–937 行 if (series.empty()) series.load(...)——内存里没有就先从文件载入,再 append、write。这保证多次调用 foamToVTK 追加时间步而不是覆盖。

各字段写出由若干 #include 的小文件完成(convertVolumeFields.H/convertSurfaceFields.H/convertAreaFields.H/convertLagrangian.H/convertTopoSet.H/convertProcessorPatches.H)。

12.4 foamToVTK 命令清单(模板 T3)

foamToVTK                                       # 基本:全部时间步 → VTK/(默认 XML inline ASCII)
foamToVTK -fields '(p U "alpha.*")'             # 只转指定字段(正则)
foamToVTK -no-boundary                           # 只要内部体网格
foamToVTK -surfaceFields -edgeFields             # 同时写面/边场(如 phi)
foamToVTK -cellZone water                        # 只转子集 cellZone
foamToVTK -with-ids -with-point-ids              # 附带 mesh id 字段
foamToVTK -poly-decomp                           # 多面体分解为 tet/pyramid
foamToVTK -name vtk_out -base-name run1          # 自选目录/前缀
foamToVTK -legacy                                # 旧版 VTK
foamToVTK -time 100:200                          # 时间范围

逐项说明:默认输出 VTK/<region>/<case>_<index>.vtu + .vtm.series;-fields 用 wordRes 正则白名单;-no-boundary 不写 patch 面;-surfaceFields/-edgeFields 额外写面/边场;-cellZone 用 fvMeshSubsetProxy 取子集;-with-ids/-with-point-ids 追加 cellID/procID/patchID/pointID(体积略增);-poly-decomp 分解多面体(cell 数变多);-name/-base-name 改目录/前缀;-legacy 旧版格式(64 位 label 时自动退 ASCII);-time 走 timeSelector。


13. foamToEnsight 逐段解读

13.1 Make/files 与选项

applications/utilities/postProcessing/dataConversion/foamToEnsight/Make/files(第 1–4 行):

readFields.cxx
foamToEnsight.cxx

EXE = $(FOAM_APPBIN)/foamToEnsight

主函数 foamToEnsight.cxx 把选项映射成 ensightCase::options 与 ensightMesh::options(第 344–417 行):

    // Default to binary output, unless otherwise specified
    const IOstreamOption::streamFormat format =
    (
        args.found("ascii") ? IOstreamOption::ASCII : IOstreamOption::BINARY
    );

    const bool doBoundary    = !args.found("no-boundary");
    const bool doInternal    = !args.found("no-internal");
    const bool doCellZones   = !args.found("no-cellZones");
    const bool nearCellValue = args.found("nearCellValue") && doBoundary;

    label indexingNumber(0);
    const bool doConsecutive = args.readIfPresent("index", indexingNumber);
    bool doGeometry = !args.found("no-mesh");

    ensightCase::options caseOpts(format);
    caseOpts.nodeValues(doPointValues && args.found("nodeValues"));
    caseOpts.width(args.getOrDefault<label>("width", 8));
    caseOpts.overwrite(!args.found("no-overwrite"));

    ensightMesh::options writeOpts;
    writeOpts.useBoundaryMesh(doBoundary);
    writeOpts.useInternalMesh(doInternal);
    writeOpts.useCellZones(doCellZones);
  • 第 344–349 行:默认二进制(IOstreamOption::BINARY),-ascii 才改 ASCII。与 VTK 默认 ASCII 相反——EnSight 的 "C Binary" 是其常用格式。
  • 第 352–358 行:一批 do* 布尔(同 foamToVTK 的否定命名风格)。
  • 第 361–362 行:-index <start> → indexingNumber;doConsecutive 表示"用连续编号而非时间索引"。
  • 第 365–366 行:-no-mesh → 不写几何(可只转结果)。
  • 第 382 行:ensightCase::options caseOpts(format)。第 385 行 nodeValues(...)——强制插值到节点;第 386 行 width 默认 8(data/######## 编号位宽);第 387 行默认覆盖旧 EnSight 目录。
  • 第 392–414 行:把 patch/cellZone/faceZone 选择直接设进 ensightMesh::options(patchSelection/patchExclude/faceZoneSelection/cellZoneSelection)。

13.2 时间循环与几何写出

主循环关键段(第 499–603 行):

    if (UPstream::master())
    { Info<< "Converting " << timeDirs.size() << " time steps" << nl; }

    #include "checkMeshMoving.H"
    if (hasMovingMesh && !doGeometry)
    { Info<< "has moving mesh: ignoring '-no-mesh' option" << endl; doGeometry = true; }

    #include "findCloudFields.H"
    #include "checkFieldAvailability.H"
    ...
    forAll(timeDirs, timei)
    {
        runTime.setTime(timeDirs[timei], timei);
        #include "getTimeIndex.H"
        Info<< "Time [" << timeIndex << "] = " << runTime.timeName() << nl;

        forAll(regionNames, regioni)
        {
            auto& mesh = meshes[regioni];
            polyMesh::readUpdateState meshState = mesh.readUpdate();
            const bool moving = (meshState != polyMesh::UNCHANGED);
            auto& ensCase = ensightCases[regioni];
            auto& ensMesh = ensightMeshes[regioni];
            ensCase.setTime(timeDirs[timei], timeIndex);

            if (moving) { ensMesh.expire(); ensMesh.correct(); }

            if ((timei == 0 || moving) && doGeometry)
            {
                autoPtr<ensightGeoFile> os = ensCase.newGeometry(hasMovingMesh);
                ensMesh.write(os.ref());
            }
            ...
        }
    }
  • 第 506–511 行:checkMeshMoving 判断动网格;有动网格时 -no-mesh 被忽略(必须每次写几何)并提示。
  • 第 514–517 行:找 cloud(拉格朗日)字段与检查字段可用性。
  • 第 534 行:getTimeIndex.H 计算 EnSight 时间索引(-index 时用连续编号)。
  • 第 550–557 行:读网格更新状态(moving = state != UNCHANGED),ensCase.setTime(...)——把时间写进 .case。
  • 第 570–580 行:动网格时 ensMesh.expire() + correct()——令缓存几何失效并重算。
  • 第 582–590 行:仅第一个时间步或网格移动时写几何(timei==0 || moving 且 doGeometry),用 ensCase.newGeometry(hasMovingMesh) 得几何文件再 ensMesh.write。

收尾(第 657–668 行):循环结束后统一 write() 各区的 .case 文件。数据文件在循环里写、case 文件在末尾写——这是 EnSight 的组织顺序。

13.4 foamToEnsight 命令清单(并入模板 T3)

foamToEnsight                                    # 基本:默认 C Binary → EnSight/
foamToEnsight -ascii                             # ASCII 版
foamToEnsight -no-mesh                           # 只写结果、不写(静态)几何
foamToEnsight -fields '(p U T)'                  # 指定字段
foamToEnsight -no-boundary -cellZones water       # 只要内部网格 + 指定 cellZone
foamToEnsight -index 1000                         # 连续编号(base 从 1000 起)
foamToEnsight -nodeValues                         # 强制节点插值
foamToEnsight -time 500:1000 -name EnSight_run1   # 时间范围 + 目录改名

EnSight vs VTK 用法差异对比表:

维度foamToVTKfoamToEnsight
默认格式XML inline ASCIIC Binary
开关-ascii/-legacy/-poly-decomp-ascii/-nodeValues/-index/-width
输出目录VTK/(-name 改)EnSight/(-name 改)
多区.vtm + .vtm.series每区一个 .case
编号用 timeIndex-index 连续编号 / -width 位宽
子集-cellSet/cellZone/faceSet/pointSet-cellZones/-faceZones/-patches

14. reconstructPar 与并行拼装算法

14.1 主流程:从 processorN/ 到整域

reconstructPar.C 的入口先确定进程数与建立各进程 Time(第 217–266 行):

    label nProcs{0};
    if (regionNames.empty())
    { FatalErrorInFunction << "No regions specified or detected." << exit(FatalError); }
    else if (regionNames[0] == polyMesh::defaultRegion)
    { nProcs = fileHandler().nProcs(args.path()); }
    else
    { nProcs = fileHandler().nProcs(args.path(), regionNames[0]); }

    if (!nProcs)
    { FatalErrorInFunction << "No processor* directories found" << exit(FatalError); }

    // Warn fileHandler of number of processors
    fileHandler().constCast().nProcs(nProcs);

    // Create the processor databases
    PtrList<Time> databases(nProcs);
    forAll(databases, proci)
    {
        databases.set(proci, new Time(Time::controlDictName, args.rootPath(),
            args.caseName()/("processor" + Foam::name(proci)),
            args.allowFunctionObjects(), args.allowLibs()));
    }
  • 第 219–237 行:通过 fileHandler().nProcs() 自动探测进程数(非用户传参);region 为默认 polyMesh 时查根路径,否则查指定 region 子目录。
  • 第 239–244 行:nProcs == 0 → "No processor* directories found"。并行拼装第一道门:没有 processorN 目录就无法重建。
  • 第 247 行:fileHandler().constCast().nProcs(nProcs)——告知 fileHandler 进程数(后续 filePath 解析依赖它)。
  • 第 250–266 行:为每个 processor{proci} 建独立 Time。"每个进程是独立算例"的体现——拼装时分头读它们。第 303–307 行再把所有进程 Time 对齐到重建网格时间。

14.2 场重建的核心:fvFieldReconstructor

fvFieldReconstructor.H(第 57–86 行)的数据成员:

class fvFieldReconstructor
{
    //- Reconstructed mesh reference
    fvMesh& mesh_;
    //- List of processor meshes
    const UPtrList<fvMesh>& procMeshes_;
    //- List of processor face addressing lists
    const UPtrList<labelIOList>& faceProcAddressing_;
    //- List of processor cell addressing lists
    const UPtrList<labelIOList>& cellProcAddressing_;
    //- List of processor boundary addressing lists
    const UPtrList<labelIOList>& boundaryProcAddressing_;
    //- Number of fields reconstructed
    label nReconstructed_;
};
  • mesh_:重建后的整域网格(引用)。
  • procMeshes_:各 processorN 的网格列表。
  • faceProcAddressing_ / cellProcAddressing_ / boundaryProcAddressing_:三套映射表——这是"并行拼装算法"的核心数据。它们由 decomposePar 时生成,存于 processorN/constant/polyMesh/{cellProcAddressing,faceProcAddressing,boundaryProcAddressing},记录"进程局部 index → 全局 index"。
  • nReconstructed_:计数(用于统计"没有场"的情况)。

体积场的重建算法在 fvFieldReconstructor.txx(第 37–71 行):

template<class Type>
Foam::tmp<Foam::DimensionedField<Type, Foam::volMesh>>
Foam::fvFieldReconstructor::reconstructField
(
    const IOobject& fieldObject,
    const UPtrList<DimensionedField<Type, volMesh>>& procFields
) const
{
    // Create the internalField
    Field<Type> internalField(mesh_.nCells());

    forAll(procMeshes_, proci)
    {
        const auto& procField = procFields[proci];
        internalField.rmap(procField.field(), cellProcAddressing_[proci]);
    }

    auto tfield = tmp<DimensionedField<Type, volMesh>>::New
    (
        fieldObject, mesh_, procFields[0].dimensions(), internalField
    );
    tfield.ref().oriented() = procFields[0].oriented();
    return tfield;
}

逐行解读(并行拼装最核心的一段):

  • 第 46 行:internalField(mesh_.nCells())——按全局网格单元数分配(不是按某个 processor 的单元数)。
  • 第 53–57 行:遍历所有进程,调用 internalField.rmap(procField.field(), cellProcAddressing_[proci])。rmap 是"反向映射":dst.rmap(src, addr) 表示"对每个 i,把 src[addr[i]] 写到 dst[i]"。addr 即该进程的"局部 i → 全局 g",于是把进程局部值散播到全局场的对应单元。多个 proci 顺序调用,天然完成拼装——每个全局单元只被一个进程写一次。
  • 第 59–68 行:组装成以 mesh_ 为基础的新场(维度取自 procFields[0]),oriented() 亦继承。

边界场更复杂(同文件第 74–165 行):

        forAll(boundaryProcAddressing_[proci], patchi)
        {
            const label curBPatch = boundaryProcAddressing_[proci][patchi];
            const labelList::subList cp =
                procField.mesh().boundary()[patchi].patchSlice(faceProcAddressing_[proci]);
            if (curBPatch >= 0)
            {
                const label curPatchStart = mesh_.boundaryMesh()[curBPatch].start();
                labelList reverseAddressing(cp.size());
                forAll(cp, facei)
                {
                    if (cp[facei] <= 0)
                    {
                        FatalErrorInFunction << "Processor " << proci << " patch "
                            << procField.mesh().boundary()[patchi].name()
                            << " face " << facei << " originates from reversed face since "
                            << cp[facei] << exit(FatalError);
                    }
                    reverseAddressing[facei] = cp[facei] - 1 - curPatchStart;
                }
                patchFields[curBPatch].rmap(procField.boundaryField()[patchi], reverseAddressing);
            }
        }

逐行解读:

  • 第 100–110 行:为每个进程 patch 取 curBPatch = boundaryProcAddressing_[proci][patchi](该进程 patch 对应全局哪个 patch),并用 patchSlice(faceProcAddressing_[proci]) 取该 patch 的面地址子列表 cp。
  • 第 113 行:curBPatch >= 0 → 普通物理 patch。
  • 第 135–136 行:curPatchStart 是全局 patch 在全局面数组里的起点。
  • 第 140–153 行:cp[facei] <= 0 直接 FatalError——注意 faceProcAddressing 用正负号编码面方向,≤0 说明数据损坏。
  • 第 157 行:reverseAddressing[facei] = cp[facei] - 1 - curPatchStart。-1 与 -curPatchStart 抵消全局 faceProcAddressing 存的"1-based + patch 偏移",还原成该 patch 内的 0-based 索引(OpenFOAM 约定全局面索引存为 ±(globalFace+1))。
  • 第 161–165 行:patchFields[curBPatch].rmap(...) 用反向地址把进程边界值写进全局 patch。

机制链 F(完整推导):reconstructPar 的并行拼装算法

decomposePar 阶段(前置):
   为每个进程生成三张 addressing 表:
     cellProcAddressing  : 局部 cell i → 全局 cell g
     faceProcAddressing  : 局部 face f → ±(全局 face + 1)   (符号编码方向)
     boundaryProcAddressing : 局部 patch p → 全局 patch P(或 -1/-表示 processor 侧)
   存到 processorN/constant/polyMesh/

reconstructPar 阶段(本节):
   1) fileHandler().nProcs() 探测进程数 → 建 nProcs 个 Time(各读 processorN)
   2) timeSelector 选时间 → 各进程 Time 对齐
   3) 读整域网格 fvMesh(从 constant/polyMesh)
   4) processorMeshes procMeshes(databases, region)   ← 装载各进程网格 + 三张 addressing
   5) 每个时间步:
        a) mesh.readUpdate() / procMeshes.readUpdate()   ← 处理动网格
           - POINTS_MOVED → procMeshes.reconstructPoints(mesh)
        b) 扫描 processor0 的 IOobjectList → 得到要重建的场清单
        c) 体积场:fvFieldReconstructor
             internalField(全局 nCells)
             for proci: internalField.rmap(procField, cellProcAddressing[proci])
        d) 边界场:对每个进程 patch
             curBPatch = boundaryProcAddressing[proci][patchi]
             cp = patchSlice(faceProcAddressing[proci])
             reverseAddressing = cp - 1 - patchStart      (还原 0-based,符号即方向)
             patchFields[curBPatch].rmap(...)
        e) 点场:pointFieldReconstructor
        f) 拉格朗日:lagrangianReconstructor(两趟:先收云名,再重建)
        g) 面积场:faFieldReconstructor
        h) sets:cellSet/faceSet/pointSet 用 cellMap/faceMap/pointMap 反查
        i) hexRef8Data(AMR 细化数据)、uniform/ 目录从 master 复制

14.3 sets 与 uniform 的重建

sets 重建(第 631–812 行,节选):cellSet 用 cellMap[celli]、pointSet 用 pointMap[pointi] 把进程局部索引映射到全局;faceSet 用 fSet.insert(mag(faceMap[facei])-1)——**先取绝对值(去方向符号)再减 1(去 1-based 偏移)**得到全局 face 索引。uniform 目录复制(第 959–992 行):把 processor0 的 uniform/ 目录(time 文件、profiling 报告等)拷到重建的时间目录——因为 uniform 里是"全域一致"的元数据,直接取 master 的即可;多区时第 0 区再额外拷一次顶层 uniform/。

14.4 reconstructPar 命令清单

# 1) 基本:重建所有时间、所有场(跳过已有)
reconstructPar

# 2) 只重建新时间(增量,避免覆盖)
reconstructPar -newTimes

# 3) 只重建指定字段
reconstructPar -fields '(p U)'

# 4) 不重建拉格朗日/集合
reconstructPar -no-lagrangian -no-sets

# 5) 只重建网格(等价于 reconstructParMesh)
reconstructPar -no-fields

# 6) 指定时间范围
reconstructPar -time 1000:5000

# 7) 多区
reconstructPar -allRegions
选项语义典型场景
-newTimes只重建 master 尚无的时间长时间运行中增量收集
-fields白名单只关心几个场
-no-lagrangian跳过粒子无拉格朗日
-no-sets跳过 sets不需要 cellSet 等
-no-fields只重建网格配合 -constant
-time时间选择timeSelector 语法

15. 并行 I/O 模式与结果重建总览

15.1 两种并行 I/O 模式

模式产物形态读取方式适用控制项
uncollated(分散写)各进程各写 processorN/<time>/...必须 reconstructPar 或按进程读大多数并行算例;可用 reconstructPar 灵活拼装默认
collated(合并写)单套文件(配合 fileHandler),文件数少可单文件直接读(ParaView/postProcess)大规模算例、单文件读取友好fileHandler / writeFormat

controlDict 的 writeFormat 控制 ASCII/binary;fileHandler 控制 collated/uncollated 与各类 fileHandler 实现(如 collated、uncollated、adios)。大规模算例常用 collated + 并行 I/O 库(如 modules/adios 的 ADIOS2)以提升写盘带宽。

15.2 并行后处理命令清单(模板 T3 的第三部分)

# 分解 → 运行 → 重建的完整链路
decomposePar                 # 生成 processorN/
mpirun -np 8 solver -parallel
reconstructPar               # 拼回整域时间目录

# 只看网格结构
reconstructParMesh -constant

# 重分布(改进程数后重新分区)
redistributePar -parallel

# 转换并行结果(foamToVTK 支持并行运行,只由 master 写盘)
mpirun -np 8 foamToVTK -parallel

15.3 数据交换全链路

一句话概括:外部网格格式经 *ToFoam(+ src/conversion/common/reader)转成 polyMesh → 求解产生结果(collated 或 processorN/)→ 后处理分四路:reconstructPar(拼回整域)、foamToVTK(VTK/.vtm.series)、foamToEnsight(EnSight/.case)、postProcess(functionObjects)。


16. modules/:官方可选扩展

16.1 modules 与 plugins 的区别

modules/README.md(第 10–13 行)说清了二者的分野:"modules are mainly maintained and released by OpenFOAM developers, whereas plugins are an open and welcoming area for add-ons, predominantly maintained and driven by OpenFOAM community members and groups."——modules 是官方维护、随主仓库发布;plugins 是社区贡献的开放区。

16.2 submodule 机制

modules/README.md(第 21–25 行)给出首次使用步骤:

cd $WM_PROJECT_DIR
git submodule update --init --recursive
  • 一条命令克隆全部(含嵌套)子模块;可只初始化指定项:git submodule update --init --recursive modules/turbulence-community。git submodule status --recursive 看状态,cat .gitmodules 看清单,git submodule deinit <name> 清空某项。

本仓库 .gitmodules 里的 modules 条目:

子模块路径仓库
adiosmodules/adiosgitlab.com/openfoam/modules/adiosfoam.git
OpenQBMMmodules/OpenQBMMgithub.com/OpenQBMM/OpenQBMM.git(branch openfoam.com)
visualizationmodules/visualizationgitlab.com/openfoam/modules/visualization.git
external-solvermodules/external-solvergitlab.com/openfoam/modules/external-solver.git

(另有 plugins/ 下的 cfmesh、avalanche、bindings/pybFoam、pyOFTools、vtk-hdf、turbulence-community、data-community、research。)

16.3 构建入口:modules/list-modules 与 modules/Allwmake

modules/list-modules(第 68–82 行):

# Each first-level directory with an Allwmake file
for moduleName in *
do
    if [ -f "$moduleName/Allwmake" ]
    then
        case "$moduleName" in
        (build | doc | platform*)   # Skip some directory names
            ;;
        (*)
            echo "$moduleName"
            ;;
        esac
    fi
done
  • 判定标准:第一级目录且含 Allwmake 文件;跳过 build/doc/platform*。

modules/Allwmake(第 1–39 行)关键段:

targetType=libso
[ -n "$FOAM_MODULE_PREFIX" ] || FOAM_MODULE_PREFIX="${FOAM_LIBBIN%/*}"
export FOAM_MODULE_PREFIX

case "$FOAM_MODULE_PREFIX" in
(false | none)      # 允许通过 prefix 禁用整个 modules 构建
    echo "OpenFOAM modules disabled (prefix=${FOAM_MODULE_PREFIX})"
    exit 0
esac

set +e
export WM_CONTINUE_ON_ERROR=true

for moduleName in $(./list-modules)
do
    if [ -d "$moduleName" ]
    then
        ( cd "$moduleName" && wmake -all $targetType )   # -all 允许 Allwmake.override
    fi
done
  • 第 11 行:FOAM_MODULE_PREFIX 默认取 ${FOAM_LIBBIN%/*}——即默认装到 OpenFOAM 项目位置。
  • 第 15–20 行:前缀为 false/none 时整体禁用 modules并干净退出。
  • 第 27–28 行:set +e + WM_CONTINUE_ON_ERROR=true——模块构建出错不中止(模块是可选组件)。
  • 第 32 行:list-modules 列出各项,cd 进去 wmake -all——注释点明用 wmake -all 而非 Allwmake 以允许 Allwmake.override 覆盖。

16.4 构建位置与 -prefix

modules/README.md(第 86–91、106–135 行)给出 -prefix 对照表与模块代码要求:

命令安装位置
./Allwmake -prefix=user$FOAM_USER_APPBIN、$FOAM_USER_LIBBIN
./Allwmake -prefix=group$FOAM_SITE_APPBIN、$FOAM_SITE_LIBBIN
./Allwmake -prefix=openfoam$FOAM_APPBIN、$FOAM_LIBBIN
./Allwmake -prefix=/some/pathname/some/pathname/bin、/some/pathname/lib

模块要支持各位置,Make/files 应写成 EXE = $(FOAM_MODULE_APPBIN)/...、LIB = $(FOAM_MODULE_LIBBIN)/...;Make/options 里 include $(GENERAL_RULES)/module-path-user(或旧版兼容写法:sinclude .../module-path-user 再 ifeq (,$(FOAM_MODULE_APPBIN)) FOAM_MODULE_APPBIN=$(FOAM_USER_APPBIN) endif)。

顶层 Allwmake(第 75–97 行)也处理 modules:FOAM_MODULE_PREFIX=false/none 时打印"modules disabled、可用 ./Allwmake-modules -prefix=... 单独构建",否则 cd modules && wmake -all。

modules 模块对比表:

模块作用依赖
adios/ADIOS2 高性能并行 I/O(大规模写盘)ADIOS2 库
visualization/ParaView Catalyst 原位可视化(免写盘出图)Catalyst
external-solver/外部线性求解器(PETSc/Hypre 等)集成PETSc/Hypre
OpenQBMM/矩方法/群体平衡(成核、聚并)—

17. applications/test/:单元测试体系

17.1 规模

applications/test/ 下有 309 个一级目录(本仓库实测 Get-ChildItem -Directory 计数),一个目录一个测试程序(多数命名为 Test-<thing>.C/.cxx,Make/ 内两个文件)。覆盖面从基础类型到并行通信、从容器到 AMR。

从命名可看覆盖面(部分):基础类型(argList、autoPtr、dictionary、tmp、refPtr);场与张量(Field/volField、tensor/tensorFields);表达式(exprEntry/exprValue1/2/exprTraits);容器(hashTable1..4/HashSet/HashPtrTable、List/List2);网格与求解(hexRef8、GAMMAgglomeration、dynamicIndexedOctree);并行(parallel-comm0..3b、parallel-nonBlocking、parallel-scan 等);coded(codeStream、dynamicLibrary);IO/环境(ied、IOobject、etcFiles、foamVersion、wmake1)。

17.2 逐段:wmake1/Test-wmake1.C

applications/test/wmake1/Test-wmake1.C(第 48–61 行)演示"如何测试 wmake 的条件编译":

int main(int argc, char *argv[])
{
    printTest();

    #ifdef FULLDEBUG
    Info<< nl << "Compiled with FULLDEBUG" << nl;
    #else
    Info<< nl << "Regular compilation" << nl;
    #endif

    Info<< "\nEnd\n" << nl;
    return 0;
}
  • 调用 printTest()(在 newStub.C/oldStub.C 之一里定义,用于测试 Make/files 的源文件切换)。
  • #ifdef FULLDEBUG 分支打印——-DFULLDEBUG 由 Debug 档位的 c++DBUG 提供(见 3.2 的 cDebug/c++Debug)。用同一个源文件验证"Opt/Debug 编译路径是否真的注入/未注入宏"。

17.3 逐段:parallel-comm0(通信器基础)

applications/test/parallel-comm0/Test-parallel-comm0.cxx(第 47–131 行)节选:

void printInfo(const label comm)
{
    Info<< "comm:" << comm << " nprocs:" << UPstream::nProcs(comm)
        << " all:" << UPstream::allProcs(comm) << " sub:" << UPstream::subProcs(comm) << nl;
}

int main(int argc, char *argv[])
{
    argList::noBanner();
    argList::noCheckProcessorDirectories();
    ...
    label worldVal = returnReduce(val, sumOp<label>(), UPstream::msgType(), UPstream::commWorld());
    label selfVal  = returnReduce(val, sumOp<label>(), UPstream::msgType(), UPstream::commSelf());

    bitSet localUsed(nProcs);
    localUsed.set(myRank, ((myRank % 4) == 0));
    localUsed.reduceOr(UPstream::worldComm, false);

    bitSet procUsed(bitSet::allGather((myRank % 4) == 0));

    PackedList<2> uniformity(10);       // 0:empty 1:uniform 2:nonuniform 3:mixed
    ...
    Foam::reduce(uniformity.data(), uniformity.num_blocks(), bitOrOp<unsigned int>(),
        UPstream::msgType(), UPstream::worldComm);
}
  • 第 48–61 行:打印世界/自身通信器的 nProcs/allProcs/subProcs。
  • 第 92–98 行:returnReduce 在 commWorld 与 commSelf 上求 sumOp<label>——验证"同一归约在不同通信器上结果不同"。
  • 第 113–120 行:bitSet::reduceOr 与 bitSet::allGather——测试位集归约/全收集。
  • 第 133–173 行:PackedList<2>(每位 2 bit,编码 4 种均匀性)+ Foam::reduce + bitOrOp——测试对"打包位集"的按位或归约(注释给出 0:empty/1:uniform/2:nonuniform/3:mixed 编码)。

17.4 逐段:foamVersion

applications/test/foamVersion/Test-foamVersion.cxx(第 43–112 行)节选:

int main()
{
    foamVersion::printBuildInfo(Info.stdStream());         // 打印构建信息
    Info<< "version " << Foam::FOAMversion << " build " << Foam::FOAMbuild
        << " buildArch " << Foam::FOAMbuildArch << nl;     // 宏
    Info<< "api " << foamVersion::api << " patch " << foamVersion::patch
        << " version " << foamVersion::version << nl;      // 命名空间

    for (const std::string& str : { "MSB;label=32;scalar=64", "LSB;label=64;scalar=32",
                                    "LSB;label=;scalar=junk", "LSB label=32 scalar=64" })
    { testExtraction(str); }                                // 尺寸串解析健壮性
}
  • 第 55 行:foamVersion::printBuildInfo——打印构建信息。
  • 第 58–61 行:宏 Foam::FOAMversion/FOAMbuild/FOAMbuildArch。
  • 第 66–70 行:命名空间 foamVersion::api/patch/version/build/buildArch。
  • 第 43–49 行 + 第 90–106 行:用一组"畸形/正常"尺寸串测试 labelByteSize/scalarByteSize 的解析健壮性——这正是"版本/精度元数据"的单元测试。

17.5 用法清单(模板 T5)

# 编译并运行单个测试
cd $WM_PROJECT_DIR/applications/test/hashTable1
wmake            # 生成 Test-hashTable1
./Test-hashTable1

# 有算例的测试(自带头部算例目录)
cd ../volField
wmake && ./Test-volField -case cavity

# 批量编译全部测试(applications/Allwmake 会遍历)
cd $WM_PROJECT_DIR/applications && ./Allwmake -test
用法命令说明
单测编译wmake生成 Test-<name> 二进制
运行./Test-<name>打印 Info/断言结果
带算例-case <dir>部分测试自带 case1/、cavity/ 等
选学对象直接读 Test-*.C每个测试是"可运行的文档"

18. tutorials/ 端到端回归与版本元数据

18.1 tutorials/:端到端回归集

tutorials/ 下每个求解器都有对应教程算例,配合 Allrun/Allclean 脚本可批量"跑一遍看是否正常"。这是新版本发布前的主要验证手段:既验证求解器数值正确,也验证 I/O、后处理脚本在真实算例上可用。

18.2 版本元数据与一致性校验

  • META-INFO/:含 api-info 与 README.md,记录 API/补丁级别。顶层 Allwmake 末尾会 sed 打印 META-INFO/api-info(第 117 行)。
  • wmake -build-info:查询 {api, branch, build}。
  • foamEtcFile -show-api / -show-patch:脚本友好的一行输出。
  • foamVersion(applications/test/foamVersion):打印全部版本信息。

顶层 Allwmake(第 20–43 行)在编译前还会报告环境:list_tools(或 mpirun 位置)、wmake -show-path-cxx 得到的编译器路径与 --version 首行、wmake -show-cxxflags,并打印 WM_PROJECT_DIR、WM_COMPILER [WM_COMPILE_CONTROL]、WM_OPTIONS, with WM_MPLIB FOAM_MPI——构建日志本身就是一份可复现的环境快照。


19. 常见报错与排障(27 条)

下表按"现象 → 成因 → 排查 → 解决"组织,覆盖本节的五个主题面(第 1–24 条为 MinGW/profiling/VTK/Ensight/reconstructPar 相关,第 25–27 条为环境/元数据相关)。

#现象成因排查解决
1MinGW 链接 undefined reference to ..._Z...Windows DLL 中该 C++ mangled 符号未被导出llvm-nm --defined-only --extern-only <lib>.o | awk '$2~/[BDRT]/ && $3~/^_Z/'用 LLVM_MINGW_EXPORT_DEF 生成 .def,再 -Wl,<def> 重链
2运行 exe 报"找不到 XXX.dll"DLL 未随 exe 部署,或 PATH 无 FOAM_LIBBIN看依赖 x86_64-w64-mingw32-objdump -p X.exe | grep DLL用 _foamAddLib "$FOAM_LIBBIN/dummy";把 FOAM_LIBBIN 加入 Windows PATH
3循环依赖:OpenFOAM 编译期找不到 Pstream 符号Pstream↔OpenFOAM 成环,DLL 模型无法 --start-group看 src/Allwmake 是否进入 */Mingw* 分支确认 Pstream/Allwmake libo + FOAM_LINK_DUMMY_PSTREAM=libo 已执行
4.def 文件缺失libo 未生成 .def(非 ARM64 分支不自动生成)ls $FOAM_LIBBIN/dummy/*.defARM64 走 LLVM_MINGW_EXPORT_DEF;x86_64 可用 nm 手工生成
5wmake 报找不到 x86_64-w64-mingw32-g++宿主未装 mingw-w64 工具链which x86_64-w64-mingw32-g++apt install mingw-w64
6交叉编译编译到宿主的 .so 而非 .dll未设 FOAM_CONFIG_ETC=etc-mingw,规则目录错echo $WM_OPTIONS 是否含 Mingw设 FOAM_CONFIG_ETC=etc-mingw 并重新 source etc/bashrc
7WM_OSTYPE 不是 MSwindowsWM_COMPILER 未设为 Mingw*echo $WM_COMPILERexport WM_COMPILER=Mingw
8编译报 too many sections大文件超 16 位 COFF section 上限看是否缺 -mbig-obj规则已加 c++FLAGS += -Wa,-mbig-obj(第 22 行)
9调试信息无法被 Windows 调试器识别用了 DWARF 而非 stabs看 c++Debug 是否 -gstabs3用 Debug 档位(-gstabs3)
10链接期 -lPstream 找不到dummy stub 未建或路径错ls $FOAM_LIBBIN/dummy设 WM_MPLIB=dummy 或依赖 PROJECT_LIBS 默认 dummy 目录
11profiling 报告没生成active 被设 false,或全局 allowProfiling 0grep -i 'allowProfiling' 于 etc/controlDictcontrolDict 加 profiling{},或命令行 -profiling 强制
12profiling 报告只有 trigger0用户代码没插 addProfiling查报告 description 仅 application::main在关键段加 addProfiling(Name, "...")
13profiling 运行变慢memInfo 每次计时点都 update()(系统调用)看 controlDict 是否开了 memInfo关掉 memInfo(默认 false)
14并行 profiling 只在 processor0 有各进程各写一份ls processor*/<time>/uniform/profiling用 profilingSummary 汇总
15profilingSummary 报找不到文件某些 rank 缺 uniform/profiling看输出 "Found k/n profiling files. Skipping"确保所有 rank 都开了 profiling 并写了报告
16addProfiling 后 FatalError "different id than the top"addProfiling/endProfiling 配错对看报错打印的 info/top id 与描述保证同一作用域/同名变量配对
17foamToVTK 输出 VTK 打开报 XML 错开闭标签不匹配(formatter 状态机触发警告)看 stderr 的 WarningInFunction: ... xml ...修输出(一般不会,属内部问题);确认版本
18foamToVTK 缺字段字段被 -fields/-no-fields/类别剪枝过滤看 Info 的 "Including/Excluding fields"显式 -fields,或去掉 -no-point-data/-no-surface 类限制
19legacy binary VTK 尺寸不符警告sizeof(float)!=4 或 sizeof(label)!=4看警告 "Using ASCII rather than legacy binary"该情形自动退 ASCII,属预期;改用 XML 格式
20foamToEnsight 数据编号跳号用时间索引而非连续编号看 data/######## 文件名用 -index <start> 连续编号
21foamToEnsight 动网格 -no-mesh 无效有动网格时必须每步写几何看 Info "has moving mesh: ignoring '-no-mesh'"属预期;如需静态几何先冻结网格
22reconstructPar 报 "No processor* directories found"未 decomposePar,或路径/大小写错查 ls processor*先 decomposePar;确认在算例根目录运行
23reconstructPar 边界场面 originates from reversed facefaceProcAddressing 符号 ≤0,说明数据损坏看报错 patch/face 索引重新 decomposePar(勿手工改 addressing 文件)
24多次 foamToVTK 后时间序列乱.vtm.series 未增量更新或被截断看 .vtm.series 内容依赖内置 series.load→append→write;勿手工删 series

补充三条与环境/元数据相关的排障:

#现象成因排查解决
25wmake 用了错误规则目录WM_COMPILER 家族识别错(版本后缀未剥离)看 COMPILER_FAMILY(sed 's/[-+.0-9~].*//')检查 WM_COMPILER 值(如 Gcc/Mingw)
26modules 未构建FOAM_MODULE_PREFIX=false/none看顶层 Allwmake 的 "modules disabled"./Allwmake-modules -prefix=openfoam
27某 module 编译失败但整体不报错WM_CONTINUE_ON_ERROR=true 有意放行看 modules/<name> 的 wmake 输出单独 cd modules/<name> && wmake -all libso 排查

20. 工程工具链收官

把 S2-15 与本节合起来,一个 OpenFOAM 开发者需要掌握的工具链闭环是:

写代码    → wmake(S2-15、第一季第 2 章)
调试      → Info/FatalError/DebugInfo + applications/test
性能      → profiling(controlDict)→ 定位瓶颈 → OpenMP/SPDP/算法
并行      → decomposePar → 运行 → reconstructPar
后处理    → functionObjects / foamToVTK / foamToEnSight / postProcess
跨平台    → MinGW 交叉编译(.def / libo / stub)
扩展      → libs(...) / modules(adios/visualization/external-solver)/ plugins
验证      → applications/test + tutorials + META-INFO

五条主线回顾:

主线核心机制关键文件
跨平台构建.def 显式导出 + libo 单对象破环 + dummy stubsrc/Allwmake、wmake/makefiles/general、wmake/rules/*Mingw
性能剖析计时树(pool_/children_/stack_/times_)+ 报告字典src/OpenFOAM/global/profiling/*
数据交换formatter 家族 + base64/append 布局 + meshReader 抽象src/fileFormats/*、src/conversion/*
结果重建cellProcAddressing.rmap + face 反向寻址fvFieldReconstructor.txx、reconstructPar
扩展与验证submodule modules + applications/test 自检modules/*、applications/test/*

贯穿两季的主题:OpenFOAM 用三层抽象(运行期选择 RTS、字典驱动、模板 + 零拷贝)把"配置、物理、数值、并行、网格"统一在一个可编程框架里;而工程工具链则把这份框架"能构建、可测量、可交换、可验证"地交付给使用者。理解这两面,就能在任意一层(库/应用/配置/构建)扩展 OpenFOAM。

本节核心结论:工程面看似"外围",实则决定了源码能否落地。MinGW 的"符号模型差异"、profiling 的"计时树",到 VTK 的"base64/append 布局"与 reconstructPar 的"反向映射拼装",每一处都是用具体数据结构解决具体平台/性能/格式约束的范例。