OpenFOAM 源码深度剖析 · 第二季-12 `coded` 系列:在字典里写 C++ 并在运行期编译

OpenFOAM 源码深度剖析 · 第二季-12 coded 系列:在字典里写 C++ 并在运行期编译

OpenFOAM 有一个"魔法":你可以在字典文件里直接写 C++ 代码 ,程序会在运行期把它编译成 .so、加载、并执行。这就是 #codeStream 与 coded* 系列类。它让"改一个边界条件/源项/函数对象"不再需要重新编译整个 OpenFOAM------是 OpenFOAM "配置驱动"哲学的极致形态。本章从字典里的 #{ ... #} 原样文本一路讲到 SHA1 缓存键、运行期 wmake libso、dlopen/dlsym 与函数指针缓存、codedBase 的热更新判定与钩子函数。

前置建议:第一季第 2 章(wmake、Make/files/Make/options)、第一季第 3 章(Time 持有 dlLibraryTable)。coded 是这些机制在运行期的汇流点。


0. 本章地图

本章要回答的源码级问题(括号为对应章节):

  1. #{ ... #} 在字典里怎么被解析成 token?(§7.1)
  2. dynamicCodeContext 读了哪些键?SHA1 怎么算?为什么 codeOptions 不加 #line?(§3)
  3. 生成的目录与库文件叫什么名?codeName 与 codeDirName 为何不同?(§4.1)
  4. 模板怎样被"复制 + 变量替换"成可编译源文件?${typeName} 在哪被替换?(§4.3、§5)
  5. Make/files、Make/options 内容?LIB = $(PWD)/../platforms/$(WM_OPTIONS)/lib 什么意思?(§4.4)
  6. wmake libso 做了什么?为什么 coded 强依赖本机编译器?(§4.5)
  7. .so 怎么加载?dlsym 找哪个符号?为什么每个库都有 extern "C" void <typeName>_<SHA1>(bool) 钩子?(§6、§8.5)
  8. 函数指针缓存在哪?"改一行就重编"的判定条件?(§4.6、§6.3)
  9. 并行/NFS 下只让 master 编译再广播的机制?(§4.8、§8.3)
  10. 哪些编译标志、WM_OPTIONS、lnInclude 参与,ABI 一致性为何重要?(§14、§15)

0.1 四条机制推导链(先给结论,后文逐条证明)

  • 链 A(源码生成) :#{ ... #} verbatim token → dynamicCodeContext::readEntry → SHA1 → addCompileFile/addCopyFile → copyAndFilter(${...} 替换)→ 落盘 .C/.H。
  • 链 B(编译) :createMakeFiles/createMakeOptions → writeDigest(写 Make/SHA1Digest)→ wmakeLibso() → lib<codeName>.so。
  • 链 C(加载) :dlLibraryTable::open → dlOpen → loadHook(dlsym 找 <typeName>_<SHA1> 并执行 fun(true))→ 函数指针 → 句柄缓存进 libPtrs_。
  • 链 D(重编译判定) :每次 updateLibrary(name) 重建 dynamicCodeContext(重算 SHA1)→ 新 libPath() → findLibrary 命中即返回 → 否则比 Make/SHA1Digest 的 upToDate → 不同则重编重载。

1. coded 家族全景

形态 / 类 运行期生成对象 C 模板 头模板 关键代码键 所在库
functionEntries::codeStream extern "C" void (Ostream&, const dictionary&) 自由函数 codeStreamTemplate.C 无 code/codeInclude/codeOptions/codeLibs src/OpenFOAM
codedFixedValueFvPatchField fixedValueFvPatchField 子类 fixedValueFvPatchFieldTemplate.C .H 同上 + localCode/codeContext src/finiteVolume
codedMixedFvPatchField mixedFvPatchField 子类 mixedFvPatchFieldTemplate.C .H 同上 src/finiteVolume
codedFixedValuePointPatchField fixedValuePointPatchField 子类 fixedValuePointPatchFieldTemplate.C .H 同上 src/OpenFOAM
functionObjects::codedFunctionObject functionObject 子类 functionObjectTemplate.C .H codeRead/codeExecute/codeWrite/codeEnd/codeData utilityFunctionObjects
fv::CodedSource fv::cellSetOption 子类 codedFvOptionTemplate.C .H codeCorrect/codeAddSup/codeAddSupRho/codeConstrain src/fvOptions
codedPoints0MotionSolver motionSolver 子类 codedPoints0MotionSolverTemplate.C .H code src/dynamicMesh
Function1Types coded 函数 Function1 子类 codedFunction1Template.C .H code src/OpenFOAM
codedPatchFunction1 PatchFunction1 子类 codedPatchFunction1Template.C .H code src/finiteVolume

所有 coded 类都继承 codedBase,实现其五个纯虚接口 libs()/description()/clearRedirect()/codeDict()/prepare(),从而把各自的代码片段注入 dynamicCode。codeStream 是例外 :它不继承 codedBase,直接使用 dynamicCode(它生成的只是一个自由函数,不是对象子类)。

codedBase.H(src/OpenFOAM/db/dynamicLibrary/codedBase/ 第 30--40 行)的定位:

vbnet 复制代码
Base class for function objects and boundary conditions using dynamic code
that provides methods for managing loading/unloading/updating of a dynamic
library. ... For simple coded objects, the default state management is
sufficient. When there are more complicated code segments
(eg, functionObjects::codedFunctionObject), the state management must also
register these elements as well, starting with an initial setCodeContext()
call and followed by append() to register each element.

最后一句是关键:代码片段多于一个时,codedBase 默认只对"主 code 字典"做哈希,必须显式 append() 把额外片段也纳入哈希,否则改了别的片段却不重编(§8.2、§11.2)。


2. 底层三件套总览

类 位置 职责 关键方法
dynamicCodeContext db/dynamicLibrary/dynamicCode/dynamicCodeContext.{H,C} 保存片段、算 SHA1、给出编译选项与库 sha1()、options()、libs()、include()、localCode()、code()、dict()、setCodeContext()、append()
dynamicCode 同目录 dynamicCode.{H,C} 生成源文件、写 Make、调 wmake、管路径与 digest reset()、addCompileFile()、addCopyFile()、setMakeOptions()、setFilterVariable()、copyOrCreateFiles()、wmakeLibso()、libPath()、upToDate()
dlLibraryTable db/dynamicLibrary/dlLibraryTable/ 动态库表(dlopen/dlsym/dlclose),挂在 Time 上 open()、findLibrary()、close()、loadHook()、unloadHook()、fullname()、basename()、静态 libs()

数据流:

scss 复制代码
字典 dict (code/codeInclude/codeOptions/codeLibs)
  │ dynamicCodeContext ctx(dict):setCodeContext 逐键 readEntry,边读边 append 进 SHA1
  │                          sha1() → 40 位十六进制;str(true) → "_<sha1>"
  ▼ dynamicCode dynCode(codeName, codeDirName)
  │ reset(ctx) → 设 code/codeInclude/localCode/SHA1sum 替换变量
  │ prepare()  → 追加 typeName/TemplateType/FieldType 等 + addCompileFile/addCopyFile
  │ copyOrCreateFiles() → 模板变量替换落盘 + 写 Make/files、Make/options、Make/SHA1Digest
  │ wmakeLibso() → system("wmake -s libso <codePath>")
  ▼ lib<codeName>.so (<case>/dynamicCode/<codeDirName>/platforms/$WM_OPTIONS/lib/)
  │ dlLibraryTable::open(libPath) → dlopen
  │ loadHook(handle, codeName) → dlsym(codeName) → (*fun)(true)
  ▼ 真实对象:patchField / functionObject / fvOption / motionSolver / 自由函数

符号名区分 :dlsym 用的名字是 codeName(codeStream 为 codeStream_<sha1>,coded 为 <name>_<sha1>);模板另外导出 extern "C" void <typeName>_<SHA1>(bool) 钩子,其名恰等于 codeName(因 prepare 令 typeName=短名、SHA1sum=无前缀摘要)。该等式是"能加载"的前提(§8.5)。


3. dynamicCodeContext:字典片段的解析与 SHA1

文件:src/OpenFOAM/db/dynamicLibrary/dynamicCode/dynamicCodeContext.H / .C。

3.1 数据结构(逐行)

dynamicCodeContext.H(第 54--78 行):

cpp 复制代码
class dynamicCodeContext
{
    std::reference_wrapper<const dictionary> dict_;   //- parent dictionary
    SHA1 sha1_;                                       //- SHA1 of contents
    string codeOptions_;   //- "codeOptions" (optional)
    string codeLibs_;      //- "codeLibs"    (optional)
    string codeInclude_;   //- "codeInclude" (optional)
    string localCode_;     //- "localCode"   (optional)
    string code_;          //- "code"        (optional)

逐行解读:

  • dict_ 用 std::reference_wrapper<const dictionary>(可重绑定、不拥有),默认构造指向 dictionary::null(.C 第 80--83 行),因此 good() 靠"是否还指向 null"判断(.C 第 96--99 行 return &(dict_.get()) != &(dictionary::null);)。
  • sha1_ 是增量哈希 :每次成功 readEntry 都 sha1_.append(...);setCodeContext 开头 sha1_.clear() 重置。
  • 五个字符串对应五个可选代码键,映射为:codeOptions→codeOptions_、codeLibs→codeLibs_、codeInclude→codeInclude_、localCode→localCode_、code→code_。
  • 访问器(第 132--178 行):options()/libs()/include()/localCode()/code()/sha1()/dict()。

3.2 setCodeContext:读键顺序与 #line 策略(逐行)

dynamicCodeContext.C(第 163--177 行):

cpp 复制代码
void Foam::dynamicCodeContext::setCodeContext(const dictionary& dict)
{
    dict_ = std::cref<dictionary>(dict);
    sha1_.clear();

    // No #line for options (Make/options)
    readIfPresent("codeOptions", codeOptions_, false);

    // No #line for libs (LIB_LIBS)
    readIfPresent("codeLibs", codeLibs_, false);

    readIfPresent("codeInclude", codeInclude_);
    readIfPresent("localCode", localCode_);
    readIfPresent("code", code_);
}

逐行解读:

  1. dict_ = std::cref<dictionary>(dict);:绑定新字典。
  2. sha1_.clear();:清空哈希重算------这是热更新能生效的前提。
  3. readIfPresent("codeOptions", codeOptions_, false):第三参 false = 不加 #line 。因为 codeOptions 最终写进 Make/options(编译/链接命令行,如 -I.../-l...),不是 C++ 源文件,加 #line 只会是垃圾。
  4. codeLibs 同理不加 #line。
  5. 后三个(codeInclude/localCode/code)用默认 withLineNum=true,即加 #line 。因为它们会被拼进 .C,加 #line 才能让编译器报错指回字典文件与行号。

3.3 readEntry:读内容、展开、喂哈希、加 #line(逐行)

dynamicCodeContext.C(第 108--149 行):

cpp 复制代码
bool Foam::dynamicCodeContext::readEntry(const word& key, string& str,
                                         bool mandatory, bool withLineNum)
{
    str.clear();
    sha1_.append("<" + key + ">");

    const dictionary& dict = this->dict();
    const entry* eptr = dict.findEntry(key, keyType::LITERAL);

    if (!eptr)
    {
        if (mandatory)
        {
            FatalIOErrorInFunction(dict)
                << "Entry '" << key << "' not found in dictionary " << dict.name()
                << nl << exit(FatalIOError);
        }
        return false;
    }

    eptr->readEntry(str);
    dynamicCodeContext::inplaceExpand(str, dict);
    sha1_.append(str);

    if (withLineNum)
    {
        addLineDirective(str, eptr->startLineNumber(), dict);
    }
    return true;
}

逐行解读(三个易忽略细节):

  1. str.clear();:先清空输出串,保证多次调用不残留旧内容。
  2. sha1_.append("<" + key + ">");:先把 <键名> 标签喂进哈希 。这样把内容从 code 挪到 localCode(内容一样但语义不同)也会得出不同哈希,避免缓存误命中。
  3. dict.findEntry(key, keyType::LITERAL):LITERAL 精确查找,不解释正则。找不到且 mandatory → FatalIOError:Entry 'code' not found in dictionary ...。
  4. eptr->readEntry(str);:把 entry 的 token 读成字符串。
  5. inplaceExpand(str, dict)(第 34--42 行):先 stringOps::inplaceTrim(去首尾空白,对编译选项必要),再 stringOps::inplaceExpand(按字典上下文展开 $var/${var},不是按环境变量 )。这也是 codeStream.H 里 ~ 限制的根源。
  6. sha1_.append(str);:把展开后的最终文本喂进哈希------哈希针对"实际参与编译的文本"。
  7. if (withLineNum) addLineDirective(str, eptr->startLineNumber(), dict);:加 #line。在哈希之后加------行号不进哈希,改行号不触发重编。

3.4 addLineDirective(逐行)

dynamicCodeContext.C(第 45--64 行):

cpp 复制代码
unsigned Foam::dynamicCodeContext::addLineDirective(string& code, label lineNum, const string& file)
{
    ++lineNum;  // Change from 0-based to 1-based
    const auto len = code.length();

    if (lineNum > 0 && len && !file.empty())
    {
        code = "#line " + Foam::name(lineNum) + " \"" + file + "\"\n" + code;
        return (code.length() - len);
    }
    return 0;
}

逐行解读:

  1. ++lineNum;:startLineNumber() 是 0 基,#line 是 1 基。
  2. if (lineNum > 0 && len && !file.empty()):三重保护,任一不满足则不加 #line(防止生成 #line 0 "" 非法指令)。
  3. 拼出 #line 12 "system/controlDict"\n<原始代码>;编译器报错会显示 system/controlDict:12,用户能直接跳到字典那行。返回新增长度用于偏移恢复。
  4. 另一重载(第 67--75 行)用 dict.name() 作文件名,即上面调用的版本。

3.5 SHA1 的构成

sha1_ 的输入序列:

xml 复制代码
<codeOptions> + 展开后 codeOptions
<codeLibs>    + 展开后 codeLibs
<codeInclude> + 展开后 codeInclude
<localCode>   + 展开后 localCode
<code>        + 展开后 code

外加:对 codedFunctionObject 等,codedBase::append() 再把 codeExecute/codeWrite 等片段 sha1_.append(...) 进来(§11.2)。所以 SHA1 是"这套代码的全部语义输入 + 键名标签"的指纹。

SHA1::str(bool prefixed)(src/OpenFOAM/primitives/hashes/SHA1/SHA1.H 第 143--144 行)返回 40 位十六进制;prefixed=true 时前缀一个下划线 _。于是:

