Serial Studio 组合根改造(Spec 0001):用显式构造顺序与依赖捕获终结 59 个 Meyers 单例的启动竞态
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
本篇技术指南以
doc/claude/specs/0001-composition-root/spec.md为骨架,结合同目录下的 plan.md、tasks.md 以及仓库当前源码,完整讲解 Serial Studio 将约 59 个 Meyers 单例服务收敛为"组合根 + 依赖捕获"架构的动机、目标、设计、验收标准与七阶段落地路径。读者将理解:为什么惰性单例的构造顺序会让启动图依赖用户保存的设置、如何用instantiateCoreModules()固定拓扑顺序并消灭AppState<->ProjectModel重入隐患,以及如何在不回归 256 kHz 热路径的前提下把 1,852 处散落的X::instance()调用点收缩到受监管的许可面内。
背景与问题:59 个 Meyers 单例的隐式构造顺序
Serial Studio 的约 59 个核心服务全部是 Meyers 单例——在第一次调用X::instance()时才惰性构造。问题在于:这个构造顺序不是由代码定义的,而是由"哪个instance()调用恰好最先触发"涌现出来的,而第一次触发取决于用户机器上保存的设置。
spec 文档给出了一个具体而典型的例子:在一台保存了operation_mode为 ProjectFile 的机器上,AppState的构造函数会执行deriveFrameConfig(),其 ProjectFile 分支调用ProjectModel::instance()——于是ProjectModel在AppState构造函数内部被构造;而在 QuickPlot 机器上该分支从不执行,ProjectModel被推迟到更晚才构造。开发者在某台机器上验证过的启动图,只是众多顺序中的一种;任何重构若只在一种顺序下验证过,都是不完整的验证。
这个"设置条件性边界"(settings-conditional edge)不只是不整洁,而是一个活的重入风险:
ProjectModel的构造函数调用newJsonFile(),后者会发出groupsChanged信号,而此时AppState仍处于构造中途;- 如果在
newJsonFile()之前就连接了触碰 AppState 的 lambda,就会在 AppState 自己的 Meyers 初始化过程中重入AppState::instance()——这是未定义行为; - 维护者此前靠调整一次
connect调用的顺序躲过了这个问题,源码注释(spec 中引用的ProjectModel.cpp:446:"newJsonFile() emits groupsChanged while AppState is still mid-init")正是这一险情的记录。
换言之,惰性依赖图今天之所以还像一个 DAG,只是靠一条注释和一个精心放置的 connect 碰巧维持的;任何一次 connect 重排都可能触发 UB,而没有任何机制把它钉在原地。
与此同时,ModuleManager事实上已经是组合根(composition root)——它强制实例化各模块、按顺序执行 17 次setupExternalConnections()调用、随后restoreLastProject()、再注册约 60 个Cpp_*QML 上下文属性、最后加载 QML。缺少的只是:代码钉死的构造顺序,以及显式、被强制执行的"构造-接线"两阶段生命周期。Spec 0001 正是要把这一点形式化,让启动图不再依赖保存的设置,移除AppState<->ProjectModel隐患,并把松散散落的::instance()调用面(59 个单例头文件、1,852 个调用点)在回归守卫的监督下逐步收缩——同时不回归 256 kHz 热路径。
目标与非目标
目标(Goals)
- 核心模块的构造顺序由代码定义,在 QuickPlot、ProjectFile、Console-only 三种启动模式下完全一致,绝不随持久化的
operation_mode变化。 - 消除
AppState<->ProjectModel重入隐患:ProjectModel永远先于AppState构造,使设置条件性边界无法形成。 ModuleManager成为唯一、显式的组合根,具备两阶段生命周期(先构造全部模块,再接线);三条既有顺序不变式(INV-1..INV-3,见下文)由构造保证而非偶然成立。- 非热路径消费者把依赖作为捕获成员持有,而非到处散布
X::instance()调用;松散调用点数量逐波严格下降。 - 回归守卫使任何新增
::instance()调用点在 code-verify 报告中可见,调用面只减不增。 - 256 kHz 热路径门禁保持或改善(成员加载比 Meyers 守卫的原子加载加分支更廉价)。
非目标(Non-Goals)
- 不是容器/框架式依赖注入(DI):约 60 个
Cpp_*QML 上下文属性需要稳定、长寿命的QObject地址;容器会引发约 60 参数的接线爆炸和 1,852 个调用点的 flag-day 式重写;QML 从中得不到任何好处;且最终形态与普通构造函数注入在结构上完全相同——捕获成员将来可以机械地提升为构造参数。容器在这里买不到任何东西,却要付出一场重写的代价。 - 不是服务定位器(service locator):它把同样的全局状态藏在更慢、更难 grep 的查找后面,在 256 kHz 不允许间接成本的热路径上增加开销,且对真正的病灶——未钉死的惰性构造顺序——毫无作用。
- 不删除单例,也不改变 QML 上下文属性模型。
instance()访问器和约 60 个上下文属性都保留;改变的只是依赖的获取方式。 - 不把
FrameParser转换为捕获或强制实例化依赖(见附录特例):其构造闭包依赖项目内容,保持惰性/延迟。
目标架构:组合根 + 依赖捕获
Spec 0001 主张的是形式化的"组合根 + 依赖捕获",而非引入新机制。
组合根:ModuleManager拥有启动
ModuleManager拥有启动过程。新的私有方法instantiateCoreModules()最先执行,在一个由代码钉死的拓扑顺序中强制每个核心模块进入存在,使依赖图不再取决于哪个instance()先触发。该顺序的承载性质(load-bearing property)是ProjectModel先于AppState,这直接消灭设置条件性边界。
两阶段生命周期:先构造,再接线
- 阶段一(构造):
instantiateCoreModules()依次构造模块。每个构造函数只做自初始化、读取QSettings、连接到模块自身强制的对象。 - 阶段二(接线):按顺序对每个模块执行
setupExternalConnections(),连接跨模块的信号/槽。 - 随后
restoreLastProject()(INV-1)、注册上下文属性(INV-2)、安装 Qt 消息处理器(INV-3)、加载 QML。
这正是 DI 会强加的 construct/wire 拆分,只不过表达在我们已有的根里。
依赖捕获(Dependency Capture)
取代每个使用点的即席X::instance(),消费者一次性捕获依赖并持有:
- 真叶类与近叶类:作为构造函数初始化列表中的引用成员(
-Wreorder+ 零警告在构建期抓住初始化顺序错误); - 五边形(pentagon)核心模块(
AppState、ProjectModel、FrameBuilder、ConnectionManager、Dashboard):这些类在接线前就可能被触达,因此在它们各自的setupExternalConnections()顶部以指针形式捕获,接线前的表面继续保持直接instance()调用; - 热路径最后转换,并受基准门禁约束。
Ratchet(棘轮收缩)
一条建议性(advisory)lint 规则标记新增的松散::instance()调用点;许可面逐阶段收缩,直至只剩组合根、main入口、类自身的instance()定义和setupExternalConnections函数体。
三条不变式:组合根必须保住的顺序
INV-1:接线先于项目恢复
每个模块的setupExternalConnections()必须运行在restoreLastProject()之前。恢复项目会驱动接线逻辑,其前提是所有模块都已连接完毕。源码印证:ModuleManager.cpp 中appState->restoreLastProject()位于全部setupExternalConnections()调用之后。
INV-2:上下文属性注册时序
约 60 个Cpp_*QML 上下文属性必须在所有接线完成之后、QML 引擎加载之前注册,保证 QML 永远不会绑定到半接线的模块。
INV-3:Qt 消息处理器的安装时机
qInstallMessageHandler(MessageHandler)只能在Console::Handler和NotificationCenter存在之后安装。原因很细:消息处理器在第一次警告时会从任意线程构造这两个类;而Console::Handler的构造函数会拉取CommonFonts,后者触碰字体数据库,必须运行在 GUI 线程。过早安装消息处理器,可能在第一条零散警告出现时导致非 GUI 线程访问字体数据库。
附加约束
- 不得回归 256 kHz 的
--benchmark-hotpath门禁(全部七档)。 - 必须在 QuickPlot、ProjectFile、Console-only 三种模式下行为一致。
- 不得给
FrameReader/CircularBuffer添加互斥锁;热路径信号跳转保持Qt::DirectConnection;依赖捕获不得改变热路径上的连接类型。 - 行为保持:固定顺序中的每个强制实例化模块,必须是组合根今天已经传递构造的模块(仅自初始化 +
QSettings+ 自强制连接),不引入任何新构造,只是把构造提前钉住。 - 作用域纪律:在上下文属性注册时才首次构建的模块(
ProjectEditor、ProtoImporter、Examples、HelpCenter等)保持在原处,不拉入固定核心顺序。
需求与验收标准
| 编号 | 需求 | 验收标准(均已勾选完成) |
|---|---|---|
| R1 | 核心模块构造顺序由代码定义,QuickPlot/ProjectFile/Console-only 一致,不随operation_mode变化 | AC1:三种模式均正常启动运行;组合根在每种模式下都先构造 ProjectModel 再构造 AppState |
| R2 | ProjectModel 永远先于 AppState 构造,deriveFrameConfig() -> ProjectModel::instance() -> newJsonFile() -> groupsChanged重入边无法再形成 | AC1(同上) |
| R3 | 所有模块接线经 ModuleManager 两阶段完成,INV-1..INV-3 成立 | AC2:restoreLastProject()在所有setupExternalConnections()之后;上下文属性在接线后、QML 加载前注册;消息处理器在Console::Handler/NotificationCenter存在后安装 |
| R4 | 非热路径消费者以捕获成员持有依赖;松散::instance()调用点逐波严格下降、永不增加 | AC3:每波转换后grep -c "X::instance()"在构造函数初始化列表(叶类)或接线前表面(五边形)之外为零;头文件每个依赖恰好新增一个成员;advisory 计数严格下降 |
| R5 | 新建议性 lint 规则报告许可面外的新增::instance()调用点,不改变阻塞错误数 | AC4:规则落地前后python3 scripts/code-verify.py app/src的阻塞错误数完全一致,advisory 报告只新增该种类;合成Foo::instance()片段触发规则,各许可模式不触发 |
| R6 | 热路径捕获阶段后--benchmark-hotpath七档全部通过,无回归;逐帧依赖访问是成员加载而非 Meyers 守卫 | AC5:七档全部通过,与阶段前基线相比无回归 |
实现路径:七个门控阶段
plan.md 把改造拆成七个门控阶段(S1-S7),对应 tasks.md 中的任务 T1-T9,全部标记完成。
构建门控(贯穿所有实现阶段)
- 一次构建一个阶段:每个构建-启动周期只落地一个阶段(或一个 S4 波次/一个五边形类),绝不把两个顺序敏感变更混进同一次构建。
- 三种操作模式全部启动:任何改变构造顺序(S3)或模块接线(S5)的阶段之后,都要在 QuickPlot、ProjectFile、Console-only 三种模式下启动验证。
- S6 之后跑
--benchmark-hotpath:全部七档,与 S6 前基线对比;S3/S4/S5 在逐帧路径之外,不强制跑基准,但仍需启动冒烟测试。
S1:依赖普查 + 初始化顺序契约(文档)— 已完成
编写 spec/plan/tasks 包与 architecture.md 新增小节"Composition Root & Construction Order -- ModuleManager":包含验证过的构造器边图(spec 附录表)、S3 固定实例化顺序、三条不变式 INV-1..INV-3,以及逐文件::instance()清点(1,852 个调用点、59 个单例头文件)作为晨间波次的预划定。验证命令:python3 scripts/documentation-verify.py。
S2:回归守卫arch-singleton-instance建议性规则(Python)— 已完成
在 scripts/code_verify_rules.py 中新增建议性规则,并把它注册进 scripts/code-verify.py 的_ADVISORY_KINDS。当前源码中的实现要点(见 code_verify_rules.py 的 "Composition-root rule (spec 0001)" 注释段):
- 触发正则:
\b[A-Za-z_][\w:]*::instance\(\)(_SINGLETON_INSTANCE_RE); - Qt 自身的静态访问器(
QCoreApplication::instance()等)先被_QT_INSTANCE_RE从行内剥除,因为它们不是 Serial Studio 单例、没有未钉死的构造顺序; - 许可面(不产生 finding):组合根文件、访问器自身
instance()函数体、setupExternalConnections()接线函数体、过渡期static auto& x = X::instance()热路径缓存惯用法、构造初始化列表捕获、以及单行Q_ASSERT(...)表达式; _SINGLETON_SANCTIONED_FUNCS = frozenset({"instance", "setupExternalConnections"})定义了许可函数集合。
验证:规则落地前后code-verify.py app/src阻塞错误数一致、advisory 报告只新增该种类;合成片段触发、各许可模式不触发。
S3:钉死构造顺序(形式化组合根)— 已完成
新增私有void instantiateCoreModules();,作为setupCrossModuleConnections()的第一行调用。当前源码实现在 ModuleManager.cpp,其固定构造顺序(与 spec 设计一致,ProjectModel在AppState之前是承载性质)为:
TranslatorTimerEventsCommonFontsWorkspaceManagerNotificationCenterThemeManagerExtensionManagerControlScriptProjectModel(先于AppState——消灭设置条件性边)AppStateLemonSqueezy/MachineID(商业版)FrameBuilderIO::ConnectionManagerConsole::HandlerAPI::ServerCSV::PlayerMDF4::PlayerSessions::Player(商业版/可选)- 四个导出器
FrameParserUI::Dashboard(最后——其构造函数触碰五个核心模块 + 两个播放器 +TimerEvents)
注意当前源码已演进:instantiateCoreModules()同时做了消息总线挂接(attachMessageBus)、通过SessionContext收养模块(adoptProjectModel/adoptAppState等)与许可状态发布(商业版),但ProjectModel先于AppState的顺序被严格保留。作用域规则:只列出setupCrossModuleConnections今天已传递构造的模块;上下文属性注册时才构建的模块(ProjectEditor、ProtoImporter、Examples、HelpCenter等)留在原处。
验证(grep 对称性):instantiateCoreModules中的每个类也出现在后面的setupCrossModuleConnections中或已被传递构造;顺序与 spec 表完全一致;restoreLastProject()仍在所有setupExternalConnections()之后(INV-1)。
S4:叶类构造器引用捕获(宽泛、按波次)— 已完成
对每个消费者类,添加引用成员(如Misc::TimerEvents& m_timerEvents;),在构造初始化列表初始化(m_timerEvents(Misc::TimerEvents::instance())),并把文件内非热路径调用替换为成员。已验证零单例出边的叶类:Translator、TimerEvents、CommonFonts、NotificationCenter、WorkspaceManager、CSV::Player、MDF4::Player、CommandHandler(外加近叶ThemeManager)。-Wreorder+ 零警告在构建期抓住初始化顺序错误——初始化顺序错误是编译期错误而非运行时错误。
三个波次:(1)Misc/*+UI/Widgets/*中消费 CommonFonts/TimerEvents/ThemeManager 的类;(2)Console/、CSV/、MDF4/;(3)DataModel/编辑器。
逐文件验证:(a).cpp中grep -c "X::instance()"在构造初始化列表之外为 0;(b) 头文件每个依赖恰好新增一个成员;(c) 初始化列表位置与成员声明顺序一致;(d) advisory 计数严格下降。
S5:五边形延迟指针捕获 — 已完成
AppState、ProjectModel、FrameBuilder、ConnectionManager、Dashboard各添加X* m_x(构造初始化列表置 nullptr),在各自setupExternalConnections()顶部赋值,使用点Q_ASSERT(m_x);方法体调用点转为m_x->。构造器体与接线前可达的方法保持直接instance()——这些"接线前表面"包括:AppState::deriveFrameConfig()、ProjectModel::newJsonFile()/setModified()/watchProjectFile()及其可达路径、Dashboard构造函数连接的一切、ConnectionManager的m_uiDriverSaveTimerlambda。
顺序(每次晨间构建一个类):AppState->ConnectionManager->FrameBuilder->ProjectModel->Dashboard。S3 之后AppState可以构造器捕获ProjectModel(因为 S3 保证 ProjectModel 先构造),但绝不允许从 ProjectModel 构造器捕获 AppState(活体 A<->B 隐患)。
当前源码印证:AppState.cpp 中m_projectModel(DataModel::ProjectModel::instance())已出现在构造初始化列表,setupExternalConnections()(同文件第 122 行)捕获m_frameBuilder = &DataModel::FrameBuilder::instance()并连接jsonFileChanged等信号——五边形捕获已落地。
验证(每类):头文件恰好新增指针成员;每个被转换的调用点位于已被证明在接线后运行的方法中;接线前表面仍用直接instance();advisory 计数下降;三种模式启动。
S6:热路径捕获(基准门控,最后)— 已完成
把ConnectionManager::{onFrameReady,onRawDataReceived,processPayload,processMultiSourcePayload}与Dashboard::updateStreamAvailable中的static auto&缓存静态量转换为 S5 指针成员;把onFrameReady中逐帧的AppState::instance().operationMode()轮询替换为缓存的m_operationMode,由operationModeChanged信号刷新(复用 FrameBuilder 的缓存标志模式)。所有连接保持Qt::DirectConnection。
风险点:若m_operationMode未接入operationModeChanged,onFrameReady中的操作模式分支会静默变陈旧。验证:转换后的调用点是成员而非静态量;m_operationMode刷新已接到operationModeChanged;--benchmark-hotpath全部七档无回归。
S7:棘轮收缩 — 已完成
把arch-singleton-instance的许可面收缩到{ModuleManager.cpp, main.cpp, 类自身 instance() 定义, setupExternalConnections 函数体},去掉 S6 已移除的static auto&缓存惯用法豁免;可选地把五边形提升为按类阻塞。此后新服务一律从构造参数获取依赖。
源码印证:组合根当前的实际形态
仓库当前代码完整反映了 spec 的落地方案:
- ModuleManager.cpp 的
setupCrossModuleConnections()第一行即调用instantiateCoreModules(),随后执行bindInterfaces()、registerApiHandlers()、各模块setupExternalConnections(),最后appState->restoreLastProject()(INV-1); instantiateCoreModules()中ProjectModel(ctx.adoptProjectModel(...))严格先于AppState(ctx.adoptAppState(...))构造;ModuleManager.h中instantiateCoreModules/bindInterfaces/registerApiHandlers/setupHeadlessSessionConnections均为公开静态方法,并被 CLI.cpp 的无界面路径(基准、会话校验、schema 导出)复用——固定顺序成为所有启动形态的公共底座;- AppState.cpp 展示了"叶式引用成员 + 接线期指针捕获"的混合形态;
- scripts/code_verify_rules.py 中的
arch-singleton-instance规则与 spec/plan 描述的触发正则以树-sitter 作用域实现,对 Qt 静态访问器做了显式剥离。
落地后的回归事故(tasks.md 事后记录)
tasks.md 记录了一次值得警惕的回归:落地次日凌晨发现,某个在newJsonFile()内加入 AppState/Dashboard 同步的修复在 ProjectModel 构造函数内执行,在 ProjectFile 机器上递归了 Meyers 守卫(__cxa_guard_acquire在启动时 abort)。T3 的构造器边证明在书写时是正确的,但后续修复波次使其失效。修复方式:用m_initialized标志门控该同步。由此确立一条常驻规则:任何修改 ProjectModel 构造闭包(newJsonFile、watchProjectFile、scheduleAutoSave、setCode 链)的编辑,在合并前必须重新触发构造器边检查。
权衡与备选方案
plan.md 的权衡表可概括为:
| 决策点 | 选项 | 选择及理由 |
|---|---|---|
| 整体形态 | 容器 DI / 服务定位器 / 组合根+捕获 | 组合根+捕获——ModuleManager 本就是根;两阶段 construct/wire 是 DI 的拆分,却免去约 60 参数爆炸和 1,852 点 flag-day;热路径严格改善 |
| ProjectModel vs AppState 顺序 | 保持设置条件性 / 钉 PM 优先 / 钉 AppState 优先 | 钉 ProjectModel 优先——正是生产环境 ProjectFile 机器每天运行的顺序,且消灭重入边 |
| 叶类捕获机制 | 构造初始化列表引用 / 延迟指针 / 惰性访问器 | 构造初始化列表引用——-Wreorder使初始化顺序错误成为编译期错误;仅在接线前可达的类用延迟指针 |
| 热路径时机 | 随 S5 一起 / 延迟到门控的最后阶段 | 延迟到 S6 基准门控——把唯一逐帧可见的变化隔离在--benchmark-hotpath之后 |
| 守卫强度 | 立即阻塞 / 建议后棘轮 | 建议后棘轮——1,852 个既有调用点使阻塞式 flag-day 不可行;建议制让调用面逐波收缩 |
风险与缓解
- 构造期重入(核心隐患):S3 钉住 ProjectModel 先于 AppState,设置条件性边无法形成;
ProjectModel.cpp:446的围栏表面与所有接线前表面保持直接instance()(绝不捕获)。 - 叶引用成员初始化顺序错误:
-Wreorder+ 零警告使其成为构建失败。 - 接线前使用未赋值的五边形指针:使用点
Q_ASSERT(m_x);S5 明确接线前表面清单;一次构建一个类。 - 陈旧缓存标志导致热路径静默故障:S6 把
m_operationMode接入operationModeChanged;--benchmark-hotpath门禁兜底。 - 强制实例化导致行为变化:作用域规则只钉住根已传递构造的模块;S3 grep 对称性检查。
- 1,852 个调用点上的范围蔓延:严格波次边界;advisory 计数逐波严格下降、永不增加。
测试与验证计划
- 单元:无直接单元测试(无
tests/scripts/JS 面);S2 lint 规则用合成Foo::instance()片段自测,并对比code-verify.py前后阻塞计数。 - 静态:
python3 scripts/documentation-verify.py校验 S1 文档;每波对变更的 C++ 文件跑python3 scripts/code-verify.py --check;各阶段 grep 对称性配方;每个 C++ diff 经qt-cpp-review。 - 集成(维护者执行):S3 与每个 S5 五边形类之后在 QuickPlot、ProjectFile、Console-only 三种模式启动,确认正常启动、项目恢复与控制台输出。
- 热路径(维护者执行):S6 后跑
--benchmark-hotpath全部七档,与 S6 前基线对比;deploy.yml对发布的 PGO 二进制做最终兜底门禁。 - 提交:每次提交前运行
python3 scripts/sanitize-commit.py;工作树无 lint 债务。
附录:构造器捕获安全表(前 15 大单例)
下表来自 spec 附录("Capture BY others" = 其他类捕获该类是安全的;"Own deps capture mode" = 该类应如何获取自己的依赖):
| # | 单例(调用点数) | 已验证构造器出边 | 可被他人捕获 | 自身依赖捕获模式 |
|---|---|---|---|---|
| 1 | ProjectModel (343) | ControlScript(经 newJsonFile,PM.cpp:2116) | 可以,但 ControlScript/worker 除外 | setupExternalConnections中的延迟指针;ctor/newJsonFile 表面保持直接调用。绝不可 ctor 捕获 AppState(活体 A<->B 隐患) |
| 2 | ConnectionManager (301) | 无 | 可以 | 延迟指针(AppState、ProjectModel、FrameBuilder、Console::Handler、API::Server);可 ctor 捕获 Translator |
| 3 | Dashboard (152) | CSV::Player、MDF4::Player、ConnectionManager、AppState、FrameBuilder、Sessions::Player、TimerEvents(Dashboard.cpp:172-229) | 不可以——无人 ctor 捕获 Dashboard | 延迟指针;ctor 连接保持 |
| 4 | AppState (146) | ProjectModel(条件性,deriveFrameConfig) | 可以,但 ProjectModel 与 ControlScript 除外 | S3 后可 ctor 捕获 ProjectModel;其余不可 |
| 5 | ThemeManager (58) | WorkspaceManager(loadUserThemes)、Translator | 可以,但这两者除外 | 两者均可 ctor 引用捕获 |
| 6 | FrameBuilder (56) | LemonSqueezy -> MachineID(商业版) | 可以 | 延迟指针;热路径成员仅在 S6 |
| 7 | Console::Handler (33) | CommonFonts | 可以;必须早于qInstallMessageHandler存在 | 可 ctor 捕获 CommonFonts |
| 8 | CommonFonts (31) | 无(GUI 线程字体库) | 可以 | 真叶类 |
| 9 | Translator (29) | 无 | 可以 | 真叶类 |
| 10 | TimerEvents (28) | 无 | 可以 | 真叶类 |
| 11 | API::Server (27) | CommandHandler(平凡) | 可以 | 可 ctor 捕获 CommandHandler |
| 12 | CSV::Player (26) | 无 | 可以 | 近叶(qApp 过滤器) |
| 13 | WorkspaceManager (25) | 无(mkdir + 遗留迁移) | 可以 | 真叶类 |
| 14 | MDF4::Player (25) | 无 | 可以 | 近叶 |
| 15 | NotificationCenter (25) | 无 | 可以;必须早于消息处理器存在 | 真叶类 |
附录:FrameParser 特例
FrameParser(23 个调用点)只能保持惰性/延迟:其构造函数调用engineForSource(0),而 Lua 引擎构造会触达FrameBuilder::instance()(spec 附录引用的 LuaScriptEngine.cpp:167);构造闭包依赖项目内容,因此FrameParser不能被 ctor 捕获。它仍可出现在固定顺序中被强制实例化(因为根已传递构造它),但任何类都不得把它作为成员捕获。
延伸阅读:完整规范见 spec.md,技术设计见 plan.md,任务清单与落地记录见 tasks.md;架构总览见 architecture.md;lint 规则实现见 code_verify_rules.py 与 code-verify.py;组合根实现在 ModuleManager.cpp。
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考