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. 本章地图
本章要回答的源码级问题(括号为对应章节):
#{ ... #}在字典里怎么被解析成 token?(§7.1)dynamicCodeContext读了哪些键?SHA1 怎么算?为什么codeOptions不加#line?(§3)- 生成的目录与库文件叫什么名?
codeName与codeDirName为何不同?(§4.1) - 模板怎样被"复制 + 变量替换"成可编译源文件?
${typeName}在哪被替换?(§4.3、§5) Make/files、Make/options内容?LIB = $(PWD)/../platforms/$(WM_OPTIONS)/lib什么意思?(§4.4)wmake libso做了什么?为什么 coded 强依赖本机编译器?(§4.5).so怎么加载?dlsym找哪个符号?为什么每个库都有extern "C" void <typeName>_<SHA1>(bool)钩子?(§6、§8.5)- 函数指针缓存在哪?"改一行就重编"的判定条件?(§4.6、§6.3)
- 并行/NFS 下只让 master 编译再广播的机制?(§4.8、§8.3)
- 哪些编译标志、
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_);
}
逐行解读:
dict_ = std::cref<dictionary>(dict);:绑定新字典。sha1_.clear();:清空哈希重算------这是热更新能生效的前提。readIfPresent("codeOptions", codeOptions_, false):第三参false= 不加#line。因为codeOptions最终写进Make/options(编译/链接命令行,如-I.../-l...),不是 C++ 源文件,加#line只会是垃圾。codeLibs同理不加#line。- 后三个(
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;
}
逐行解读(三个易忽略细节):
str.clear();:先清空输出串,保证多次调用不残留旧内容。sha1_.append("<" + key + ">");:先把<键名>标签喂进哈希 。这样把内容从code挪到localCode(内容一样但语义不同)也会得出不同哈希,避免缓存误命中。dict.findEntry(key, keyType::LITERAL):LITERAL精确查找,不解释正则。找不到且mandatory→FatalIOError:Entry 'code' not found in dictionary ...。eptr->readEntry(str);:把 entry 的 token 读成字符串。inplaceExpand(str, dict)(第 34--42 行):先stringOps::inplaceTrim(去首尾空白,对编译选项必要),再stringOps::inplaceExpand(按字典上下文展开$var/${var},不是按环境变量 )。这也是codeStream.H里~限制的根源。sha1_.append(str);:把展开后的最终文本喂进哈希------哈希针对"实际参与编译的文本"。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;
}
逐行解读:
++lineNum;:startLineNumber()是 0 基,#line是 1 基。if (lineNum > 0 && len && !file.empty()):三重保护,任一不满足则不加#line(防止生成#line 0 ""非法指令)。- 拼出
#line 12 "system/controlDict"\n<原始代码>;编译器报错会显示system/controlDict:12,用户能直接跳到字典那行。返回新增长度用于偏移恢复。 - 另一重载(第 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_); }
逐行推导:
codeRoot_ = argList::envGlobalPath()/topDirName:envGlobalPath()返回FOAM_CASE,故codeRoot_ = <case>/dynamicCode。libSubDir_ = stringOps::expand("platforms/${WM_OPTIONS}/lib"):展开WM_OPTIONS(如linux64GccDPInt32Opt),得platforms/<WM_OPTIONS>/lib。不同编译选项的.so天然隔离。codeDirName_为空则退化为codeName_。codePath()(定义在 H 第 200--203 行)=codeRoot_/codeDirName_=<case>/dynamicCode/<codeDirName>。libPath()=<case>/dynamicCode/platforms/<WM_OPTIONS>/lib/lib<codeName>.so。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());
}
逐行解读:
filterVars_是HashTable<string>(H 第 87 行):保存"模板${名字}替换成什么"的映射。clear()清空四类文件列表与替换表,预置typeName = codeName_(下游prepare()通常覆盖为短名)、SHA1sum = SHA1Digest().str()(40 个0)。- 默认
makeOptions_ = "EXE_INC = -g\n\n\nLIB_LIBS = ";-g带调试信息(便于 gdb 调运行期代码)。 reset(context)=clear()+setFilterContext(context)。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;
}
逐段解读:
compileFiles_(要编译的.C模板)与copyFiles_(要拷贝的.H模板)合并统计nFiles,分开解析 ------.C与.H可来自不同目录。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 行)。outputDir = codePath(),mkDir(outputDir)建目录。- 每个模板:目标
outputDir/srcFile.name()(只取文件名 ),copyAndFilter(is, os, filterVars_)做替换(见下)。 createFiles_(addCreateFile直接给定内容的文件)只对文件名stringOps::expand一次,内容直接写盘,不经替换。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());
}
逐行解读:
- 前置检查读/写流;坏则
FatalError。 - 逐行
getLine,stringOps::inplaceExpand(line, mapping)------按 HashTable 映射展开 (只替换filterVars_登记的键),不按环境变量展开(注释:"按环境变量展开会带来太多意外")。 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;
}
逐行解读:
-
compileFiles_空则return false。 -
目标
<case>/dynamicCode/<codeDirName>/Make/files。 -
writeCommentSHA1(os)(第 183--194 行):若替换表有SHA1sum,写 C 注释/* dynamicCode:\n * SHA1 = <sha1>\n */------人类可读的"库对应哪份代码"标记。 -
逐行写出各
.C文件名(如codeStreamTemplate.C)。 -
末行:
targetLibDir + "/lib" + codeName_,即makefileLIB = $(PWD)/../platforms/$(WM_OPTIONS)/lib/libcodeStream_<sha1>LIB =值不含扩展名 ,扩展名由 wmake 平台规则补(Linux 补.so)。 -
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;
}
逐行解读:
- 命令行
{"wmake","-s","libso",codePath()}:-ssilent、libso目标是动态库、末参是编译目录(wmake 在其中找Make/)。这是"coded = 运行期调用 wmake"的直接证据。 - 注释指出可显式解析 wmake 路径;默认只依赖 PATH 里的
wmake------coded 强依赖 shell 中 wmake 可用(§16 的 "could not find wmake" 根因)。 infoDetailLevel > 0走Info,否则InfoErr:编译耗时,即便关了详细输出也打印Invoking wmake libso <path>。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 完整版)
- 每次
updateLibrary(name)重新构造dynamicCodeContext(codedBase.C第 364 行updateLibrary(name, dynamicCodeContext(dict)))→setCodeContext重算 SHA1。 - 用新 SHA1 构造
dynamicCode:codeName = name + "_<新sha1>",libPath()带新 SHA1。 if (libs().findLibrary(libPath)) return;(codedBase.C第 320--323 行):新路径对应的库已加载则直接返回。- 否则
loadLibrary(libPath, ...)尝试加载已存在 的.so(可能上次运行留下)。 - 加载失败 →
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);
}
}
逐行解读:
-
第一道闸:
isAdministrator()(以 root/管理员运行)→FatalIOError。理由:运行期dlopen加载算例提供代码,若以管理员身份执行等于开放任意代码执行权限,风险极高。 -
第二道闸:
!allowSystemOperations→FatalIOError,报错里直接给出开启方法 :system/controlDict的InfoSwitches加allowSystemOperations 1,并列出三个 controlDict 位置。 -
etc/controlDict第 74--75 行正是该键:ini// Allow case-supplied C++ code (#codeStream, codedFixedValue) allowSystemOperations 1;官方 etc 默认已置 1,但
dynamicCode静态默认是 0 。若未加载到含该键的 controlDict,默认禁止------这解释了很多"code may have been disabled"报错。 -
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
}
逐行解读:
- 非并行直接返回。
- 广播 master 上的文件大小给全体。
- 轮询条件:未超过
maxFileModificationPolls(默认 20)且fileModificationSkew > 0(默认 5 秒)。 - 大小一致即返回;比 master 大 →
FatalIOError(Excessive size ... detected size X whereas master size is Y bytes);比 master 小 →sleep(fileModificationSkew)后重查。 - 轮询耗尽仍不一致 → 再报
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
}
}
逐行解读:
- 顶部固定
#include了dictionary.H/Ostream.H/Pstream.H/pointField.H/tensor.H/unitConversion.H------方便用户代码免写codeInclude就能用scalar/vector/tensor/单位换算;要更多头文件仍需codeInclude。 //{{{ begin codeInclude ... }}} end codeInclude://{{{ }}}只是普通注释,对编译器无害;这三行是"注入区锚点",${codeInclude}被替换为用户#include列表。${localCode}同理注入"文件作用域辅助代码"。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&);对应。${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}
);
}
逐行解读:
#include "fixedValueFvPatchFieldTemplate.H"------即被addCopyFile拷来的.H。.C与.H必须配对。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 时,找的就是这个钩子------这是有意为之的设计。if (load)/else:loadHook传true(加载后初始化),unloadHook传false(卸载前清理)。即codedBase::loadLibrary注释"Manual execution of code after loading. This is mandatory for codedBase."所指的钩子。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();
}
逐行解读:
- 开头检查
this->updated()。 if constexpr (${verbose:-false}):替换表里无verbose时展开为false(编译期常量),if constexpr(false)整块丢弃、零开销;#ifdef FULLDEBUG时prepare设verbose="true"(codedFixedValueFvPatchField.txx第 105--109 行),于是打印updateCoeffs rampedFixedValue sha1: ...。${code}注入用户代码(operator==(...)赋值)。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
逐行解读:
#define namespaceFoam:定义该宏以抑制fvCFD.H里的using namespace Foam;(避免与模板自身的namespace Foam {...}冲突/污染)。defineTypeNameAndDebug(${typeName}FunctionObject, 0)+addRemovableToRunTimeSelectionTable(functionObject, ...):类型名就是${typeName}(即name_,不是"coded"),注册进functionObject选择表,供redirectFunctionObject里functionObject::New(name_, ...)构造。mesh()用refCast<const fvMesh>(obr_),供用户代码访问网格。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
}
逐行解读:
${SourceType}由CodedSource::prepare设为sourceType + "Source"(CodedSource.C第 78 行),sourceType = pTraits<Type>::typeName;生成类名如sourceTimeFvOptionscalarSource。- 类型名
${typeName}注册到option选择表(fv::option::New在redirectOption()用)。 - 四个注入点
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_;
}
逐行解读:
dlcloseOnTerminate:OptimisationSwitches开关,默认 0 (不 dlclose)。etc/controlDict第 247--250 行注释解释:dlclose 更干净,但可能把"仍在被需要(用于终止库自身)的函数"也卸掉,故默认关。global_:静态单例,libs()惰性构造。coded 的libs()多直接调db().time().libs();codeStream在非baseIOdictionary场景下用全局libs()或裸dlOpen。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前缀 + 平台扩展名
}
逐行解读:
basename:取stem()(去路径去扩展名)再removeStart("lib"):librampedFixedValue_abc.so→rampedFixedValue_abc。fullname:空名返回空;否则加lib前缀并把扩展名设为EXT_SO。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;
}
逐行解读:
- 空名静默返回
nullptr。 fileName(libName).expand()展开~/$,再Foam::dlOpen(libName, msg)(带std::string& errorMsg的重载),失败时把dlerror()捕进msg。- 失败按
verbose用WarningInFunction或InfoErr打印Could not load <name>+ 平台错误。 - 成功则把句柄与名字追加进两列表------"函数指针缓存"落地的一刻。
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 行)要点:
rfind找索引;index < 0或空名返回false。ptr == nullptr则清名字返回false。Foam::dlClose(ptr)成功则清空该槽。- 成功后"别名处理"(第 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); }
逐行解读:
!handle || funcName.empty()返回false。void* symbol = Foam::dlSymFind(handle, funcName);:符号解析 ,dlSymFind第三参默认required=false,找不到返回nullptr而不报错。reinterpret_cast<loaderType>(symbol):把void*强转为typedef void (*loaderType)(bool);(H 第 115 行)------"函数指针"的铸造点。(*fun)(load):执行;true=加载钩子,false=卸载钩子。try{...}catch(...){}:吞掉钩子抛出的任何异常,ok保持false。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;
}
逐行解读:
required=false(默认,被dlSymFound用)且句柄空/符号空 → 返回空。(void) ::dlerror();:先清空历史错误 ------dlsym可能返回合法 NULL 地址,必须靠dlerror区分"真没找到"与"找到了但地址为 0"。::dlsym(handle, symbol.c_str())取地址。- 再查
::dlerror():有错误时required为真才警告,否则静默返回nullptr(区分"探测型"与"必须型")。 - 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); }
}
逐行解读:
- 词法器读到
#(token::HASH)时进入,区分三种情况。 peek()窥探下一字符;若是{(token::BEGIN_BLOCK)→ verbatim 模式 :(void) get(nextC)丢弃{,readVerbatim(*this, val)一直吃到#},内容存val。t = std::move(val); t.setType(token::tokenType::VERBATIM);:token 类型标为VERBATIM(token.H第 110 行)。- "Token stored without the surrounding delimiters" :内容不含
#{与#}。故readEntry拿到的是纯文本,不会把#{/#}当代码编译。 - 若
#后不是{但是合法 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); } // 不缓存
}
逐行解读:
dynamicCodeContext context(codeDict);读键并算 SHA1。sha1Str = sha1().str(true)→"_<sha1>";dynCode("codeStream"+sha1Str, sha1Str)→codeName="codeStream_<sha1>"、codeDirName="_<sha1>"。topDict = parentDict.topDict();doingMasterOnlyReading(topDict)判断是否"仅 master 读取"(codeStream.C第 76--97 行)。libPath = dynCode.libPath()------SHA1 已编码在路径里。- 若顶层是
baseIOdictionary(有Time)→libs(parentDict).findLibrary(libPath)查已加载库。 - 没找到则先
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;
}
逐行解读:
isNFS = (fileModificationSkew > 0)(默认 5,即默认视为 NFS)。- 编译条件三与:库未加载 + (是 master 或非 NFS)+ 不 up-to-date。NFS 下只有 master 编译;非 NFS(分布式)下各节点自编。
dynCode.reset(context):设替换变量;addCompileFile(codeTemplateC):codeTemplateC = "codeStreamTemplate.C"(codeStream.H第 151 行)。setMakeOptions(...):EXE_INC = -g+ 用户codeOptions;LIB_LIBS = -lOpenFOAM+ 用户codeLibs。codeStream 默认链-lOpenFOAM。copyOrCreateFiles(true)/wmakeLibso()失败分别Failed writing files for .../Failed wmake ...。if (!masterOnly && returnReduceOr(lib == nullptr)):并非 master-only 且任一进程 仍未拿到库 → 分布式broadcastCopy从 master 拷库,再waitForFile轮询。returnReduceOr是"全规约或"。- 再尝试一次加载;仍失败则
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入口。 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 ...。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; }
逐行解读:
evaluate先打印Using #codeStream at line <n> in file <dict>(DetailInfo)。checkSecurity(§4.7)。dictionary codeDict("#codeStream", parentDict, is);:把is当前位置起的内容读成子字典 ------即"读 code/codeInclude/codeOptions"的来源,字典名#codeStream。OStringStream os(is.format());:建内存输出流。getFunction(...)取函数指针,(*function)(os, parentDict)执行------用户代码把结果写进os。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;
};
逐行解读:
context_:对象私有 的dynamicCodeContext。简单对象(codedFixedValue)它就是主 code 字典的上下文;复杂对象(codedFunctionObject)先setCodeContext(dict)再append()补片段进哈希。oldLibPath_标mutable:updateLibrary是const成员却要更新"上次加载库路径"(典型缓存手法)。- 五个纯虚接口:
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; // 留存备用
}
逐行解读:
checkSecurity(..., context.dict()):安全闸门,报错绑定"代码所在字典"。dynamicCode(name + sha1().str(true), name)→codeName = "<name>_<sha1>"、codeDirName = "<name>"------"目录固定、库名带 SHA1"的来源。if (libs().findLibrary(libPath)) return;:最快短路,同一 SHA1 的库已加载则直接结束(同步内被多次调用不重复做事)。DetailInfo打印Using dynamicCode for <description> at line <n> in <dict>(description()子类给,如"patch inlet on field U")。this->clearRedirect();:先清掉重定向对象(旧库提供的实例),因为可能要换新库。unloadLibrary(oldLibPath_, basename(oldLibPath_), context):basename把路径转库名(lib<name>_<sha1>.so→<name>_<sha1>,恰等于卸载钩子符号名);首次调用oldLibPath_为空,open返回空即直接返回。loadLibrary(libPath, codeName, context):加载磁盘上可能早先编好的库。if (returnReduceOr(lib == nullptr)):任一进程 没加载成功就进编译分支;若本进程恰好成功而别的进程没有,先unloadLibrary(libPath, ...)保证一致,再createLibrary。createLibrary(dynCode, context):真正编译(§8.3),随后再次loadLibrary加载新库。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;
}
逐行解读:
libs().open(libPath, false):加载库;false= 不啰嗦(失败静默返回nullptr,由调用方处理)。libs().loadHook(handle, funcName, false):加载后必须执行钩子 ;funcName = codeName。- 钩子查找失败 →
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());
}
逐行解读:
isNFS同前。isCompileNode逻辑:默认worldComm的 master;分布式文件系统仅 world master 编译 (之后广播);非分布式且非 NFS (本地单节点)用commLocalNode()的 master(同节点进程共享本地文件系统,只需一个编译)。if (isCompileNode && !dynCode.upToDate(context)):编译节点 + digest 不 up-to-date 才编。dynCode.reset(context); this->prepare(dynCode, context);:重置变量、子类注入片段与选项------codedBase与子类的分工。copyOrCreateFiles(true)(true= verbose,打印Creating new library in <relpath>);失败报Failed writing files for <libRelPath>。wmakeLibso():失败报Failed wmake <libRelPath>。UPstream::barrier(worldComm):编译完先 barrier,避免"master 还没编完,别人就开始轮询空文件"。- 分布式时
broadcastCopy把库从 master 拷到各节点。 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;
}
逐行解读:
writeCodeDict把六个代码键原样写回(不存在则静默跳过)。用途:codedFixedValue::write写边界时把代码一起写出,让foamToVTK/postProcess重读时仍能重建。codeDict(obr, dictName="codeDict"):"内联 code 或缺省从system/codeDict读" 的基础。先在obr找名为codeDict的 IOdictionary;没有则以READ_MODIFIED(运行期可修改)+NO_WRITE+REGISTER从磁盘读system/codeDict并store进注册表缓存。READ_MODIFIED是热更新的关键 :system/codeDict运行中被改,下次读取会感知(配合Time的文件时间/inotify 检测)。- 字典名默认
"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>
逐行解读:
- 双继承 :
public fixedValueFvPatchField<Type>(最终是 patchField)+protected codedBase(加载/编译机制)。 dict_:去掉 heavy 数据后的代码字典;name_:用户从name键给的短名,决定生成库目录名与钩子前缀。redirectPatchFieldPtr_:指向动态生成的 BC 实例;mutable使其能在const的redirectPatchField()里惰性构造。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_));
}
逐行解读:
codeContext():有codeContext子字典就返回它,否则dictionary::null。它会被转发给生成对象(§9.3),让用户代码能读到那段上下文。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); // 跑一次代码赋值
}
}
逐行解读:
parent_bctype(p, iF, dict, IOobjectOption::NO_READ):value可选,先不读。dict_ = dictionaryContent::copyDict(dict, /*allow=*/wordList(), /*deny=*/{"type","value"}):拷贝字典但剔除type(冗余)与value(大块数据) ,保留code/codeInclude/name/codeOptions/codeContext等以便生成对象重建。copyDict定义在dictionaryContent.H第 96--101 行。name_(dict.getCompat<word>("name", {{"redirectType", 1706}})):读name,同时接受旧键名redirectType(v1706 兼容)。updateLibrary(name_):构造时立刻编译/加载(首次使用即触发运行期编译)。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_;
}
逐行解读:
if (!redirectPatchFieldPtr_):惰性构造,只造一次(除非clearRedirect后再次进入)。OCharStream os; this->writeValueEntry(os);:先把当前 value 写进内存流,再用ISpanStream/dictionary读回------构造生成对象时能带上"当前最新的 value"作初值。constructDict.set("type", name_);:把type设为name_。于是fvPatchField::New去找运行时类型名name_(如rampedFixedValue)------正是动态库里makeRemovablePatchTypeField注册的生成类。这就是"运行期把 C++ 变成可选择类型"的闭环。dynamic_cast<dictionaryContent*>成功后contentPtr->dict(this->codeContext()):把用户的codeContext注入生成对象的dictionaryContent(模板.H里codeContext()返回dictionaryContent::dict())。- 若生成类没继承
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);
}
逐行解读:
if (this->updated()) return;:本步已更新则跳过。updateLibrary(name_):每步都调用 ------热更新触发点:若system/codeDict(或内联 code)这步被改,SHA1 变、库重编重载。const_cast<...>(fvp).updateCoeffs():真正对象是const引用但updateCoeffs非 const,去 const 调用------用户代码(operator==(...))就在这一步执行。this->operator==(fvp):把真正对象算出的值拷贝回外层 patchField。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 行):cppdictionaryContent::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 行):cppredirectPatchFieldPtr_.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()
);
}
逐行解读:
setFilterVariable("typeName", name_):关键 ------模板里<typeName>FunctionObject的注册名与钩子前缀都变成name_。- 设
codeData/codeRead/codeExecute/codeWrite/codeEnd五个替换变量,对应模板.H私有数据区与.C的read/execute/write/end注入点。 addCompileFile(codeTemplateC)+addCopyFile(codeTemplateH):注意 functionObject 的.H里也有codeInclude注入(functionObjectTemplate.H第 41--43 行),故头模板也参与替换。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);
}
逐行解读:
codedBase::setCodeContext(dict):用整个 dict 建 context,读code/codeInclude/localCode/codeOptions/codeLibs全部纳入 SHA1。dict.readCompat<word>("name", {{"redirectType", 1706}}, name_):读name。auto& ctx = codedBase::codeContext();:取可变引用。ctx.readIfPresent(...)五个都读、不用||短路 (注释 "no short-circuiting")------每个存在的片段都append进 SHA1。这是"改了codeExecute也能重编"的保证。- 一个代码键都没有 →
IOWarning:No critical "code" prefixed keywords found. 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(); }
逐行解读:
constructDict.set("type", name_):类型设为name_→functionObject::New按运行时类型名name_找生成类。functionObject::New(name_, time_, constructDict):第一参是实例名name_,第二参Time。- 回填
codeContext(同 §9.3)。 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()
);
}
逐行解读:
TemplateType = sourceType(scalar/vector...);SourceType = sourceType + "Source"(scalarSource...);两者都进生成类名。typeName = name_(短名)。codeCorrect/codeConstrain直接设。codeAddSup/codeAddSupRho:空则注入"NotImplemented"------让"用错分支(compressible vs incompressible)"暴露(注释原话)。- 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;
}
逐行解读:
codedBase::setCodeContext(coeffs_):用coeffs_(源项子字典)建上下文 ,不是整个 fvOptions 字典------源项代码写在<source>Coeffs子字典里。cellSetOption::read(dict):读selectionMode/cellZone等;coeffs_.readEntry("fields", fieldNames_)读fields (h)。dict.readCompat<word>("name", {{"redirectType", 1706}}, name_)。ctx.readEntry("codeCorrect", codeCorrect_):readEntry默认mandatory=true,所以codeCorrect实际必填 ------不写会报Entry 'codeCorrect' not found(常见坑)。mandatory = (!findEntry("codeAddSup") && !findEntry("codeAddSupRho")):codeAddSup与codeAddSupRho二者其一即可;都没有则报缺失。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_) + 转发
逐行解读:
constructDict.set("type", name_):让fv::option::New找到生成类。constructDict.changeKeyword(modelType_ & "Coeffs", name_ & "Coeffs"):把子字典键从<modelType>Coeffs改名为<name>Coeffs。例如原字典是scalarCodedSourceCoeffs,生成对象期望sourceTimeCoeffs(name_=sourceTime)。不改名则生成对象读不到参数------这是CodedSource隐蔽但关键的适配。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 必须与主程序一致,否则会出现"能编过、一加载就崩/符号找不到"。
要点:
- 目标目录带
WM_OPTIONS:libSubDir_ = platforms/${WM_OPTIONS}/lib。WM_OPTIONS(如linux64GccDPInt32Opt)编码了编译器、位宽、精度、优化档;换编译器/档位 → 路径变 → 不会误用不兼容的.so。 EXE_INC的-I$(LIB_SRC)/.../lnInclude与LIB_LIBS的-l<lib>必须与主程序一致:主程序用哪个编译档,coded 库就链同一个档,否则同名符号的地址/布局可能不同。extern "C"是 ABI 关键 :钩子/入口都用extern "C"导出,避开 C++ 名字修饰差异,dlsym才能按裸名找到。- 不匹配的典型症状 :
undefined symbol、段错误、dlopen成功但调用后崩溃、Failed symbol lookup。根治办法:清掉dynamicCode/用正确的WM_OPTIONS重编。 - 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. 安全与权限类
Loading shared libraries using case-supplied code may have been disabled ... allowSystemOperations 1------ 成因:InfoSwitches未开启;解决:system/controlDict加allowSystemOperations 1(或确认etc/controlDict第 74--75 行被加载)。This code should not be executed by someone with administrator rights ...------ 成因:以 root/管理员运行;解决:用普通用户运行。Entry 'code' not found in dictionary ...------ 成因:code必填却缺失或层级错;解决:补code,或改从system/codeDict取。Entry 'codeCorrect' not found ...(CodedSource)------readEntry默认必填;解决:补codeCorrect。Entry 'codeAddSup' not found ...(CodedSource)------codeAddSup/codeAddSupRho都没有;解决:至少给一个。No critical "code" prefixed keywords found.(codedFunctionObject 警告)------ 五个code*都缺;解决:至少写codeExecute/codeWrite。
B. 模板与生成文件类
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 两种落位之一放置。Failed writing files for <dynamicCode/.../libXXX.so>------ 落盘失败(权限/磁盘/占用);解决:检查<case>可写、清理dynamicCode/<name>/。- 生成目录里
.C/.H名与#include不一致 ------.C模板的#include与addCopyFile的.H名不匹配;解决:让二者文件名一致。
C. 编译失败类
- 编译器语法错误(
error: expected ';' ...,行数指回字典)------code段 C++ 语法错;排查:看#line指回的system/controlDict:NN;解决:修 C++。 fatal error: fvCFD.H: No such file or directory------ 缺-I或未codeInclude;解决:codeInclude加fvCFD.H,codeOptions补-I$(LIB_SRC)/finiteVolume/lnInclude。undefined reference to <symbol>------LIB_LIBS缺库;解决:codeLibs加-l<lib>。could not find wmake/wmake: command not found------ shell 未 source 环境;解决:source $WM_PROJECT_DIR/etc/bashrc。Failed wmake <dynamicCode/.../libXXX.so>------system("wmake ...")非 0;排查:手动cd <path> && wmake libso复现;解决:按 stderr 修。- 编译标志不一致的
undefined symbol: _ZN...------.so与主程序不同档位;解决:清dynamicCode/,确保WM_OPTIONS一致。
D. 加载与符号类
Failed loading library <libRelPath> on some processors. Did you add all libraries to the 'libs' entry in system/controlDict?------dlopen失败或并行缺库;解决:补libs (...),查权限/路径。Failed symbol lookup <codeName> from <libPath>------ 库内无<codeName>钩子(自定义模板没导出<typeName>_<SHA1>,或typeName与name_不一致);解决:修模板/prepare。Failed looking up symbol codeStream_<sha1> in library ...------ 主函数未导出/被 C++ 修饰;解决:确认extern "C"且${typeName}替换正确。Could not load <libName>+ 平台消息(undefined symbol/file too short)------.so未编完/损坏/缺依赖;解决:删库重编、ldd查依赖。X Did not derive from dictionaryContent(警告)------ 生成类没继承dictionaryContent;解决:模板.H加public dictionaryContent并转发codeContext()。
E. 重编译与缓存类
- 改代码不生效 ------ 字典没被重读(
READ_MODIFIED)或改的是未进哈希 的片段;排查:看 stdout 有无Invoking wmake/Using dynamicCode;解决:确认改的键已入 SHA1,或touch字典。 - 缓存反向失效(没改却重编)------ 字典含
$/时间等被inplaceExpand展开的内容;解决:避免在 code 段写会被展开的变量。 - NFS 下
Excessive size when reading (NFS mounted) library ... increase fileModificationSkew or maxFileModificationPolls/Cannot read (NFS mounted) library------ master 与本地库大小/可见性不一致;解决:调大fileModificationSkew/maxFileModificationPolls,非 NFS 分布式设fileModificationSkew 0。 - 并行下只一个进程编、其他崩 ------ 只 master 编译但未
broadcastCopy/waitForFile;解决:确认fileHandler与fileModificationSkew。
F. 语法与输入类
#{ ... #}里出现~或字典解析错乱(忘了 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 落地到正式类
- 迭代用 coded,定型落地 :先快速迭代,参数与公式定下后把代码"翻译"成正式的
fvPatchField/fv::option/functionObject子类(见 S2-15 第 9 节),获得可移植性与无运行期编译依赖。 - 代码与配置分离 :把
code段放到system/codeDict(READ_MODIFIED),边界文件只留type/name/参数------热改代码不动边界,边界文件也干净。 - 最小
codeInclude/codeOptions:只加真正需要的头与-I/-l;code 段里别写会被string::expand展开的~/$。 - 别在 coded 里做重活 :
updateLibrary已做哈希短路(不改代码时开销极小),但 JIT 有首次延迟、产物散落,不宜把重算放进依赖重编的路径。 - 并行/集群注意
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 为什么能做到"配置即代码"。