  • codeStream.C 第 112--113 行:sha1Str = sha1().str(true) → "_<sha1>";dynamicCode("codeStream" + sha1Str, sha1Str) → codeName="codeStream_<sha1>"、codeDirName="_<sha1>"。
  • codedBase.C 第 310--314 行:dynamicCode(name + sha1().str(true), name) → codeName="<name>_<sha1>"、codeDirName="<name>"。

这个下划线前缀决定了两类对象在文件系统上的落位 :codeStream 每变一次代码就落到新目录 _<新sha1>;coded 对象目录名固定为 <name>,SHA1 只体现在库文件名 上(lib<name>_<sha1>.so)。


4. dynamicCode:从上下文到 .so

文件:src/OpenFOAM/db/dynamicLibrary/dynamicCode/dynamicCode.H / .C(640 行)。

4.1 静态常量与路径公式

dynamicCode.C 静态数据(第 44--60 行):

cpp 复制代码
int Foam::dynamicCode::allowSystemOperations = Foam::debug::infoSwitch("allowSystemOperations", 0);
const Foam::word     Foam::dynamicCode::codeTemplateEnvName = "FOAM_CODE_TEMPLATES";
const Foam::fileName Foam::dynamicCode::codeTemplateDirName = "codeTemplates/dynamicCode";
const char* const    Foam::dynamicCode::targetLibDir = "LIB = $(PWD)/../platforms/$(WM_OPTIONS)/lib";
const char* const    Foam::dynamicCode::topDirName   = "dynamicCode";
  • allowSystemOperations:来自 InfoSwitches,默认 0(关闭)------运行期代码执行的安全闸门(§4.7)。
  • codeTemplateEnvName:环境变量名,优先从该目录找模板(§4.3)。
  • codeTemplateDirName:回退路径,经 findEtcFile 在 <etc>/ 下解析(etc/codeTemplates/dynamicCode/)。
  • targetLibDir:写进 Make/files 的一行;$(PWD) 是 wmake 运行目录,../platforms/$(WM_OPTIONS)/lib 是产物落点。
  • topDirName:动态代码根目录名(相对 case)。注意 codeStream.H 旧文档写"local codeStream directory",当前代码实际统一为 <case>/dynamicCode/。

构造与路径(第 291--324 行):

cpp 复制代码
Foam::dynamicCode::dynamicCode(const word& codeName, const word& codeDirName)
:
    codeRoot_(argList::envGlobalPath()/topDirName),
    libSubDir_(stringOps::expand("platforms/${WM_OPTIONS}/lib")),
    codeName_(codeName), codeDirName_(codeDirName)
{
    if (codeDirName_.empty()) { codeDirName_ = codeName_; }
    clear();
}

Foam::fileName Foam::dynamicCode::codeRelPath() const { return topDirName/codeDirName_; }
Foam::fileName Foam::dynamicCode::libPath() const
{ return codeRoot_/libSubDir_/dlLibraryTable::fullname(codeName_); }
Foam::fileName Foam::dynamicCode::libRelPath() const
{ return codeRelPath()/libSubDir_/dlLibraryTable::fullname(codeName_); }

逐行推导:

  1. codeRoot_ = argList::envGlobalPath()/topDirName:envGlobalPath() 返回 FOAM_CASE,故 codeRoot_ = <case>/dynamicCode。
  2. libSubDir_ = stringOps::expand("platforms/${WM_OPTIONS}/lib"):展开 WM_OPTIONS(如 linux64GccDPInt32Opt),得 platforms/<WM_OPTIONS>/lib。不同编译选项的 .so 天然隔离。
  3. codeDirName_ 为空则退化为 codeName_。
  4. codePath()(定义在 H 第 200--203 行)= codeRoot_/codeDirName_ = <case>/dynamicCode/<codeDirName>。
  5. libPath() = <case>/dynamicCode/platforms/<WM_OPTIONS>/lib/lib<codeName>.so。
  6. libRelPath() 用 codeRelPath()(dynamicCode/<codeDirName>)替换 codeRoot_。

命名的两种口径:

调用者 codeName codeDirName codePath() 库文件名
codeStream codeStream_<sha1> _<sha1> <case>/dynamicCode/_<sha1> libcodeStream_<sha1>.so
codedBase <name>_<sha1> <name> <case>/dynamicCode/<name> lib<name>_<sha1>.so

digest 文件(H 第 221--224 行):codeRoot_/codeDirName_/"Make/SHA1Digest",即 <case>/dynamicCode/<codeDirName>/Make/SHA1Digest------重编译判定就靠读它比对 SHA1(§4.6)。

4.2 clear / reset / setFilterContext:替换变量表

dynamicCode.C(第 327--384 行):

cpp 复制代码
void Foam::dynamicCode::clear()
{
    compileFiles_.clear(); copyFiles_.clear(); createFiles_.clear();
    filterVars_.clear();
    filterVars_.set("typeName", codeName_);
    filterVars_.set("SHA1sum", SHA1Digest().str());   // 全零摘要
    makeOptions_ = "EXE_INC = -g\n" "\n\nLIB_LIBS = ";
}
void Foam::dynamicCode::reset(const dynamicCodeContext& context)
{ clear(); setFilterContext(context); }
void Foam::dynamicCode::setFilterContext(const dynamicCodeContext& context)
{
    filterVars_.set("localCode", context.localCode());
    filterVars_.set("code", context.code());
    filterVars_.set("codeInclude", context.include());
    filterVars_.set("SHA1sum", context.sha1().str());
}

逐行解读:

  1. filterVars_ 是 HashTable<string>(H 第 87 行):保存"模板 ${名字} 替换成什么"的映射。
  2. clear() 清空四类文件列表与替换表,预置 typeName = codeName_(下游 prepare() 通常覆盖为短名)、SHA1sum = SHA1Digest().str()(40 个 0)。
  3. 默认 makeOptions_ = "EXE_INC = -g\n\n\nLIB_LIBS = ";-g 带调试信息(便于 gdb 调运行期代码)。
  4. reset(context) = clear() + setFilterContext(context)。
  5. setFilterContext 覆盖 localCode/code/codeInclude/SHA1sum。SHA1sum 用 str()(无下划线前缀) ------关键点:模板 .H 里 SHA1sum = "${SHA1sum}",H 钩子是 extern "C" void <typeName>_<SHA1sum>,必须与 codeName = name + sha1().str(true) 对齐,所以"命名用 str(true),替换用 str()"必须分清。

4.3 copyOrCreateFiles:模板解析 + 变量替换 + 落盘(逐行,链 A 终点)

dynamicCode.C(第 403--492 行,节选):

cpp 复制代码
bool Foam::dynamicCode::copyOrCreateFiles(const bool verbose) const
{
    if (verbose) { DetailInfo << "Creating new library in " << this->libRelPath() << endl; }

    const label nFiles = compileFiles_.size() + copyFiles_.size();
    DynamicList<fileName> resolvedFiles(nFiles), badFiles(nFiles);

    resolveTemplates(compileFiles_, resolvedFiles, badFiles);
    resolveTemplates(copyFiles_, resolvedFiles, badFiles);

    if (!badFiles.empty())
    {
        FatalErrorInFunction
            << "Could not find code template(s): " << badFiles << nl
            << "Under the $" << codeTemplateEnvName << " directory or via the <etc>/"
            << codeTemplateDirName << " expansion" << exit(FatalError);
    }

    const fileName outputDir = this->codePath();
    mkDir(outputDir);

    for (const fileName& srcFile : resolvedFiles)
    {
        const fileName dstFile(outputDir/srcFile.name());
        IFstream is(srcFile);  /* 失败则 FatalError: Failed opening ... */
        OFstream os(dstFile);  /* 失败则 FatalError: Failed writing ... */
        copyAndFilter(is, os, filterVars_);   // ← 变量替换
    }

    for (const auto& content : createFiles_)
    {
        const fileName dstFile(outputDir/stringOps::expand(content.first()));
        mkDir(dstFile.path());
        OFstream os(dstFile);
        os.writeQuoted(content.second(), false) << nl;
    }

    createMakeFiles();
    createMakeOptions();
    writeDigest(filterVars_["SHA1sum"]);
    return true;
}

逐段解读:

  1. compileFiles_(要编译的 .C 模板)与 copyFiles_(要拷贝的 .H 模板)合并统计 nFiles,分开解析 ------.C 与 .H 可来自不同目录。
  2. resolveTemplates(...) 把模板名解析成真实路径(§4.3 后半)。任一模板没找到 → badFiles 非空 → FatalError:Could not find code template(s): ... Under the $FOAM_CODE_TEMPLATES directory or via the <etc>/codeTemplates/dynamicCode expansion(第 422--428 行)。
  3. outputDir = codePath(),mkDir(outputDir) 建目录。
  4. 每个模板:目标 outputDir/srcFile.name()(只取文件名 ),copyAndFilter(is, os, filterVars_) 做替换(见下)。
  5. createFiles_(addCreateFile 直接给定内容的文件)只对文件名 stringOps::expand 一次,内容直接写盘,不经替换。
  6. createMakeFiles()(§4.4)、createMakeOptions()(§4.4)、writeDigest(filterVars_["SHA1sum"])(§4.5)。

替换引擎 copyAndFilter(第 102--136 行):

cpp 复制代码
void Foam::dynamicCode::copyAndFilter(ISstream& is, OSstream& os,
                                      const HashTable<string>& mapping)
{
    if (!is.good()) { FatalErrorInFunction << "Failed opening for reading " << is.name() ...; }
    if (!os.good()) { FatalErrorInFunction << "Failed writing " << os.name() ...; }

    // Copy file while rewriting $VARS and ${VARS}
    string line;
    do
    {
        is.getLine(line);
        // Expand according to HashTable mapping, not the environment.
        stringOps::inplaceExpand(line, mapping);
        os.writeQuoted(line, false) << nl;
    }
    while (is.good());
}

逐行解读:

  1. 前置检查读/写流;坏则 FatalError。
  2. 逐行 getLine,stringOps::inplaceExpand(line, mapping)------按 HashTable 映射展开 (只替换 filterVars_ 登记的键),不按环境变量展开(注释:"按环境变量展开会带来太多意外")。
  3. os.writeQuoted(line, false) << nl;:以"未加引号"形式写出,保证字面复制,再补换行。

这就是"模板 + 变量替换"的全部:模板里的 ${typeName}、${SHA1sum}、${code}、${TemplateType}、${FieldType}、${verbose:-false} 都由 inplaceExpand 查表替换;表里没有的键(如 ${verbose:-false} 的"带默认值"语义)按 inplaceExpand 的 ${name:-default} 规则处理。

resolveTemplates(第 139--180 行):

cpp 复制代码
bool Foam::dynamicCode::resolveTemplates(const UList<fileName>& templateNames,
                                         DynamicList<fileName>& resolvedFiles,
                                         DynamicList<fileName>& badFiles)
{
    const fileName templateDir(Foam::getEnv(codeTemplateEnvName));  // FOAM_CODE_TEMPLATES
    bool allOkay = true;
    for (const fileName& templateName : templateNames)
    {
        fileName file;
        if (!templateDir.empty() && isDir(templateDir))
        {
            file = templateDir/templateName;
            if (!isFile(file, false)) { file.clear(); }
        }
        if (file.empty()) { file = findEtcFile(codeTemplateDirName/templateName); }  // <etc>/
        if (file.empty()) { badFiles.push_back(templateName); allOkay = false; }
        else              { resolvedFiles.push_back(file); }
    }
    return allOkay;
}

逐行解读:先读 FOAM_CODE_TEMPLATES 作为候选目录(只在该目录存在时用 );对每个模板名先在该目录拼路径并 isFile 检查,不存在则 clear();仍空则 findEtcFile("codeTemplates/dynamicCode/<name>");最终仍空记入 badFiles,否则记入 resolvedFiles。

实用推论 :自制模板有两种落位------(a) 在 $FOAM_CODE_TEMPLATES/<name>(文件名不带 codeTemplates/dynamicCode 前缀,因为直接 templateDir/templateName);(b) 放 <etc>/codeTemplates/dynamicCode/<name> 让 findEtcFile 找到。很多人混淆这两者而失败。

4.4 createMakeFiles / createMakeOptions:程序自己写 Make/

dynamicCode.C(第 197--261 行,节选):

cpp 复制代码
bool Foam::dynamicCode::createMakeFiles() const
{
    if (compileFiles_.empty()) { return false; }
    const fileName dstFile(this->codePath()/"Make/files");
    mkDir(dstFile.path());
    OFstream os(dstFile);
    if (!os.good()) { FatalErrorInFunction << "Failed writing " << dstFile ...; }

    writeCommentSHA1(os);                     // /* dynamicCode: SHA1 = <sha1> */
    for (const fileName& file : compileFiles_) { os.writeQuoted(file, false) << nl; }
    os  << nl << targetLibDir << "/lib" << codeName_.c_str() << nl;   // LIB = ...
    return true;
}

bool Foam::dynamicCode::createMakeOptions() const
{
    if (compileFiles_.empty() || makeOptions_.empty()) { return false; }
    const fileName dstFile(this->codePath()/"Make/options");
    ... writeCommentSHA1(os); os.writeQuoted(makeOptions_, false) << nl;
    return true;
}

逐行解读:

  1. compileFiles_ 空则 return false。

  2. 目标 <case>/dynamicCode/<codeDirName>/Make/files。

  3. writeCommentSHA1(os)(第 183--194 行):若替换表有 SHA1sum,写 C 注释 /* dynamicCode:\n * SHA1 = <sha1>\n */------人类可读的"库对应哪份代码"标记。

  4. 逐行写出各 .C 文件名(如 codeStreamTemplate.C)。

  5. 末行:targetLibDir + "/lib" + codeName_,即

    makefile 复制代码
    LIB = $(PWD)/../platforms/$(WM_OPTIONS)/lib/libcodeStream_<sha1>

    LIB = 值不含扩展名 ,扩展名由 wmake 平台规则补(Linux 补 .so)。

  6. createMakeOptions 同构:makeOptions_ 非空才写;writeCommentSHA1 + 整段写出。分工:Make/files 声明"编什么、产什么",Make/options 声明"用什么 include 路径与链接库"。

运行期产物目录:

bash 复制代码
<case>/dynamicCode/_<sha1>/                 # codeStream:目录名=带下划线的 SHA1
├── Make/{files, options, SHA1Digest}
├── codeStreamTemplate.C                    # 模板替换生成
└── platforms/<WM_OPTIONS>/lib/libcodeStream_<sha1>.so

对 coded 对象目录名是 <name>,同目录可能堆着 lib<name>_<旧sha1>.so 与 lib<name>_<新sha1>.so------改代码后新库名变、目录不变。

4.5 wmakeLibso / writeDigest / upToDate(逐行)

wmakeLibso(第 495--514 行):

cpp 复制代码
bool Foam::dynamicCode::wmakeLibso() const
{
    stringList cmd({"wmake", "-s", "libso", this->codePath()});
    // NOTE: could also resolve wmake command explicitly:
    //   cmd[0] = stringOps::expand("$WM_PROJECT_DIR/wmake/wmake");
    OSstream& os = (Foam::infoDetailLevel > 0 ? Info : InfoErr);
    os  << "Invoking wmake libso " << this->codePath().c_str() << endl;
    if (Foam::system(cmd) == 0) { return true; }
    return false;
}

逐行解读:

  1. 命令行 {"wmake","-s","libso",codePath()}:-s silent、libso 目标是动态库、末参是编译目录(wmake 在其中找 Make/)。这是"coded = 运行期调用 wmake"的直接证据。
  2. 注释指出可显式解析 wmake 路径;默认只依赖 PATH 里的 wmake------coded 强依赖 shell 中 wmake 可用(§16 的 "could not find wmake" 根因)。
  3. infoDetailLevel > 0 走 Info,否则 InfoErr:编译耗时,即便关了详细输出也打印 Invoking wmake libso <path>。
  4. Foam::system(cmd) == 0 是"编译是否成功"的唯一判据;失败返回 false,调用方随即 FatalIOError(如 codeStream.C 第 186--191 行 Failed wmake ...)。

writeDigest(第 264--286 行,两个重载):

cpp 复制代码
bool Foam::dynamicCode::writeDigest(const SHA1Digest& sha1) const { ...; sha1.write(os, true) << nl; }
bool Foam::dynamicCode::writeDigest(const std::string& sha1) const
{ ...; os << '_'; os.writeQuoted(sha1, false) << nl; return os.good(); }

copyOrCreateFiles 调用字符串重载:先写 _ 再写 40 位摘要(因 filterVars_["SHA1sum"] 无前缀,补 _ 与 codeDirName 口径一致)。

upToDate(第 517--533 行):

cpp 复制代码
bool Foam::dynamicCode::upToDate(const SHA1Digest& sha1) const
{
    const fileName file = digestFile();
    if (!exists(file, false) || SHA1Digest(IFstream(file)()) != sha1) { return false; }
    return true;
}
bool Foam::dynamicCode::upToDate(const dynamicCodeContext& context) const
{ return upToDate(context.sha1()); }

逐行解读:digestFile() 不存在 → 不 up-to-date;存在则读内容构造 SHA1Digest 比较,不等 → 不 up-to-date;相等 → 跳过编译。第二个重载吃 context 取其 sha1()。注意 writeDigest 写了 _ 前缀,upToDate 用 SHA1Digest(IFstream(file)()) 解析------SHA1Digest 的文本解析必须容忍 _ 前缀,这是隐式格式契约。

4.6 重编译判定链(链 D 完整版)

  1. 每次 updateLibrary(name) 重新构造 dynamicCodeContext(codedBase.C 第 364 行 updateLibrary(name, dynamicCodeContext(dict)))→ setCodeContext 重算 SHA1。
  2. 用新 SHA1 构造 dynamicCode:codeName = name + "_<新sha1>",libPath() 带新 SHA1。
  3. if (libs().findLibrary(libPath)) return;(codedBase.C 第 320--323 行):新路径对应的库已加载则直接返回。
  4. 否则 loadLibrary(libPath, ...) 尝试加载已存在 的 .so(可能上次运行留下)。
  5. 加载失败 → createLibrary → if (isCompileNode && !dynCode.upToDate(context)) → 不 up-to-date 才 reset+prepare+copyOrCreateFiles+wmakeLibso。

结论 :SHA1 变 → libPath() 变 → 找不到已加载库、也加载不了旧 .so → 触发重编。"改一行就重编"不是比较文件时间,而是内容哈希生成新路径 。文件时间戳只在 NFS 的 waitForFile 里用到。

4.7 checkSecurity:安全闸门(逐行)

dynamicCode.C(第 65--97 行):

cpp 复制代码
void Foam::dynamicCode::checkSecurity(const char* title, const dictionary& dict)
{
    if (isAdministrator())
    {
        FatalIOErrorInFunction(dict)
            << "This code should not be executed by someone"
            << " with administrator rights for security reasons." << nl
            << "It generates a shared library which is loaded using dlopen"
            << nl << endl << exit(FatalIOError);
    }
    if (!allowSystemOperations)
    {
        FatalIOErrorInFunction(dict)
            << "Loading shared libraries using case-supplied code may have"
            << " been disabled" << nl << "by default for security reasons." << nl
            << "If you trust the code, you may enable this by adding" << nl << nl
            << "    allowSystemOperations 1" << nl << nl
            << "to the InfoSwitches setting in the system controlDict." << nl
            << "The system controlDict is any of" << nl << nl
            << "    ~/.OpenFOAM/" << foamVersion::api << "/controlDict" << nl
            << "    ~/.OpenFOAM/controlDict" << nl
            << "    $WM_PROJECT_DIR/etc/controlDict" << nl << endl
            << exit(FatalIOError);
    }
}

逐行解读:

  1. 第一道闸:isAdministrator()(以 root/管理员运行)→ FatalIOError。理由:运行期 dlopen 加载算例提供代码,若以管理员身份执行等于开放任意代码执行权限,风险极高。

  2. 第二道闸:!allowSystemOperations → FatalIOError,报错里直接给出开启方法 :system/controlDict 的 InfoSwitches 加 allowSystemOperations 1,并列出三个 controlDict 位置。

  3. etc/controlDict 第 74--75 行正是该键:

    ini 复制代码
    // Allow case-supplied C++ code (#codeStream, codedFixedValue)
    allowSystemOperations   1;

    官方 etc 默认已置 1,但 dynamicCode 静态默认是 0 。若未加载到含该键的 controlDict,默认禁止------这解释了很多"code may have been disabled"报错。

  4. title 参数未被使用(仅保留签名),真正的报错与 dict 绑定以带文件名与行号。

4.8 waitForFile:NFS 分布式同步(逐行)

dynamicCode.C(第 538--637 行,节选):

cpp 复制代码
void Foam::dynamicCode::waitForFile(const fileName& file, const dictionary& contextDict)
{
    if (!UPstream::parRun()) { return; }

    off_t localSize = Foam::fileSize(file);
    off_t masterSize = localSize;
    Pstream::broadcast(masterSize);

    for (label iter = 0;
         (iter < IOobject::maxFileModificationPolls && IOobject::fileModificationSkew > 0);
         ++iter)
    {
        if (localSize == masterSize) { return; }
        if (localSize > masterSize)
        { FatalIOErrorInFunction(contextDict) << "Excessive size when reading (NFS mounted) library " ...; }
        else
        { Foam::sleep(IOobject::fileModificationSkew); localSize = Foam::fileSize(file); }
    }
    // 轮询耗尽仍不一致 → 再报 FatalIOError
}

逐行解读:

  1. 非并行直接返回。
  2. 广播 master 上的文件大小给全体。
  3. 轮询条件:未超过 maxFileModificationPolls(默认 20)且 fileModificationSkew > 0(默认 5 秒)。
  4. 大小一致即返回;比 master 大 → FatalIOError(Excessive size ... detected size X whereas master size is Y bytes);比 master 小 → sleep(fileModificationSkew) 后重查。
  5. 轮询耗尽仍不一致 → 再报 FatalIOError。

对应 etc/controlDict 第 93--98 行的 fileModificationSkew 5; maxFileModificationPolls 20;。行业意义 :NFS 有写入延迟,master 刚编出的 .so 其他节点可能几秒后才见;coded 用"广播大小 + 轮询"同步,避免读到半截/缺失的库。

4.9 setFieldTemplates(逐行)

dynamicCode.H(第 281--293 行):

cpp 复制代码
template<class Type>
void setFieldTemplates()
{
    std::string val(pTraits<Type>::typeName);
    setFilterVariable("TemplateType", val);            // scalar / vector / ...
    val[0] = toupper(val[0]); val += "Field";          // ScalarField / VectorField / ...
    setFilterVariable("FieldType", val);
}

逐行解读:pTraits<Type>::typeName 取 scalar/vector/symmTensor;设为 TemplateType(模板里 DimensionedField<${TemplateType}, volMesh> 的类型参数);首字母大写加 "Field" 设为 FieldType(类名后缀与 fvPatch${FieldType})。例如 p 上编码时,生成类名是 rampedFixedValueFixedValueFvPatchScalarField。


5. 模板机制:etc/codeTemplates/dynamicCode/

模板文件 用途 使用者
codeStreamTemplate.C codeStream 自由函数 codeStream::getFunction
fixedValueFvPatchFieldTemplate.C/.H codedFixedValue codedFixedValueFvPatchField::prepare
mixedFvPatchFieldTemplate.C/.H codedMixed codedMixedFvPatchField::prepare
fixedValuePointPatchFieldTemplate.C/.H 点场 codedFixedValue codedFixedValuePointPatchField::prepare
functionObjectTemplate.C/.H codedFunctionObject codedFunctionObject::prepare
codedFvOptionTemplate.C/.H CodedSource CodedSource::prepare
codedPoints0MotionSolverTemplate.C/.H codedPoints0MotionSolver ...::prepare
codedFunction1Template.C/.H coded Function1 ...::prepare
codedPatchFunction1Template.C/.H coded PatchFunction1 ...::prepare

5.1 codeStreamTemplate.C(逐行)

etc/codeTemplates/dynamicCode/codeStreamTemplate.C:

cpp 复制代码
#include "dictionary.H"
#include "Ostream.H"
#include "Pstream.H"
#include "pointField.H"
#include "tensor.H"
#include "unitConversion.H"

//{{{ begin codeInclude
${codeInclude}
//}}} end codeInclude

namespace Foam
{
//{{{ begin localCode
${localCode}
//}}} end localCode

extern "C" void ${typeName}(Foam::Ostream& os, const Foam::dictionary& dict)
{
//{{{ begin code
    ${code}
//}}} end code
}
}

逐行解读:

  1. 顶部固定 #include 了 dictionary.H/Ostream.H/Pstream.H/pointField.H/tensor.H/unitConversion.H------方便用户代码免写 codeInclude 就能用 scalar/vector/tensor/单位换算;要更多头文件仍需 codeInclude。
  2. //{{{ begin codeInclude ... }}} end codeInclude://{{{ }}} 只是普通注释,对编译器无害;这三行是"注入区锚点",${codeInclude} 被替换为用户 #include 列表。
  3. ${localCode} 同理注入"文件作用域辅助代码"。
  4. extern "C" void ${typeName}(Foam::Ostream& os, const Foam::dictionary& dict):${typeName} 被替换为 codeName(即 codeStream_<sha1>)------导出符号名 = codeName ,与 codeStream.C 第 242 行 Foam::dlSym(lib, dynCode.codeName()) 完全一致。签名 (Ostream&, const dictionary&) 与 codeStream.H 第 125 行的 typedef void (*streamingFunctionType)(Ostream&, const dictionary&); 对应。
  5. ${code} 被替换为用户代码体,必须把结果 writeEntry 进 os。

5.2 fixedValueFvPatchFieldTemplate.C(逐行,重点看钩子)

关键段(第 29--100 行):

cpp 复制代码
#include "fixedValueFvPatchFieldTemplate.H"
#include "addToRunTimeSelectionTable.H"
#include "fvPatchFieldMapper.H"
#include "volFields.H" ... 

namespace Foam
{
extern "C" void ${typeName}_${SHA1sum}(bool load)      // 钩子!符号名 = codeName
{
    if (load) { /* after loading  */ }
    else      { /* before unloading */ }
}

makeRemovablePatchTypeField
(
    fvPatch${FieldType},
    ${typeName}FixedValueFvPatch${FieldType}
);
}

逐行解读:

  1. #include "fixedValueFvPatchFieldTemplate.H"------即被 addCopyFile 拷来的 .H。.C 与 .H 必须配对。
  2. extern "C" void ${typeName}_${SHA1sum}(bool load):替换后形如 extern "C" void rampedFixedValue_<sha1>(bool load)。typeName 被 prepare 设为短名 name_,SHA1sum 是无前缀摘要,因此符号名恰等于 codeName = name + sha1().str(true)。于是 loadLibrary(libPath, dynCode.codeName(), ...) 用 codeName 去 dlsym 时,找的就是这个钩子------这是有意为之的设计。
  3. if (load) / else:loadHook 传 true(加载后初始化),unloadHook 传 false(卸载前清理)。即 codedBase::loadLibrary 注释"Manual execution of code after loading. This is mandatory for codedBase."所指的钩子。
  4. makeRemovablePatchTypeField(fvPatch${FieldType}, ${typeName}FixedValueFvPatch${FieldType}):把动态生成的 patchField 类注册进运行期选择表 ,于是 fvPatchField<Type>::New(9.3 节的 redirectPatchField)能按 type = rampedFixedValue 找到它。这就是"用户 C++ 在运行期变成可选择类型"的落点。

updateCoeffs 段(第 159--178 行):

cpp 复制代码
void Foam::${typeName}FixedValueFvPatch${FieldType}::updateCoeffs()
{
    if (this->updated()) { return; }
    if constexpr (${verbose:-false}) { printMessage("updateCoeffs ${typeName}"); }
//{{{ begin code
    ${code}
//}}} end code
    this->parent_bctype::updateCoeffs();
}

逐行解读:

  1. 开头检查 this->updated()。
  2. if constexpr (${verbose:-false}):替换表里无 verbose 时展开为 false(编译期常量),if constexpr(false) 整块丢弃、零开销;#ifdef FULLDEBUG 时 prepare 设 verbose="true"(codedFixedValueFvPatchField.txx 第 105--109 行),于是打印 updateCoeffs rampedFixedValue sha1: ...。
  3. ${code} 注入用户代码(operator==(...) 赋值)。
  4. this->parent_bctype::updateCoeffs(); 调父类。

5.3 functionObjectTemplate.* 与 codedFvOptionTemplate.*(逐行)

functionObjectTemplate.C 关键段(第 29--131 行):

cpp 复制代码
#include "functionObjectTemplate.H"
#define namespaceFoam  // Suppress <using namespace Foam;>
#include "fvCFD.H"
#include "addToRunTimeSelectionTable.H"

namespace Foam
{
defineTypeNameAndDebug(${typeName}FunctionObject, 0);
addRemovableToRunTimeSelectionTable(functionObject, ${typeName}FunctionObject, dictionary);
extern "C" void ${typeName}_${SHA1sum}(bool load) { ... }
//{{{ begin localCode
${localCode}
//}}} end localCode
}

const Foam::fvMesh& Foam::${typeName}FunctionObject::mesh() const
{ return refCast<const fvMesh>(obr_); }

bool Foam::${typeName}FunctionObject::read(const dictionary& dict)
{
//{{{ begin code
    ${codeRead}
//}}} end code
    return true;
}
// execute/write/end 分别注入 codeExecute/codeWrite/codeEnd

逐行解读:

  1. #define namespaceFoam:定义该宏以抑制 fvCFD.H 里的 using namespace Foam; (避免与模板自身的 namespace Foam {...} 冲突/污染)。
  2. defineTypeNameAndDebug(${typeName}FunctionObject, 0) + addRemovableToRunTimeSelectionTable(functionObject, ...):类型名就是 ${typeName}(即 name_,不是 "coded" ),注册进 functionObject 选择表,供 redirectFunctionObject 里 functionObject::New(name_, ...) 构造。
  3. mesh() 用 refCast<const fvMesh>(obr_),供用户代码访问网格。
  4. read/execute/write/end 分别注入 codeRead/codeExecute/codeWrite/codeEnd。

codedFvOptionTemplate.C 关键段:

cpp 复制代码
defineTypeNameAndDebug(${typeName}FvOption${SourceType}, 0);
addRemovableToRunTimeSelectionTable(option, ${typeName}FvOption${SourceType}, dictionary);
extern "C" void ${typeName}_${SHA1sum}(bool load) { ... }
void Foam::fv::${typeName}FvOption${SourceType}::addSup(fvMatrix<${TemplateType}>& eqn, const label fieldi)
{
//{{{ begin code
    ${codeAddSup}
//}}} end code
}

逐行解读:

  1. ${SourceType} 由 CodedSource::prepare 设为 sourceType + "Source"(CodedSource.C 第 78 行),sourceType = pTraits<Type>::typeName;生成类名如 sourceTimeFvOptionscalarSource。
  2. 类型名 ${typeName} 注册到 option 选择表(fv::option::New 在 redirectOption() 用)。
  3. 四个注入点 codeCorrect/codeAddSup/codeAddSupRho/codeConstrain。缺省的 codeAddSup/codeAddSupRho 被注入 "NotImplemented"(CodedSource.C 第 89--104 行),让"用错分支"暴露。

5.4 模板变量总表

变量 由谁设置 含义
typeName clear() 默认 codeName_;prepare 覆盖为短名 生成的类名/注册名/钩子前缀
SHA1sum setFilterContext(str(),无前缀) 40 位摘要,钩子后缀、printMessage、注释
code/codeInclude/localCode setFilterContext 主代码/头/局部代码
TemplateType/FieldType setFieldTemplates<Type>() scalar/ScalarField...
SourceType CodedSource::prepare scalarSource...
codeRead/codeExecute/codeWrite/codeEnd/codeData codedFunctionObject::prepare functionObject 各钩子
codeCorrect/codeAddSup/codeAddSupRho/codeConstrain CodedSource::prepare/read fvOption 各钩子
verbose FULLDEBUG 时 "true",否则默认 false 是否打印编译期消息
codeContext 不参与替换;作为字典随对象转发 运行期上下文字典

6. dlLibraryTable:dlopen / 符号解析 / 函数指针缓存

文件:src/OpenFOAM/db/dynamicLibrary/dlLibraryTable/dlLibraryTable.{H,C}。

6.1 静态全局与成员(逐行)

dlLibraryTable.C(第 49--88 行):

cpp 复制代码
int Foam::dlLibraryTable::dlcloseOnTerminate = Foam::debug::optimisationSwitch("dlcloseOnTerminate", 0);
std::unique_ptr<Foam::dlLibraryTable> Foam::dlLibraryTable::global_(nullptr);

Foam::dlLibraryTable& Foam::dlLibraryTable::libs()
{
    if (!global_) { global_.reset(new dlLibraryTable{}); }
    return *global_;
}

逐行解读:

  1. dlcloseOnTerminate:OptimisationSwitches 开关,默认 0 (不 dlclose)。etc/controlDict 第 247--250 行注释解释:dlclose 更干净,但可能把"仍在被需要(用于终止库自身)的函数"也卸掉,故默认关。
  2. global_:静态单例,libs() 惰性构造。coded 的 libs() 多直接调 db().time().libs();codeStream 在非 baseIOdictionary 场景下用全局 libs() 或裸 dlOpen。
  3. libPtrs_(句柄列表)与 libNames_(名字列表)一一对应(H 第 75--79 行)。函数指针缓存的物理载体就是 libPtrs_ :open 成功后句柄被 append,下次 findLibrary 直接命中,避免重复 dlopen。

6.2 fullname / basename(逐行)

dlLibraryTable.C(第 60--77 行):

cpp 复制代码
Foam::word Foam::dlLibraryTable::basename(const fileName& libPath)
{ word libName(libPath.stem()); libName.removeStart("lib"); return libName; }

Foam::word Foam::dlLibraryTable::fullname(word libName)
{
    if (libName.empty()) { return libName; }
    return "lib" + libName.ext(EXT_SO);   // lib前缀 + 平台扩展名
}

逐行解读:

  1. basename:取 stem()(去路径去扩展名)再 removeStart("lib"):librampedFixedValue_abc.so → rampedFixedValue_abc。
  2. fullname:空名返回空;否则加 lib 前缀并把扩展名设为 EXT_SO。
  3. EXT_SO(第 34--40 行)按平台定义:__APPLE__→dylib,_WIN32→dll,其它→so。同一代码三平台不同扩展名;但"运行期编译 + ABI"在 Windows 上复杂得多(§14)。

6.3 open / findLibrary / close(逐行)

open(fileName, verbose)(第 441--457 行)与 openLibrary(第 167--193 行):

cpp 复制代码
void* Foam::dlLibraryTable::open(const fileName& libName, bool verbose)
{
    void* ptr = openLibrary(libName, verbose);    // 空名静默返回 nullptr
    if (ptr) { libPtrs_.push_back(ptr); libNames_.push_back(libName); }
    return ptr;
}

void* Foam::dlLibraryTable::openLibrary(const fileName& libName, bool verbose)
{
    if (libName.empty()) { return nullptr; }
    std::string msg;
    void* ptr = Foam::dlOpen(fileName(libName).expand(), msg);   // 展开 ~/$ 后 dlopen
    if (!ptr)
    {
        OSstream& os = (verbose > 0 ? WarningInFunction : InfoErr);
        os << "Could not load " << libName << nl << msg.c_str() << endl;
    }
    return ptr;
}

逐行解读:

  1. 空名静默返回 nullptr。
  2. fileName(libName).expand() 展开 ~/$,再 Foam::dlOpen(libName, msg)(带 std::string& errorMsg 的重载),失败时把 dlerror() 捕进 msg。
  3. 失败按 verbose 用 WarningInFunction 或 InfoErr 打印 Could not load <name> + 平台错误。
  4. 成功则把句柄与名字追加进两列表------"函数指针缓存"落地的一刻。

findLibrary(第 570--580 行):

cpp 复制代码
void* Foam::dlLibraryTable::findLibrary(const fileName& libName)
{
    const label index = libNames_.rfind(libName);
    if (index < 0 || libName.empty()) { return nullptr; }
    return libPtrs_[index];
}

逐行解读:rfind(从后往前,取最近一次)查名字列表,命中返回句柄。这就是"库已加载"的判定 ,codedBase::updateLibrary 与 codeStream::getFunction 都先调它。

close(fileName, verbose)(第 514--567 行)要点:

  1. rfind 找索引;index < 0 或空名返回 false。
  2. ptr == nullptr 则清名字返回 false。
  3. Foam::dlClose(ptr) 成功则清空该槽。
  4. 成功后"别名处理"(第 552--558 行):for (label idx = 0; (idx = libPtrs_.find(ptr, idx)) >= 0; ++idx) 把同一句柄的所有别名条目 都 dlClose 并清掉------因 dlopen 同一 .so 多次调用返回同一句柄,须按 manpage"dlopen 几次就 dlclose 几次"。

6.4 functionHook / loadHook / unloadHook(逐行,链 C 核心)

dlLibraryTable.C(第 91--162 行):

cpp 复制代码
bool Foam::dlLibraryTable::functionHook(const bool load, void* handle,
                                        const std::string& funcName,
                                        const bool verbose, const std::string& context)
{
    if (!handle || funcName.empty()) { return false; }
    bool ok = false;

    void* symbol = Foam::dlSymFind(handle, funcName);   // 符号解析(探测型)
    if (symbol)
    {
        try
        {
            loaderType fun = reinterpret_cast<loaderType>(symbol);   // void(*)(bool)
            if (fun) { (*fun)(load); ok = true; }
        }
        catch (...) {}
    }
    if (verbose && !ok)
    {
        auto& err = WarningInFunction << "Failed symbol lookup " << funcName.c_str() << nl;
        if (!context.empty()) { err << "from " << context.c_str() << nl; }
    }
    return ok;
}
bool Foam::dlLibraryTable::loadHook(void* h, const std::string& f, bool v, const std::string& c)
{ return functionHook(true, h, f, v, c); }
bool Foam::dlLibraryTable::unloadHook(void* h, const std::string& f, bool v, const std::string& c)
{ return functionHook(false, h, f, v, c); }

逐行解读:

  1. !handle || funcName.empty() 返回 false。
  2. void* symbol = Foam::dlSymFind(handle, funcName);:符号解析 ,dlSymFind 第三参默认 required=false,找不到返回 nullptr 而不报错。
  3. reinterpret_cast<loaderType>(symbol):把 void* 强转为 typedef void (*loaderType)(bool);(H 第 115 行)------"函数指针"的铸造点。
  4. (*fun)(load):执行;true=加载钩子,false=卸载钩子。
  5. try{...}catch(...){}:吞掉钩子抛出的任何异常,ok 保持 false。
  6. verbose && !ok 打印 Failed symbol lookup <funcName>(可跟 from <context>)。codedBase::loadLibrary 传 verbose=false,故它不在这里警告,而是自己抛 FatalIOError(§8.3)。

缓存语义 :functionHook 每次重新 dlSymFind,不单独缓存符号地址 ------缓存的是"库句柄"(libPtrs_)。因 dlsym 很快且 findLibrary 已把重复加载挡住,整体 O(1)。若要更省可另建"句柄→符号表"缓存,当前代码没做。

6.5 POSIX dlOpen / dlSymFind(逐行)

src/OSspecific/POSIX/POSIX.C 的 dlOpen(第 1806--1866 行,节选):

cpp 复制代码
void* Foam::dlOpen(const fileName& libName, const bool check)
{
    void* handle = loadLibrary(libName);
    if (!handle)
    {
        fileName libso;
        if (!libName.has_path() && !libName.starts_with("lib"))
        { libso = "lib" + libName; handle = loadLibrary(libso); }   // 补 lib 前缀
        else { libso = libName; }
        if (!handle && !libso.has_ext(EXT_SO))
        { libso.replace_ext(EXT_SO); handle = loadLibrary(libso); }  // 补扩展名
    }
    if (!handle && check) { WarningInFunction << "dlopen error : " << ::dlerror() << endl; }
    return handle;
}

逐行解读:先按原名 loadLibrary(即 ::dlopen);失败若原名无路径且不以 lib 开头 则补 lib 前缀重试;再失败且扩展名不是 so/dylib 则把扩展名换成 EXT_SO 重试;仍失败且 check=true 才 WarningInFunction << "dlopen error : " << ::dlerror()。这个"三次尝试"解释了 codeStream.C 既传完整路径又留裸 dlOpen 兜底。

dlSymFind(第 1921--1957 行):

cpp 复制代码
void* Foam::dlSymFind(void* handle, const std::string& symbol, bool required)
{
    if (!required && (!handle || symbol.empty())) { return nullptr; }
    (void) ::dlerror();                        // 先清历史错误(manpage 要求)
    void* fun = ::dlsym(handle, symbol.c_str());
    char* err = ::dlerror();
    if (err)
    {
        if (!required) { return nullptr; }
        WarningInFunction << "Cannot lookup symbol " << symbol << " : " << err << endl;
    }
    return fun;
}

逐行解读:

  1. required=false(默认,被 dlSymFound 用)且句柄空/符号空 → 返回空。
  2. (void) ::dlerror();:先清空历史错误 ------dlsym 可能返回合法 NULL 地址,必须靠 dlerror 区分"真没找到"与"找到了但地址为 0"。
  3. ::dlsym(handle, symbol.c_str()) 取地址。
  4. 再查 ::dlerror():有错误时 required 为真才警告,否则静默返回 nullptr(区分"探测型"与"必须型")。
  5. H 里三个包装(OSspecific.H 第 310--325 行):dlSymFind(...,required=false);dlSym = dlSymFind(...,true)(必须型);dlSymFound = dlSymFind(...,false)(探测型)。codeStream.C 用 dlSym(失败即 FatalIOError);functionHook 用探测型。

7. codeStream:#codeStream 的完整流程

文件:src/OpenFOAM/db/dictionary/functionEntries/codeStream/codeStream.{H,C}。

7.1 #{ ... #} verbatim:字典解析期就决定了

ISstream.C(src/OpenFOAM/db/IOstreams/Sstreams/ 第 613--660 行):

cpp 复制代码
// Verbatim string '#{ .. #}' or dictionary '#directive'
case token::HASH :
{
    char nextC;
    int lookahead = peek();
    if (lookahead == token::BEGIN_BLOCK)
    {
        // Verbatim string: #{ ... #}
        // Token stored without the surrounding delimiters
        (void) get(nextC);                       // 丢弃 '{'
        string val;
        if (readVerbatim(*this, val).bad()) { t.setBad(); }
        else { t = std::move(val); t.setType(token::tokenType::VERBATIM); }
    }
    else if (read(nextC).bad())   { t = charToWord(c); }    // 孤立 '#'
    else if (word::valid(nextC))  { putback(nextC); putback(c); ... t.setType(token::tokenType::DIRECTIVE); }
}

逐行解读:

  1. 词法器读到 #(token::HASH)时进入,区分三种情况。
  2. peek() 窥探下一字符;若是 {(token::BEGIN_BLOCK)→ verbatim 模式 :(void) get(nextC) 丢弃 {,readVerbatim(*this, val) 一直吃到 #},内容存 val。
  3. t = std::move(val); t.setType(token::tokenType::VERBATIM);:token 类型标为 VERBATIM(token.H 第 110 行)。
  4. "Token stored without the surrounding delimiters" :内容不含 #{ 与 #}。故 readEntry 拿到的是纯文本,不会把 #{/#} 当代码编译。
  5. 若 # 后不是 { 但是合法 word 字符 → 读成 DIRECTIVE(如 #include、#codeStream 本身),并 putback 让后续解析拿到完整 #...;否则当孤立 # 词。

这就是"为什么 code 段的换行不破坏字典语法" :verbatim 读法原样保留内部换行,不按字典分隔规则断句。codeStream.H(第 73--80 行)还写了两条限制:'~' symbol not allowed inside the code sections(~ 会被当 home 目录展开)、$ 也会被展开(uses string::expand)。规避:路径用 codeOptions/codeInclude 传,或避开 ~/$。

7.2 getFunction:加载/编译/找符号(逐行)

codeStream.C(第 100--255 行)。前半段:

cpp 复制代码
streamingFunctionType codeStream::getFunction(const dictionary& parentDict, const dictionary& codeDict)
{
    dynamicCodeContext context(codeDict);            // 读 code/codeInclude/codeOptions + 算 SHA1
    std::string sha1Str(context.sha1().str(true));  // "_<sha1>"
    dynamicCode dynCode("codeStream" + sha1Str, sha1Str);  // codeName=codeStream_<sha1>, codeDir=_<sha1>

    const dictionary& topDict = parentDict.topDict();
    const bool masterOnly = doingMasterOnlyReading(topDict);
    const fileName libPath = dynCode.libPath();      // SHA1 已编码在路径里

    void* lib = nullptr;
    if (isA<baseIOdictionary>(topDict)) { lib = libs(parentDict).findLibrary(libPath); }

    if (!lib)                                         // 尽量加载已有库,避免编译
    {
        DetailInfo << "Using #codeStream with " << libPath << endl;
        if (isA<baseIOdictionary>(topDict)) { lib = libs(parentDict).open(libPath, false); }  // 缓存
        else                                { lib = Foam::dlOpen(libPath, false); }           // 不缓存
    }

逐行解读:

  1. dynamicCodeContext context(codeDict); 读键并算 SHA1。
  2. sha1Str = sha1().str(true) → "_<sha1>";dynCode("codeStream"+sha1Str, sha1Str) → codeName="codeStream_<sha1>"、codeDirName="_<sha1>"。
  3. topDict = parentDict.topDict();doingMasterOnlyReading(topDict) 判断是否"仅 master 读取"(codeStream.C 第 76--97 行)。
  4. libPath = dynCode.libPath()------SHA1 已编码在路径里。
  5. 若顶层是 baseIOdictionary(有 Time)→ libs(parentDict).findLibrary(libPath) 查已加载库。
  6. 没找到则先 DetailInfo << "Using #codeStream with ",再尝试直接加载磁盘 .so:有 Time 用 libs(...).open(libPath, false)(缓存句柄 ,程序结束统一清理);无 Time 用 Foam::dlOpen(libPath, false)(不缓存,不抱怨失败)。

后半段(第 150--255 行):

cpp 复制代码
    const bool isNFS = (IOobject::fileModificationSkew > 0);
    if (lib == nullptr && (UPstream::master() || !isNFS) && !dynCode.upToDate(context))
    {
        dynCode.reset(context);
        dynCode.addCompileFile(codeTemplateC);        // "codeStreamTemplate.C"
        dynCode.setMakeOptions
        (
            "EXE_INC = -g \\\n" + context.options() +
            "\n\nLIB_LIBS = \\\n    -lOpenFOAM \\\n" + context.libs()
        );
        if (!dynCode.copyOrCreateFiles(true)) { FatalIOErrorInFunction(parentDict) << "Failed writing files for" ...; }
        if (!dynCode.wmakeLibso())            { FatalIOErrorInFunction(parentDict) << "Failed wmake " ...; }
    }

    if (!masterOnly && returnReduceOr(lib == nullptr))
    {
        if (fileHandler().distributed())
        { fileHandler().broadcastCopy(UPstream::worldComm, UPstream::master(fileHandler().comm()), libPath, libPath); }
        dynamicCode::waitForFile(libPath, context.dict());
    }
    if (!lib) { /* 再尝试 open/dlOpen 一次 */ }
    if (masterOnly ? !lib : returnReduceOr(!lib)) { FatalIOErrorInFunction(parentDict)
        << "Failed loading library " ... << "Did you add all libraries to the 'libs' entry in system/controlDict?" ...; }

    streamingFunctionType function =
        reinterpret_cast<streamingFunctionType>(Foam::dlSym(lib, dynCode.codeName()));
    if (!function) { FatalIOErrorInFunction(parentDict) << "Failed looking up symbol " << dynCode.codeName() ...; }
    return function;
}

逐行解读:

  1. isNFS = (fileModificationSkew > 0)(默认 5,即默认视为 NFS)。
  2. 编译条件三与:库未加载 + (是 master 或非 NFS)+ 不 up-to-date。NFS 下只有 master 编译;非 NFS(分布式)下各节点自编。
  3. dynCode.reset(context):设替换变量;addCompileFile(codeTemplateC):codeTemplateC = "codeStreamTemplate.C"(codeStream.H 第 151 行)。
  4. setMakeOptions(...):EXE_INC = -g + 用户 codeOptions;LIB_LIBS = -lOpenFOAM + 用户 codeLibs。codeStream 默认链 -lOpenFOAM。
  5. copyOrCreateFiles(true)/wmakeLibso() 失败分别 Failed writing files for .../Failed wmake ...。
  6. if (!masterOnly && returnReduceOr(lib == nullptr)):并非 master-only 且任一进程 仍未拿到库 → 分布式 broadcastCopy 从 master 拷库,再 waitForFile 轮询。returnReduceOr 是"全规约或"。
  7. 再尝试一次加载;仍失败则 masterOnly ? !lib : returnReduceOr(!lib) 判定后 FatalIOError:Failed loading library ... on some processors. Did you add all libraries to the 'libs' entry in system/controlDict?------提醒你可能要把相关库加进 controlDict 的 libs 入口。
  8. Foam::dlSym(lib, dynCode.codeName()):用 codeName(codeStream_<sha1>)作符号名 去 dlsym。对照 §5.1,模板导出的 extern "C" void ${typeName}(...) 中 ${typeName} 就是 codeName,能对上。失败报 Failed looking up symbol codeStream_<sha1> in library ...。
  9. reinterpret_cast<streamingFunctionType>:转成 (Ostream&, const dictionary&) 函数指针。

7.3 evaluate / execute(逐行)

codeStream.C(第 258--314 行):

cpp 复制代码
Foam::string Foam::functionEntries::codeStream::evaluate(const dictionary& parentDict, Istream& is)
{
    DetailInfo << "Using #codeStream at line " << is.lineNumber()
               << " in file " << parentDict.relativeName() << endl;
    dynamicCode::checkSecurity("functionEntries::codeStream::evaluate(..)", parentDict);

    dictionary codeDict("#codeStream", parentDict, is);    // 读 code/codeInclude/codeOptions
    OStringStream os(is.format());                          // 内存输出流,格式沿用原流
    streamingFunctionType function = getFunction(parentDict, codeDict);
    (*function)(os, parentDict);                            // 用户代码写进 os
    return os.str();
}
bool codeStream::execute(const dictionary& parentDict, primitiveEntry& entry, Istream& is)
{ IStringStream result(evaluate(parentDict, is)); entry.read(parentDict, result); return true; }
bool codeStream::execute(dictionary& parentDict, Istream& is)
{ IStringStream result(evaluate(parentDict, is)); parentDict.read(result); return true; }

逐行解读:

  1. evaluate 先打印 Using #codeStream at line <n> in file <dict>(DetailInfo)。
  2. checkSecurity(§4.7)。
  3. dictionary codeDict("#codeStream", parentDict, is);:把 is 当前位置起的内容读成子字典 ------即"读 code/codeInclude/codeOptions"的来源,字典名 #codeStream。
  4. OStringStream os(is.format());:建内存输出流。
  5. getFunction(...) 取函数指针,(*function)(os, parentDict) 执行------用户代码把结果写进 os。
  6. execute(primitiveEntry 版):把结果字符串包成 IStringStream 交给 entry.read------"字典条目由 C++ 生成"的落点 。execute(dictionary 版):同理读进子字典(用于 #codeStream 直接生成字典块)。

7.4 完整模板:codeStream

模板 1:生成 internalField

cpp 复制代码
internalField   #codeStream
{
    code
    #{
        const IOdictionary& d = static_cast<const IOdictionary&>(dict);
        const fvMesh& mesh = refCast<const fvMesh>(d.db());
        scalarField fld(mesh.nCells(), 12.34);
        fld.writeEntry("", os);
    #};

    codeInclude
    #{
        #include "fvCFD.H"
    #};

    codeOptions
    #{
        -I$(LIB_SRC)/finiteVolume/lnInclude
    #};
};
条目 必填 说明
code 是 代码体,签名由模板固定为 (Ostream& os, const dictionary& dict);必须把结果 writeEntry 进 os
codeInclude 否 额外 #include;fvCFD.H 一次带入常用有限体积头
codeOptions 否 追加到 EXE_INC 的编译选项(如额外 -I)
codeLibs 否 追加到 LIB_LIBS(codeStream 已默认 -lOpenFOAM)

模板 2:生成时间相关标量(deltaT)

cpp 复制代码
deltaT  #codeStream
{
    code
    #{
        os  << 0.001 * (1 + 0.5*sin(2*3.14159*dict.time().value()));
    #};
};

dict 是当前字典(顶层为 controlDict 时 dict.time() 可用)。任何"写进 os 的内容能被再次解析"的写法都成立。

模板 3:生成按网格坐标的 scalarField 初值

cpp 复制代码
internalField   #codeStream
{
    code
    #{
        const fvMesh& mesh = refCast<const fvMesh>(dict.db());
        const scalarField& C = mesh.C().primitiveField().component(0);
        scalarField fld(C.size());
        forAll(fld, i) { fld[i] = 1.0 + 0.1*Foam::sin(C[i]); }
        fld.writeEntry("", os);
    #};
    codeInclude { #include "fvCFD.H" #};
    codeOptions { -I$(LIB_SRC)/finiteVolume/lnInclude #};
};

7.5 codeStream 的缓存与落位

  • 缓存键 :codeName = codeStream_<sha1>,库文件 libcodeStream_<sha1>.so。
  • 落位 :<case>/dynamicCode/_<sha1>/platforms/<WM_OPTIONS>/lib/。
  • 命中判定 :先 findLibrary(内存缓存),再 dlOpen 尝试磁盘库,最后才编译。
  • 复用边界 :库路径按 FOAM_CASE(envGlobalPath())解析,不会自动跨 case 找库;同一段代码只是算出同一 SHA1,若库被拷进本 case 才可直接加载。

8. codedBase:coded 对象的公共基类

文件:src/OpenFOAM/db/dynamicLibrary/codedBase/codedBase.{H,C}。

8.1 类结构与纯虚接口(逐行)

codedBase.H(第 67--170 行,节选):

cpp 复制代码
class codedBase
{
    dynamicCodeContext context_;       //- The code context
    mutable fileName oldLibPath_;      //- Previously loaded library

    void* loadLibrary(const fileName&, const std::string&, const dynamicCodeContext&) const;
    void  unloadLibrary(const fileName&, const std::string&, const dynamicCodeContext&) const;
    void  createLibrary(dynamicCode&, const dynamicCodeContext&) const;

  protected:
    static void writeCodeDict(Ostream&, const dictionary&);
    static const dictionary& codeDict(const objectRegistry&, const word& dictName = "codeDict");
    dynamicCodeContext& codeContext() { return context_; }
    void setCodeContext(const dictionary&);
    void append(const std::string&);
    void updateLibrary(const word& name, const dynamicCodeContext& context) const;
    void updateLibrary(const word& name, const dictionary& dict) const;
    void updateLibrary(const word& name) const;

    virtual dlLibraryTable& libs() const = 0;
    virtual string description() const = 0;
    virtual void clearRedirect() const = 0;
    virtual const dictionary& codeDict() const = 0;
    virtual void prepare(dynamicCode& dynCode, const dynamicCodeContext& context) const = 0;

  public:
    ClassName("codedBase");
    codedBase() = default;
    virtual ~codedBase() = default;
};

逐行解读:

  1. context_:对象私有 的 dynamicCodeContext。简单对象(codedFixedValue)它就是主 code 字典的上下文;复杂对象(codedFunctionObject)先 setCodeContext(dict) 再 append() 补片段进哈希。
  2. oldLibPath_ 标 mutable:updateLibrary 是 const 成员却要更新"上次加载库路径"(典型缓存手法)。
  3. 五个纯虚接口:libs()(哪张 dlLibraryTable,通常 Time::libs())、description()(打印)、clearRedirect()(清重定向对象以便重载时重建)、codeDict()(从哪读代码字典)、prepare()(把片段注入 dynCode)。

8.2 updateLibrary:热更新主循环(逐行)

codedBase.C 主重载(第 296--355 行):

cpp 复制代码
void Foam::codedBase::updateLibrary(const word& name, const dynamicCodeContext& context) const
{
    dynamicCode::checkSecurity("codedBase::updateLibrary()", context.dict());

    // codeName: name + _<sha1>;  codeDir : name
    dynamicCode dynCode(name + context.sha1().str(true), name);
    const fileName libPath = dynCode.libPath();

    if (libs().findLibrary(libPath)) { return; }     // 该库已加载 => 结束

    DetailInfo << "Using dynamicCode for " << this->description().c_str()
               << " at line " << context.dict().startLineNumber()
               << " in " << context.dict().name() << endl;

    this->clearRedirect();                            // 清掉库提供的对象实例
    unloadLibrary(oldLibPath_, dlLibraryTable::basename(oldLibPath_), context);  // 可能卸载旧库
    void* lib = loadLibrary(libPath, dynCode.codeName(), context);  // 尽量加载已有库

    if (returnReduceOr(lib == nullptr))
    {
        if (lib) { unloadLibrary(libPath, dlLibraryTable::basename(libPath), context); }  // 保证一致
        createLibrary(dynCode, context);              // 真正编译
        loadLibrary(libPath, dynCode.codeName(), context);
    }
    oldLibPath_ = libPath;                            // 留存备用
}

逐行解读:

  1. checkSecurity(..., context.dict()):安全闸门,报错绑定"代码所在字典"。
  2. dynamicCode(name + sha1().str(true), name) → codeName = "<name>_<sha1>"、codeDirName = "<name>"------"目录固定、库名带 SHA1"的来源。
  3. if (libs().findLibrary(libPath)) return;:最快短路,同一 SHA1 的库已加载则直接结束(同步内被多次调用不重复做事)。
  4. DetailInfo 打印 Using dynamicCode for <description> at line <n> in <dict>(description() 子类给,如 "patch inlet on field U")。
  5. this->clearRedirect();:先清掉重定向对象(旧库提供的实例),因为可能要换新库。
  6. unloadLibrary(oldLibPath_, basename(oldLibPath_), context):basename 把路径转库名(lib<name>_<sha1>.so → <name>_<sha1>,恰等于卸载钩子符号名);首次调用 oldLibPath_ 为空,open 返回空即直接返回。
  7. loadLibrary(libPath, codeName, context):加载磁盘上可能早先编好的库。
  8. if (returnReduceOr(lib == nullptr)):任一进程 没加载成功就进编译分支;若本进程恰好成功而别的进程没有,先 unloadLibrary(libPath, ...) 保证一致,再 createLibrary。
  9. createLibrary(dynCode, context):真正编译(§8.3),随后再次 loadLibrary 加载新库。
  10. oldLibPath_ = libPath;:记录供下次判断是否卸载。

另两个重载(第 358--378 行):

cpp 复制代码
void Foam::codedBase::updateLibrary(const word& name, const dictionary& dict) const
{ updateLibrary(name, dynamicCodeContext(dict)); }

void Foam::codedBase::updateLibrary(const word& name) const
{
    if (context_.good()) { updateLibrary(name, context_); }
    else                 { updateLibrary(name, dynamicCodeContext(this->codeDict())); }
}

逐行解读:第一个从字典现场构造 context 再转发;第二个(最常用 )若 context_ 有效(已被 setCodeContext/append 建立)就用它,否则退回 codeDict() 现场构造。这解释了复杂对象为什么必须靠 append 把额外片段纳入哈希 ------它们走 context_ 这条路。

8.3 loadLibrary / unloadLibrary / createLibrary(逐行)

loadLibrary(第 132--172 行):

cpp 复制代码
void* Foam::codedBase::loadLibrary(const fileName& libPath, const std::string& funcName,
                                   const dynamicCodeContext& context) const
{
    void* handle = libs().open(libPath, false);       // 尽量加载已有库
    if (!handle) { return handle; }

    // Manual execution of code after loading. This is mandatory for codedBase.
    const bool ok = libs().loadHook(handle, funcName, false);

    if (!ok)
    {
        FatalIOErrorInFunction(context.dict())
            << "Failed symbol lookup " << funcName.c_str() << nl
            << "from " << libPath << nl << exit(FatalIOError);
        handle = nullptr;
        if (!libs().close(libPath, false)) { FatalIOErrorInFunction(context.dict()) << ...; }
    }
    return handle;
}

逐行解读:

  1. libs().open(libPath, false):加载库;false = 不啰嗦(失败静默返回 nullptr,由调用方处理)。
  2. libs().loadHook(handle, funcName, false):加载后必须执行钩子 ;funcName = codeName。
  3. 钩子查找失败 → FatalIOError:Failed symbol lookup <funcName> from <libPath>(§16 的报错之一);随后 handle=nullptr 并 close 卸载。

与 codeStream 对比:codeStream 用 dlSym(必须型)直接找自由函数;codedBase 用 loadHook(探测型 dlSymFind)找钩子。二者符号名都用 codeName。但 codeStream 的库没有钩子,它导出的是主函数本身;coded 家族的库既有钩子又有真正实现类。

createLibrary(第 210--279 行):

cpp 复制代码
void Foam::codedBase::createLibrary(dynamicCode& dynCode, const dynamicCodeContext& context) const
{
    const bool isNFS = (IOobject::fileModificationSkew > 0);

    bool isCompileNode = UPstream::master(UPstream::worldComm);   // 默认:world master 编译
    if (fileHandler().distributed()) { /* 分布式:仅 world master 编译 */ }
    else if (!isNFS) { isCompileNode = UPstream::master(UPstream::commLocalNode()); }

    if (isCompileNode && !dynCode.upToDate(context))
    {
        dynCode.reset(context);
        this->prepare(dynCode, context);              // 子类注入片段与选项
        if (!dynCode.copyOrCreateFiles(true)) { FatalIOErrorInFunction(context.dict()) << "Failed writing files for" ...; }
        if (!dynCode.wmakeLibso())            { FatalIOErrorInFunction(context.dict()) << "Failed wmake " ...; }
    }

    UPstream::barrier(UPstream::worldComm);           // 等编译完成再做文件轮询

    const fileName libPath = dynCode.libPath();
    if (fileHandler().distributed())
    { fileHandler().broadcastCopy(UPstream::worldComm, UPstream::master(fileHandler().comm()), libPath, libPath); }
    dynamicCode::waitForFile(libPath, context.dict());
}

逐行解读:

  1. isNFS 同前。
  2. isCompileNode 逻辑:默认 worldComm 的 master;分布式文件系统仅 world master 编译 (之后广播);非分布式且非 NFS (本地单节点)用 commLocalNode() 的 master(同节点进程共享本地文件系统,只需一个编译)。
  3. if (isCompileNode && !dynCode.upToDate(context)):编译节点 + digest 不 up-to-date 才编。
  4. dynCode.reset(context); this->prepare(dynCode, context);:重置变量、子类注入片段与选项------codedBase 与子类的分工。
  5. copyOrCreateFiles(true)(true = verbose,打印 Creating new library in <relpath>);失败报 Failed writing files for <libRelPath>。
  6. wmakeLibso():失败报 Failed wmake <libRelPath>。
  7. UPstream::barrier(worldComm):编译完先 barrier,避免"master 还没编完,别人就开始轮询空文件"。
  8. 分布式时 broadcastCopy 把库从 master 拷到各节点。
  9. waitForFile:NFS 轮询(§4.8)。

unloadLibrary(第 175--207 行):

cpp 复制代码
void Foam::codedBase::unloadLibrary(const fileName& libPath, const std::string& funcName,
                                    const dynamicCodeContext& context) const
{
    void* handle = libs().open(libPath, false);
    if (!handle) { return; }                          // 首次调用 oldLibPath_ 为空

    const bool ok = libs().unloadHook(handle, funcName, false);   // 执行卸载钩子
    if (!ok)
    {
        IOWarningInFunction(context.dict())          // 只是警告,不致命
            << "Failed looking up symbol " << funcName << nl << "from " << libPath << nl;
    }
    if (!libs().close(libPath, false))
    { FatalIOErrorInFunction(context.dict()) << "Failed unloading library " << libPath << nl << exit(FatalIOError); }
}

逐行解读:路径为空(首次)→ open 空 → 直接返回;unloadHook 执行 (*fun)(false);钩子找不到只 IOWarning(有些库没有卸载钩子);close 失败才 FatalIOError。

8.4 writeCodeDict / codeDict(逐行)

codedBase.C(第 88--127 行):

cpp 复制代码
void Foam::codedBase::writeCodeDict(Ostream& os, const dictionary& dict)
{
    writeEntryIfPresent(os, dict, "codeContext");
    writeEntryIfPresent(os, dict, "codeInclude");
    writeEntryIfPresent(os, dict, "localCode");
    writeEntryIfPresent(os, dict, "code");
    writeEntryIfPresent(os, dict, "codeOptions");
    writeEntryIfPresent(os, dict, "codeLibs");
}

const Foam::dictionary& Foam::codedBase::codeDict(const objectRegistry& obr, const word& dictName)
{
    auto* dictptr = obr.getObjectPtr<IOdictionary>(dictName);
    if (!dictptr)
    {
        dictptr = new IOdictionary
        (
            IOobject(dictName, obr.time().system(), obr,
                     IOobject::READ_MODIFIED, IOobject::NO_WRITE, IOobject::REGISTER)
        );
        obr.store(dictptr);
    }
    return *dictptr;
}

逐行解读:

  1. writeCodeDict 把六个代码键原样写回(不存在则静默跳过)。用途:codedFixedValue::write 写边界时把代码一起写出,让 foamToVTK/postProcess 重读时仍能重建。
  2. codeDict(obr, dictName="codeDict"):"内联 code 或缺省从 system/codeDict 读" 的基础。先在 obr 找名为 codeDict 的 IOdictionary;没有则以 READ_MODIFIED(运行期可修改)+ NO_WRITE + REGISTER 从磁盘读 system/codeDict 并 store 进注册表缓存。
  3. READ_MODIFIED 是热更新的关键 :system/codeDict 运行中被改,下次读取会感知(配合 Time 的文件时间/inotify 检测)。
  4. 字典名默认 "codeDict",位于 obr.time().system() 即 <case>/system/codeDict。

8.5 钩子符号名与 codeName 的一致性(再次强调)

场景 库内导出符号 dlsym 查找用名
codeStream extern "C" void codeStream_<sha1>(Ostream&, const dictionary&) dynCode.codeName() = codeStream_<sha1>
coded 家族(钩子) extern "C" void <name>_<sha1>(bool) dynCode.codeName() = <name>_<sha1>

两者能对上,全靠 prepare() 把 typeName 设为 name_(短名),且 SHA1sum 用无前缀 str() 。自定义模板务必维持这个等式,否则会 Failed symbol lookup ...。


9. codedFixedValue:从字典到 patchField

文件:src/finiteVolume/fields/fvPatchFields/derived/codedFixedValue/codedFixedValueFvPatchField.{H,txx}。

9.1 类结构与 codeDict/codeContext(逐行)

codedFixedValueFvPatchField.H(第 123--171 行):

cpp 复制代码
template<class Type>
class codedFixedValueFvPatchField
:
    public fixedValueFvPatchField<Type>,   // 最终要成为一个 patchField
    protected codedBase                    // 拿到加载/编译机制(接口不外泄)
{
    typedef codedFixedValueFvPatchField<Type> this_bctype;
    typedef fixedValueFvPatchField<Type> parent_bctype;

    dictionary dict_;                                       //- 拷贝的代码字典
    const word name_;                                       //- 生成的 BC 短名
    mutable autoPtr<fvPatchField<Type>> redirectPatchFieldPtr_;  //- 真正的 BC

    virtual dlLibraryTable& libs() const;
    virtual string description() const;
    virtual void clearRedirect() const;
    virtual const dictionary& codeContext() const;
    virtual const dictionary& codeDict() const;
    virtual void prepare(dynamicCode&, const dynamicCodeContext&) const;

  public:
    static constexpr const char* const codeTemplateC = "fixedValueFvPatchFieldTemplate.C";
    static constexpr const char* const codeTemplateH = "fixedValueFvPatchFieldTemplate.H";
    TypeName("codedFixedValue");   // C++ 侧运行时类型名,区别于生成 BC 类型名 <name>

逐行解读:

  1. 双继承 :public fixedValueFvPatchField<Type>(最终是 patchField)+ protected codedBase(加载/编译机制)。
  2. dict_:去掉 heavy 数据后的代码字典;name_:用户从 name 键给的短名,决定生成库目录名与钩子前缀。
  3. redirectPatchFieldPtr_:指向动态生成的 BC 实例;mutable 使其能在 const 的 redirectPatchField() 里惰性构造。
  4. TypeName("codedFixedValue"):用户在字典里写 type codedFixedValue------区别于生成 BC 的类型名 <name>。

codeContext()/codeDict()(.txx 第 63--83 行):

cpp 复制代码
template<class Type>
const Foam::dictionary& codedFixedValueFvPatchField<Type>::codeContext() const
{
    const dictionary* ptr = dict_.findDict("codeContext", keyType::LITERAL);
    return (ptr ? *ptr : dictionary::null);
}

template<class Type>
const Foam::dictionary& codedFixedValueFvPatchField<Type>::codeDict() const
{
    // Inline "code" or from system/codeDict
    return (dict_.found("code") ? dict_ : codedBase::codeDict(this->db()).subDict(name_));
}

逐行解读:

  1. codeContext():有 codeContext 子字典就返回它,否则 dictionary::null。它会被转发给生成对象(§9.3),让用户代码能读到那段上下文。
  2. codeDict():"内联 code 就用本地字典;否则去 system/codeDict 的名字对应子字典取" 。这是 codedFixedValue.H 文档里"special form"的实现:不写 code,改在 system/codeDict 写同名条目,从而运行期热改代码而不动边界文件。

9.2 构造函数:dictionaryContent::copyDict(逐行)

.txx(第 159--197 行):

cpp 复制代码
template<class Type>
Foam::codedFixedValueFvPatchField<Type>::codedFixedValueFvPatchField
(
    const fvPatch& p, const DimensionedField<Type, volMesh>& iF, const dictionary& dict
)
:
    parent_bctype(p, iF, dict, IOobjectOption::NO_READ),   // 'value' 可选
    codedBase(),
    dict_
    (
        dictionaryContent::copyDict
        (
            dict,
            wordList(),                    // allow
            wordList({"type", "value"})    // deny: type 冗余、value 是大块数据
        )
    ),
    name_(dict.getCompat<word>("name", {{"redirectType", 1706}})),
    redirectPatchFieldPtr_(nullptr)
{
    updateLibrary(name_);              // 构造即编译/加载

    if (!this->readValueEntry(dict))
    {
        this->extrapolateInternal();   // 用内部场外插初值
        this->evaluate(Pstream::commsTypes::buffered);   // 跑一次代码赋值
    }
}

逐行解读:

  1. parent_bctype(p, iF, dict, IOobjectOption::NO_READ):value 可选,先不读。
  2. dict_ = dictionaryContent::copyDict(dict, /*allow=*/wordList(), /*deny=*/{"type","value"}):拷贝字典但剔除 type(冗余)与 value(大块数据) ,保留 code/codeInclude/name/codeOptions/codeContext 等以便生成对象重建。copyDict 定义在 dictionaryContent.H 第 96--101 行。
  3. name_(dict.getCompat<word>("name", {{"redirectType", 1706}})):读 name,同时接受旧键名 redirectType(v1706 兼容)。
  4. updateLibrary(name_):构造时立刻编译/加载(首次使用即触发运行期编译)。
  5. if (!this->readValueEntry(dict)):无 value 时 extrapolateInternal() 外插初值再 evaluate(...) 跑一次代码;有 value 则直接用。

9.3 redirectPatchField:构造真正的 BC(逐行)

.txx(第 217--260 行):

cpp 复制代码
template<class Type>
const Foam::fvPatchField<Type>&
Foam::codedFixedValueFvPatchField<Type>::redirectPatchField() const
{
    if (!redirectPatchFieldPtr_)
    {
        OCharStream os;
        this->writeValueEntry(os);                 // 把当前 value 写进内存流
        ISpanStream is(os.view());
        dictionary constructDict(is);
        constructDict.set("type", name_);          // type 改成 <name>

        redirectPatchFieldPtr_.reset
        (
            fvPatchField<Type>::New(this->patch(), this->internalField(), constructDict).ptr()
        );

        auto* contentPtr = dynamic_cast<dictionaryContent*>(redirectPatchFieldPtr_.get());
        if (contentPtr) { contentPtr->dict(this->codeContext()); }  // 转发 codeContext
        else { WarningInFunction << name_ << " Did not derive from dictionaryContent" << nl << nl; }
    }
    return *redirectPatchFieldPtr_;
}

逐行解读:

  1. if (!redirectPatchFieldPtr_):惰性构造,只造一次(除非 clearRedirect 后再次进入)。
  2. OCharStream os; this->writeValueEntry(os);:先把当前 value 写进内存流,再用 ISpanStream/dictionary 读回------构造生成对象时能带上"当前最新的 value"作初值。
  3. constructDict.set("type", name_);:把 type 设为 name_ 。于是 fvPatchField::New 去找运行时类型名 name_(如 rampedFixedValue)------正是动态库里 makeRemovablePatchTypeField 注册的生成类。这就是"运行期把 C++ 变成可选择类型"的闭环。
  4. dynamic_cast<dictionaryContent*> 成功后 contentPtr->dict(this->codeContext()):把用户的 codeContext 注入生成对象的 dictionaryContent (模板 .H 里 codeContext() 返回 dictionaryContent::dict())。
  5. 若生成类没继承 dictionaryContent → WarningInFunction << name_ << " Did not derive from dictionaryContent"。自定义模板忘继承 dictionaryContent 时常见此警告。

9.4 updateCoeffs / evaluate(逐行)

.txx(第 263--299 行):

cpp 复制代码
template<class Type>
void Foam::codedFixedValueFvPatchField<Type>::updateCoeffs()
{
    if (this->updated()) { return; }
    updateLibrary(name_);                 // 每步都检查是否重编(热更新触发点)
    const fvPatchField<Type>& fvp = redirectPatchField();
    const_cast<fvPatchField<Type>&>(fvp).updateCoeffs();   // 执行用户代码 operator==
    this->operator==(fvp);                // 把真正对象算出的值拷贝回本对象
    this->parent_bctype::updateCoeffs();
}

template<class Type>
void Foam::codedFixedValueFvPatchField<Type>::evaluate(const Pstream::commsTypes commsType)
{
    updateLibrary(name_);
    const fvPatchField<Type>& fvp = redirectPatchField();
    const_cast<fvPatchField<Type>&>(fvp).evaluate(commsType);
    this->parent_bctype::evaluate(commsType);
}

逐行解读:

  1. if (this->updated()) return;:本步已更新则跳过。
  2. updateLibrary(name_):每步都调用 ------热更新触发点:若 system/codeDict(或内联 code)这步被改,SHA1 变、库重编重载。
  3. const_cast<...>(fvp).updateCoeffs():真正对象是 const 引用但 updateCoeffs 非 const,去 const 调用------用户代码(operator==(...))就在这一步执行。
  4. this->operator==(fvp):把真正对象算出的值拷贝回外层 patchField。
  5. parent_bctype::updateCoeffs() 收尾。

9.5 完整模板:codedFixedValue(含参数表)

模板 4:codedFixedValue(速度入口随时间变化)

cpp 复制代码
inlet
{
    type            codedFixedValue;
    value           uniform (0 0 0);   // 可选:初值

    name            rampedInlet;       // 生成 BC 的名字(决定库目录名/钩子前缀)

    code
    #{
        // this 指向生成的 patchField;db() 返回 objectRegistry
        const scalar t = this->db().time().value();
        operator==(vector(1, 0, 0) * min(10.0, 0.1*t));
    #};

    codeInclude                       // 可选
    #{
        #include "fvCFD.H"
    #};

    codeOptions                       // 可选
    #{
        -I$(LIB_SRC)/finiteVolume/lnInclude
    #};

    codeContext                       // 可选:转发给生成对象的上下文字典
    {
        amplitude   1.0;
        tRamp       100;
    }
}
条目 必填 说明
type 是 固定 codedFixedValue
value 否 初值;不给则用代码 evaluate 结果
name 是 生成 BC 类型名(旧键 redirectType);决定 <case>/dynamicCode/<name>/ 与 lib<name>_<sha1>.so
code 条件必填 无内联 code 时从 system/codeDict 同名子字典取
codeInclude 否 额外头文件
codeOptions 否 追加 EXE_INC
codeLibs 否 追加 LIB_LIBS(模板已默认 -lOpenFOAM -lfiniteVolume -lmeshTools)
localCode 否 文件作用域静态辅助函数
codeContext 否 转发给生成对象的上下文字典

code 段里可用 this(生成对象)、this->db()(objectRegistry)、operator==(...)。


10. codedMixed:与 codedFixedValue 的差异

文件:src/finiteVolume/fields/fvPatchFields/derived/codedMixed/codedMixedFvPatchField.{H,txx}。

10.1 结构差异(逐行)

  • 父类换成 mixedFvPatchField<Type>(codedMixedFvPatchField.H 第 34--35 行)。

  • 构造函数 deny 列表更长(.txx 第 159--187 行):

    cpp 复制代码
    dictionaryContent::copyDict(dict, wordList(),
        wordList({"type", "value", "refValue", "refGradient", "valueFraction"}))

    mixed BC 有四个"大块"数据(value/refValue/refGradient/valueFraction),都不该拷进生成对象的代码字典。

  • redirectPatchField 用 dynamic_cast<parent_bctype*>(即 mixedFvPatchField)而非泛型 fvPatchField(.txx 第 225--236 行):

    cpp 复制代码
    redirectPatchFieldPtr_.reset(dynamic_cast<parent_bctype*>(fvPatchField<Type>::New(...).ptr()));

    先构造再 dynamic_cast,因为后续要用它的 refValue()/refGrad()/valueFraction()。

10.2 updateCoeffs:拷贝三个系数(逐行)

.txx(第 257--277 行):

cpp 复制代码
template<class Type>
void Foam::codedMixedFvPatchField<Type>::updateCoeffs()
{
    if (this->updated()) { return; }
    updateLibrary(name_);
    const parent_bctype& fvp = redirectPatchField();
    const_cast<parent_bctype&>(fvp).updateCoeffs();   // 执行用户 code:写 refValue/refGrad/valueFraction
    this->refValue()      = fvp.refValue();           // 把三个系数拷回外层
    this->refGrad()       = fvp.refGrad();
    this->valueFraction() = fvp.valueFraction();
    this->parent_bctype::updateCoeffs();
}

逐行解读:updateLibrary(name_) 同前;const_cast<...>(fvp).updateCoeffs() 执行生成对象里用户的 code(用户写 this->refValue()=...; this->refGrad()=Zero; this->valueFraction()=1.0;);再把三个系数从生成对象拷回外层;父类收尾。

10.3 完整模板:codedMixed

cpp 复制代码
outlet
{
    type            codedMixed;
    refValue        uniform (0 0 0);   // mixed 的三个系数(初值)
    refGradient     uniform (0 0 0);
    valueFraction   uniform 1;
    name            rampedMixed;

    code
    #{
        this->refValue() = vector(1, 0, 0) * min(10.0, 0.1*this->db().time().value());
        this->refGrad()  = Zero;
        this->valueFraction() = 1.0;
    #};
}

10.4 codedFixedValue vs codedMixed 对比

维度 codedFixedValue codedMixed
基类 fixedValueFvPatchField mixedFvPatchField
用户代码操作 operator==(value) refValue()/refGrad()/valueFraction()
初值条目 value(可选) refValue/refGradient/valueFraction
copyDict deny type, value 再加 refValue/refGradient/valueFraction
回拷内容 整个值 operator==(fvp) 三个系数分别拷
适用 直接给定边界值(Dirichlet) 权重混合/滑移/推导式边界

11. codedFunctionObject

文件:src/functionObjects/utilities/codedFunctionObject/codedFunctionObject.{H,C};类型名 "coded"(H 第 175 行),属库 utilityFunctionObjects。

11.1 prepare:多片段注入(逐行)

codedFunctionObject.C(第 90--129 行):

cpp 复制代码
void Foam::functionObjects::codedFunctionObject::prepare(dynamicCode& dynCode,
                                                         const dynamicCodeContext& context) const
{
    dynCode.setFilterVariable("typeName", name_);
    dynCode.setFilterVariable("codeData", codeData_);
    dynCode.setFilterVariable("codeRead", codeRead_);
    dynCode.setFilterVariable("codeExecute", codeExecute_);
    dynCode.setFilterVariable("codeWrite", codeWrite_);
    dynCode.setFilterVariable("codeEnd", codeEnd_);

    dynCode.addCompileFile(codeTemplateC);
    dynCode.addCopyFile(codeTemplateH);

    #ifdef FULLDEBUG
    dynCode.setFilterVariable("verbose", "true");
    #endif

    dynCode.setMakeOptions
    (
        "EXE_INC = -g \\\n-I$(LIB_SRC)/finiteVolume/lnInclude \\\n-I$(LIB_SRC)/meshTools/lnInclude \\\n"
      + context.options() + "\n\nLIB_LIBS = \\\n    -lOpenFOAM \\\n    -lfiniteVolume \\\n    -lmeshTools \\\n"
      + context.libs()
    );
}

逐行解读:

  1. setFilterVariable("typeName", name_):关键 ------模板里 <typeName>FunctionObject 的注册名与钩子前缀都变成 name_。
  2. 设 codeData/codeRead/codeExecute/codeWrite/codeEnd 五个替换变量,对应模板 .H 私有数据区与 .C 的 read/execute/write/end 注入点。
  3. addCompileFile(codeTemplateC) + addCopyFile(codeTemplateH):注意 functionObject 的 .H 里也有 codeInclude 注入(functionObjectTemplate.H 第 41--43 行),故头模板也参与替换。
  4. setMakeOptions:默认链 -lOpenFOAM -lfiniteVolume -lmeshTools。

11.2 read:多片段与哈希(逐行)

codedFunctionObject.C(第 210--238 行):

cpp 复制代码
bool Foam::functionObjects::codedFunctionObject::read(const dictionary& dict)
{
    timeFunctionObject::read(dict);
    codedBase::setCodeContext(dict);                    // 用整个 dict 建 context
    dict.readCompat<word>("name", {{"redirectType", 1706}}, name_);

    auto& ctx = codedBase::codeContext();
    int nKeywords = 0;                                  // 五个都读,不短路
    nKeywords += ctx.readIfPresent("codeData", codeData_);
    nKeywords += ctx.readIfPresent("codeRead", codeRead_);
    nKeywords += ctx.readIfPresent("codeExecute", codeExecute_);
    nKeywords += ctx.readIfPresent("codeWrite", codeWrite_);
    nKeywords += ctx.readIfPresent("codeEnd", codeEnd_);

    if (!nKeywords)
    { IOWarningInFunction(dict) << "No critical \"code\" prefixed keywords found." << nl
        << "Please check the code documentation for more details." << nl << endl; }

    updateLibrary(name_);
    return redirectFunctionObject().read(dict);
}

逐行解读:

  1. codedBase::setCodeContext(dict):用整个 dict 建 context,读 code/codeInclude/localCode/codeOptions/codeLibs 全部纳入 SHA1。
  2. dict.readCompat<word>("name", {{"redirectType", 1706}}, name_):读 name。
  3. auto& ctx = codedBase::codeContext();:取可变引用。
  4. ctx.readIfPresent(...) 五个都读、不用 || 短路 (注释 "no short-circuiting")------每个存在的片段都 append 进 SHA1。这是"改了 codeExecute 也能重编"的保证。
  5. 一个代码键都没有 → IOWarning:No critical "code" prefixed keywords found.
  6. updateLibrary(name_) + redirectFunctionObject().read(dict)。

与简单 coded 的差别 :简单对象的额外片段(如 codeContext)不进哈希无妨;而 codedFunctionObject 的 codeExecute 等本身就是"代码",必须逐个进 SHA1,否则改了不重编。这就是 codedBase.H "must also register these elements ... append()" 的由来。

11.3 redirectFunctionObject / execute/write/end(逐行)

codedFunctionObject.C(第 154--207 行):

cpp 复制代码
Foam::functionObject& Foam::functionObjects::codedFunctionObject::redirectFunctionObject() const
{
    if (!redirectFunctionObjectPtr_)
    {
        dictionary constructDict(dict_);
        constructDict.set("type", name_);            // type = <name>
        redirectFunctionObjectPtr_ = functionObject::New(name_, time_, constructDict);

        auto* contentPtr = dynamic_cast<dictionaryContent*>(redirectFunctionObjectPtr_.get());
        if (contentPtr) { contentPtr->dict(this->codeContext()); }
        else { WarningInFunction << name_ << " Did not derive from dictionaryContent" << nl << nl; }
    }
    return *redirectFunctionObjectPtr_;
}
bool Foam::functionObjects::codedFunctionObject::execute() { updateLibrary(name_); return redirectFunctionObject().execute(); }
bool Foam::functionObjects::codedFunctionObject::write()   { updateLibrary(name_); return redirectFunctionObject().write(); }
bool Foam::functionObjects::codedFunctionObject::end()     { updateLibrary(name_); return redirectFunctionObject().end(); }

逐行解读:

  1. constructDict.set("type", name_):类型设为 name_ → functionObject::New 按运行时类型名 name_ 找生成类。
  2. functionObject::New(name_, time_, constructDict):第一参是实例名 name_,第二参 Time。
  3. 回填 codeContext(同 §9.3)。
  4. execute/write/end 三处都先 updateLibrary(name_)------每一步都可能触发重编,codedFunctionObject 是全家族最活跃的热更新点。

11.4 完整模板:codedFunctionObject(含参数表)

模板 5:coded functionObject 写出 mag(U) 场

cpp 复制代码
functions
{
    writeMagU
    {
        type            coded;
        libs            (utilityFunctionObjects);

        name            writeMagU;   // 生成 functionObject 的名字

        codeData                      // 可选:局部成员数据(默认构造)
        #{
        #};

        codeWrite                     // 写出时执行
        #{
            const volVectorField& U = mesh().lookupObject<volVectorField>("U");
            mag(U)().write();
        #};

        codeInclude                   // 可选
        #{
            #include "fvCFD.H"
        #};

        codeOptions                   // 可选
        #{
            -I$(LIB_SRC)/finiteVolume/lnInclude
        #};
    }
}
条目 必填 说明
type 是 coded
libs 是 utilityFunctionObjects(codedFunctionObject 所在库)
name 是 生成 functionObject 的类型名(旧键 redirectType)
codeData 否 局部成员数据(默认构造,注入模板 .H 私有区)
codeRead 否 read() 时执行
codeExecute 否 execute()(每步)执行
codeWrite 否 write()(写出步)执行
codeEnd 否 end() 执行
codeInclude/codeOptions/codeLibs 否 头/编译/链接补充
localCode 否 文件作用域静态辅助函数
codeContext 否 转发的上下文字典

codeExecute 里可用 mesh()(fvMesh,模板 .C 里 refCast<const fvMesh>(obr_) 定义)、obr_、name() 等。


12. fv::CodedSource(源泉 coded)

文件:src/fvOptions/sources/general/codedSource/CodedFvSource.{H,C}。用 makeFvOption 实例化五种(codedFvSources.C):makeFvOption(CodedSource, scalar) 等,得到运行时类型名 scalarCodedSource/vectorCodedSource...。

12.1 prepare:四个源项片段(逐行)

CodedFvSource.C(第 66--134 行):

cpp 复制代码
template<class Type>
void Foam::fv::CodedSource<Type>::prepare(dynamicCode& dynCode, const dynamicCodeContext& context) const
{
    const word sourceType(pTraits<Type>::typeName);
    dynCode.setFilterVariable("typeName", name_);
    dynCode.setFilterVariable("TemplateType", sourceType);
    dynCode.setFilterVariable("SourceType", sourceType + "Source");

    dynCode.setFilterVariable("codeCorrect", codeCorrect_);
    dynCode.setFilterVariable("codeConstrain", codeConstrain_);
    dynCode.setFilterVariable("codeAddSup",
        codeAddSup_.empty() ? "NotImplemented" : codeAddSup_);       // 空则 NotImplemented
    dynCode.setFilterVariable("codeAddSupRho",
        codeAddSupRho_.empty() ? "NotImplemented" : codeAddSupRho_);

    dynCode.addCompileFile(codeTemplateC);
    dynCode.addCopyFile(codeTemplateH);

    dynCode.setMakeOptions
    (
        "EXE_INC = -g \\\n-I$(LIB_SRC)/finiteVolume/lnInclude \\\n-I$(LIB_SRC)/fvOptions/lnInclude \\\n"
        "-I$(LIB_SRC)/meshTools/lnInclude \\\n-I$(LIB_SRC)/sampling/lnInclude \\\n"
      + context.options() + "\n\nLIB_LIBS = \\\n    -lfiniteVolume \\\n    -lfvOptions \\\n    -lmeshTools \\\n    -lsampling \\\n"
      + context.libs()
    );
}

逐行解读:

  1. TemplateType = sourceType(scalar/vector...);SourceType = sourceType + "Source"(scalarSource...);两者都进生成类名。
  2. typeName = name_(短名)。
  3. codeCorrect/codeConstrain 直接设。
  4. codeAddSup/codeAddSupRho:空则注入 "NotImplemented"------让"用错分支(compressible vs incompressible)"暴露(注释原话)。
  5. include 路径与链接库含 fvOptions、sampling。

12.2 read:兼容键与必填检查(逐行)

CodedFvSource.C(第 156--204 行):

cpp 复制代码
template<class Type>
bool Foam::fv::CodedSource<Type>::read(const dictionary& dict)
{
    codedBase::setCodeContext(coeffs_);                 // 用源项子字典建 context
    if (!fv::cellSetOption::read(dict)) { return false; }
    coeffs_.readEntry("fields", fieldNames_);
    fv::option::resetApplied();
    dict.readCompat<word>("name", {{"redirectType", 1706}}, name_);

    auto& ctx = codedBase::codeContext();
    ctx.readEntry("codeCorrect", codeCorrect_);         // 默认 mandatory=true => 必填

    const bool mandatory = (!ctx.findEntry("codeAddSup") && !ctx.findEntry("codeAddSupRho"));
    ctx.readEntry("codeAddSup", codeAddSup_, mandatory);
    ctx.readEntry("codeAddSupRho", codeAddSupRho_, mandatory);

    ctx.readEntry   // Compatibility: 接受旧键 codeSetValue
    (
        coeffs_.lookupEntryCompat("codeConstrain", {{ "codeSetValue", 1812 }}, keyType::LITERAL).keyword(),
        codeConstrain_
    );
    return true;
}

逐行解读:

  1. codedBase::setCodeContext(coeffs_):用 coeffs_(源项子字典)建上下文 ,不是整个 fvOptions 字典------源项代码写在 <source>Coeffs 子字典里。
  2. cellSetOption::read(dict):读 selectionMode/cellZone 等;coeffs_.readEntry("fields", fieldNames_) 读 fields (h)。
  3. dict.readCompat<word>("name", {{"redirectType", 1706}}, name_)。
  4. ctx.readEntry("codeCorrect", codeCorrect_):readEntry 默认 mandatory=true,所以 codeCorrect 实际必填 ------不写会报 Entry 'codeCorrect' not found(常见坑)。
  5. mandatory = (!findEntry("codeAddSup") && !findEntry("codeAddSupRho")):codeAddSup 与 codeAddSupRho 二者其一即可;都没有则报缺失。
  6. codeConstrain 用 lookupEntryCompat,接受旧键名 codeSetValue(v1812 兼容)。

12.3 redirectOption / correct / addSup / constrain(逐行)

CodedFvSource.C(第 207--288 行):

cpp 复制代码
template<class Type>
Foam::fv::option& Foam::fv::CodedSource<Type>::redirectOption() const
{
    if (!redirectOptionPtr_)
    {
        dictionary constructDict(dict_);
        constructDict.set("type", name_);
        constructDict.changeKeyword(modelType_ & "Coeffs", name_ & "Coeffs");  // 改名关键
        redirectOptionPtr_ = fv::option::New(name_, constructDict, mesh_);
    }
    return *redirectOptionPtr_;
}
template<class Type>
void Foam::fv::CodedSource<Type>::addSup(fvMatrix<Type>& eqn, const label fieldi)
{
    DebugInfo << "... ::addSup for source " << name_ << endl;
    updateLibrary(name_);
    redirectOption().addSup(eqn, fieldi);
}
// addSup(rho) / constrain 同构:updateLibrary(name_) + 转发

逐行解读:

  1. constructDict.set("type", name_):让 fv::option::New 找到生成类。
  2. constructDict.changeKeyword(modelType_ & "Coeffs", name_ & "Coeffs"):把子字典键从 <modelType>Coeffs 改名为 <name>Coeffs 。例如原字典是 scalarCodedSourceCoeffs,生成对象期望 sourceTimeCoeffs(name_=sourceTime)。不改名则生成对象读不到参数------这是 CodedSource 隐蔽但关键的适配。
  3. correct/addSup/addSup(rho)/constrain 三处都是 updateLibrary(name_) + 转发。

12.4 完整模板:codedSource(含参数表)

模板 6:codedSource 热源 h(随时间的体积源)

cpp 复制代码
energySource
{
    type            scalarCodedSource;
    active          true;
    selectionMode   all;

    scalarCodedSourceCoeffs
    {
        fields          (h);
        name            sourceTime;    // 生成 fvOption 的名字

        codeInclude
        #{
        #};

        codeCorrect
        #{
            Pout<< "**codeCorrect**" << endl;
        #};

        codeAddSup
        #{
            const Time& time = mesh().time();
            const scalarField& V = mesh_.V();
            scalarField& heSource = eqn.source();
            heSource -= 0.1*sqr(time.value())*V;
        #};

        codeConstrain
        #{
            Pout<< "**codeConstrain**" << endl;
        #};
    }

    sourceTimeCoeffs
    {
        // Dummy entry(redirectOption 会改名指向它)
    }
}
条目 必填 说明
type 是 scalarCodedSource/vectorCodedSource/...
<type>Coeffs 子字典 是 代码与参数所在的 coeffs_(被 setCodeContext 使用)
fields 是 作用的字段名列表
name 是 生成 fvOption 名字;决定目录与钩子前缀
codeCorrect 是 readEntry 默认必填
codeAddSup 或 codeAddSupRho 至少其一 显式/隐式源项
codeConstrain(旧名 codeSetValue) 否 约束
codeInclude/codeOptions/codeLibs 否 头/编译/链接补充
localCode 否 局部静态函数
<name>Coeffs 子字典 否 生成对象自己的 coeffs

codeAddSup 里 mesh_ 是生成对象的 fvMesh&、eqn 是 fvMatrix、eqn.source() 显式源项、mesh_.V() 网格体积。


13. 其它 coded 家族成员

codedPoints0MotionSolver (src/dynamicMesh/motionSolvers/displacement/codedPoints0/):prepare(.C 第 75--112 行)设 typeName = name_,默认链接含 dynamicMesh、fvMotionSolvers;codeDict() 返回 motionSolver::coeffDict();redirectMotionSolver() 里 set("solver", name_) 与 set("motionSolver", name_) 同时覆盖两个键 ;curPoints/solve/movePoints/updateMesh 都先 updateLibrary(name_)。

coded Function1 / PatchFunction1 :codedFunction1Template.C 注册 Function1Types::${typeName}Function1_${TemplateType},在 value(scalar x) 注入 ${code};codedPatchFunction1Template 类似。

codedFixedValuePointPatchField :与 codedFixedValue 同构,父类 fixedValuePointPatchField,用于点场(pointMotionU 等)。

共同点:都是"双继承 + 覆盖 prepare + 重定向到生成对象 + 每步 updateLibrary";差异只在父类、模板名、默认链接库、代码注入点 。它们的模板 .H 同样让生成类继承 dictionaryContent(否则出现 "Did not derive from dictionaryContent" 警告)。


14. JIT 与 ABI

coded 是运行期 JIT(just-in-time)编译 ,不是"解释执行"。它把 C++ 交给本机编译器生成机器码 .so,因此编译标志与 ABI 必须与主程序一致,否则会出现"能编过、一加载就崩/符号找不到"。

要点:

  1. 目标目录带 WM_OPTIONS :libSubDir_ = platforms/${WM_OPTIONS}/lib。WM_OPTIONS(如 linux64GccDPInt32Opt)编码了编译器、位宽、精度、优化档;换编译器/档位 → 路径变 → 不会误用不兼容的 .so。
  2. EXE_INC 的 -I$(LIB_SRC)/.../lnInclude 与 LIB_LIBS 的 -l<lib> 必须与主程序一致:主程序用哪个编译档,coded 库就链同一个档,否则同名符号的地址/布局可能不同。
  3. extern "C" 是 ABI 关键 :钩子/入口都用 extern "C" 导出,避开 C++ 名字修饰差异,dlsym 才能按裸名找到。
  4. 不匹配的典型症状 :undefined symbol、段错误、dlopen 成功但调用后崩溃、Failed symbol lookup。根治办法:清掉 dynamicCode/ 用正确的 WM_OPTIONS 重编。
  5. Windows 更复杂 :.dll 导出表、MSVC 运行库、dlopen 语义都与 POSIX 不同(EXT_SO="dll");交叉编译环境里 coded 基本不可用。

取舍 :省去"改物理就重编整个 OpenFOAM"的长循环,代价是首次运行有编译延迟 、依赖本机工具链、产物散落 case 目录。性能上与预编译库等价(都是机器码),差别只在编译时机。


15. 编译标志与库路径

Make/options 由 setMakeOptions() 生成,语法是 wmake 的两段式:

bash 复制代码
EXE_INC = -g \
          -I$(LIB_SRC)/finiteVolume/lnInclude \
          -I$(LIB_SRC)/meshTools/lnInclude
          <用户 codeOptions>

LIB_LIBS = \
    -lOpenFOAM \
    -lfiniteVolume \
    -lmeshTools
    <用户 codeLibs>
  • EXE_INC:编译阶段的 include 路径与编译选项(-I、-g)。
  • LIB_LIBS:链接阶段的库(-l)。
  • 各行末尾 \ 是续行符(wmake 的 Make/options 约定),末行不加。
  • $(LIB_SRC) 指 <OpenFOAM 安装>/src;lnInclude 是该库把所有头文件汇总到一个目录的约定(第一季第 2 章)。
  • 各 coded 类的默认链接库见 §17 表 D。
  • 目标库路径由 Make/files 的 LIB = $(PWD)/../platforms/$(WM_OPTIONS)/lib/lib<codeName> 决定(§4.4);wmake libso 即用此 LIB = 行产出 .so。

16. 常见报错 / 警告:成因、排查、解决(≥20 条)

A. 安全与权限类

  1. Loading shared libraries using case-supplied code may have been disabled ... allowSystemOperations 1 ------ 成因:InfoSwitches 未开启;解决:system/controlDict 加 allowSystemOperations 1(或确认 etc/controlDict 第 74--75 行被加载)。
  2. This code should not be executed by someone with administrator rights ... ------ 成因:以 root/管理员运行;解决:用普通用户运行。
  3. Entry 'code' not found in dictionary ... ------ 成因:code 必填却缺失或层级错;解决:补 code,或改从 system/codeDict 取。
  4. Entry 'codeCorrect' not found ...(CodedSource)------ readEntry 默认必填;解决:补 codeCorrect。
  5. Entry 'codeAddSup' not found ...(CodedSource)------ codeAddSup/codeAddSupRho 都没有;解决:至少给一个。
  6. No critical "code" prefixed keywords found.(codedFunctionObject 警告)------ 五个 code* 都缺;解决:至少写 codeExecute/codeWrite。

B. 模板与生成文件类

  1. Could not find code template(s): <name> Under the $FOAM_CODE_TEMPLATES directory or via the <etc>/codeTemplates/dynamicCode expansion ------ 排查:echo $FOAM_CODE_TEMPLATES、确认 <etc>/codeTemplates/dynamicCode/;解决:按 §4.3 两种落位之一放置。
  2. Failed writing files for <dynamicCode/.../libXXX.so> ------ 落盘失败(权限/磁盘/占用);解决:检查 <case> 可写、清理 dynamicCode/<name>/。
  3. 生成目录里 .C/.H 名与 #include 不一致 ------ .C 模板的 #include 与 addCopyFile 的 .H 名不匹配;解决:让二者文件名一致。

C. 编译失败类

  1. 编译器语法错误(error: expected ';' ...,行数指回字典)------ code 段 C++ 语法错;排查:看 #line 指回的 system/controlDict:NN;解决:修 C++。
  2. fatal error: fvCFD.H: No such file or directory ------ 缺 -I 或未 codeInclude;解决:codeInclude 加 fvCFD.H,codeOptions 补 -I$(LIB_SRC)/finiteVolume/lnInclude。
  3. undefined reference to <symbol> ------ LIB_LIBS 缺库;解决:codeLibs 加 -l<lib>。
  4. could not find wmake / wmake: command not found ------ shell 未 source 环境;解决:source $WM_PROJECT_DIR/etc/bashrc。
  5. Failed wmake <dynamicCode/.../libXXX.so> ------ system("wmake ...") 非 0;排查:手动 cd <path> && wmake libso 复现;解决:按 stderr 修。
  6. 编译标志不一致的 undefined symbol: _ZN... ------ .so 与主程序不同档位;解决:清 dynamicCode/,确保 WM_OPTIONS 一致。

D. 加载与符号类

  1. Failed loading library <libRelPath> on some processors. Did you add all libraries to the 'libs' entry in system/controlDict? ------ dlopen 失败或并行缺库;解决:补 libs (...),查权限/路径。
  2. Failed symbol lookup <codeName> from <libPath> ------ 库内无 <codeName> 钩子(自定义模板没导出 <typeName>_<SHA1>,或 typeName 与 name_ 不一致);解决:修模板/prepare。
  3. Failed looking up symbol codeStream_<sha1> in library ... ------ 主函数未导出/被 C++ 修饰;解决:确认 extern "C" 且 ${typeName} 替换正确。
  4. Could not load <libName> + 平台消息(undefined symbol/file too short)------ .so 未编完/损坏/缺依赖;解决:删库重编、ldd 查依赖。
  5. X Did not derive from dictionaryContent(警告)------ 生成类没继承 dictionaryContent;解决:模板 .H 加 public dictionaryContent 并转发 codeContext()。

E. 重编译与缓存类

  1. 改代码不生效 ------ 字典没被重读(READ_MODIFIED)或改的是未进哈希 的片段;排查:看 stdout 有无 Invoking wmake/Using dynamicCode;解决:确认改的键已入 SHA1,或 touch 字典。
  2. 缓存反向失效(没改却重编)------ 字典含 $/时间等被 inplaceExpand 展开的内容;解决:避免在 code 段写会被展开的变量。
  3. NFS 下 Excessive size when reading (NFS mounted) library ... increase fileModificationSkew or maxFileModificationPolls / Cannot read (NFS mounted) library ------ master 与本地库大小/可见性不一致;解决:调大 fileModificationSkew/maxFileModificationPolls,非 NFS 分布式设 fileModificationSkew 0。
  4. 并行下只一个进程编、其他崩 ------ 只 master 编译但未 broadcastCopy/waitForFile;解决:确认 fileHandler 与 fileModificationSkew。

F. 语法与输入类

  1. #{ ... #} 里出现 ~ 或字典解析错乱(忘了 verbatim 包裹)------ ~ 被 string::expand 展开 / 用了普通字符串;解决:避免 ~,一律用 #{ ... #} 包多行代码。

排查通用套路 :① 看 stdout 有无 Invoking wmake libso <path> 与 Using dynamicCode for ...;② 直接 cd <该 path> && wmake libso 复现编译错误;③ 检查 dynamicCode/<name>/Make/{files,options} 与 SHA1Digest;④ 用 nm -D lib<name>_<sha1>.so | grep <codeName> 确认符号;⑤ 清 dynamicCode/ 重来。


17. 选型与对比表(≥5 张)

表 A:coded vs 编译期库 vs 运行期免编译(fvOptions/functionObject)

维度 coded(运行期编译) 编译期 C++ 库 fvOptions / 普通 functionObject
改物理是否需要重编 否(改字典即改代码) 是(重编库) 否(改字典)
表达力 任意 C++ 任意 C++ 仅内置模型/参数的组合
运行期依赖 需要 wmake + 编译器 无 无
首次延迟 有(编译) 无 无
性能 与预编译库等价(机器码) 最优 最优
可移植/可部署 弱(产物散落、跨平台受限) 强 强
可版本管理 弱(代码在字典里) 强 中

表 B:codeStream vs coded*

维度 #codeStream coded* 类
生成物 一个自由函数 (Ostream&, const dictionary&) 一个完整的对象子类(patchField/functionObject/fvOption...)
是否继承 codedBase 否(直接用 dynamicCode) 是
库导出符号 codeStream_<sha1>(主函数) <name>_<sha1>(钩子)+ 实现类
目录名 _<sha1>(每版新目录) <name>(固定目录)
缓存/复用 内容哈希天然分目录 同名多版本库共处一目录
典型用途 生成字典条目/场初值 边界/源项/函数对象

表 C:各 coded 类的默认链接库

类 LIB_LIBS 默认
codeStream -lOpenFOAM
codedFixedValue / codedMixed -lOpenFOAM -lfiniteVolume -lmeshTools
codedFunctionObject -lOpenFOAM -lfiniteVolume -lmeshTools
CodedSource -lfiniteVolume -lfvOptions -lmeshTools -lsampling
codedPoints0MotionSolver -lfiniteVolume -lmeshTools -ldynamicMesh -lfvMotionSolvers

表 D:编译目标(wmake)

目标 wmake 用法 产物 谁用
应用 wmake(EXE =) <appbin>/exe 求解器/工具
动态库 wmake libso(LIB =) platforms/<WM_OPTIONS>/lib/libX.so coded / 运行期编译
静态库 wmake lib libX.a 少数场景

表 E:codeStream 与 coded 的缓存键

项 codeStream coded
codeName codeStream_<sha1> <name>_<sha1>
codeDirName _<sha1> <name>
codePath <case>/dynamicCode/_<sha1> <case>/dynamicCode/<name>
digest <codePath>/Make/SHA1Digest 同

18. 最佳实践:从 coded 落地到正式类

  1. 迭代用 coded,定型落地 :先快速迭代,参数与公式定下后把代码"翻译"成正式的 fvPatchField/fv::option/functionObject 子类(见 S2-15 第 9 节),获得可移植性与无运行期编译依赖。
  2. 代码与配置分离 :把 code 段放到 system/codeDict(READ_MODIFIED),边界文件只留 type/name/参数------热改代码不动边界,边界文件也干净。
  3. 最小 codeInclude/codeOptions :只加真正需要的头与 -I/-l;code 段里别写会被 string::expand 展开的 ~/$。
  4. 别在 coded 里做重活 :updateLibrary 已做哈希短路(不改代码时开销极小),但 JIT 有首次延迟、产物散落,不宜把重算放进依赖重编的路径。
  5. 并行/集群注意 fileModificationSkew :NFS 挂载保留默认(5/20),分布式不共享文件系统时设为 0;不要把 coded 用于安全敏感场景 (checkSecurity 禁止以管理员身份运行运行期 dlopen 的算例代码)。

19. 小结

coded 机制的完整链路(回看 §0.1 四条链):

bash 复制代码
字典里的 code/codeInclude/codeOptions/codeLibs
  │  #{ ... #} verbatim token(ISstream,内容不含定界符)
  ▼
dynamicCodeContext(setCodeContext:逐键 readEntry,边读边 append 进 SHA1;加 #line)
  ▼
dynamicCode(codeName×codeDirName → 目录 codePath → 库 libPath;替换变量表 filterVars_)
  ├ upToDate(比 Make/SHA1Digest)? 是 → 直接加载
  └ 否 → copyAndFilter(${...} 替换模板)+ createMakeFiles/createMakeOptions + writeDigest
         → wmakeLibso() → system("wmake -s libso <codePath>")   ← 运行期调用 wmake!
  ▼
dlLibraryTable(挂 Time):open(dlopen) + loadHook(dlsym → (*fun)(true)),句柄缓存进 libPtrs_
  ▼
真实对象(functionObject / fvOption / patchField / motionSolver / 自由函数)

关键结论:

  • `coded = "把第一季第 2 章的 wmake 工具链搬到运行期来编译字典里的 C++"。
  • 缓存键 = 内容哈希(SHA1) ,且 codeName/codeDirName 的命名差异决定落位;"改一行就重编"靠"哈希生成新路径",不靠文件时间。
  • 符号名 codeName 与模板钩子 <typeName>_<SHA1> 必须相等 ------靠 prepare 把 typeName 设为短名、SHA1sum 用无前缀 str()。
  • 多片段对象(codedFunctionObject/CodedSource)必须把每个代码片段纳入哈希 (readEntry/append),否则热更新失效。
  • 依赖本机 wmake + 编译器,且受 allowSystemOperations 与"非管理员"两道安全闸限制。
  • 表达力换部署成本:coded 适合探索/特例,定型后落地为正式 C++ 类。

理解它,就理解了 OpenFOAM 为什么能做到"配置即代码"。

相关推荐
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-01 多相流与 VOF:`interFoam`、界面压缩与 MULES
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-14 `functionObjects`、求解器控制框架 与 表达式系统
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-13 `fvOptions` / `fvConstraints` / `corrections`
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:`Cloud`/`parcel` 模板体系、运动积分与子模型群
算法·开源
hetao17338371 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法
小严不想当码农1 小时前
icpc 亚洲区域赛补题链接整理
算法
xiaok1 小时前
Compositor图编辑器
算法
CAD开发小慧1 小时前
CAE软件开发:Coreform为何用ACIS处理CAD几何?
算法·架构·计算机图形学
七牛云行业应用1 小时前
DeepSeek Harness 自定义 Skill 教程:从 SKILL.md 到可复用 Agent 工作流
算法·github