☰
F´ Fw::Obj 模块深度解析:ObjBase 对象基类与对象注册机制
2026/9/25 3:06:40 网站建设 项目流程
  • 嵌入式
  • 系统编程

【免费下载链接】fprime

F´ - A flight software and embedded systems framework

项目地址:https://gitcode.com/gh_mirrors/fpri/fprime
点击查看免费下载

本文以 F´(Flight Software Framework)仓库中的 Fw/Obj/docs/sdd.md 设计说明文档为主体,结合Fw::ObjBase、Fw::ObjRegistry的源码实现、FpConfig.hpp编译开关以及拓扑自动生成代码,系统讲解 F´ 对象体系的根节点设计、对象命名存储、可选的对象注册机制及其工程实践。读完本文,你将掌握Fw::Obj模块的类层次定位、三个核心配置宏(FW_OBJECT_NAMES、FW_OBJECT_TO_STRING、FW_OBJECT_REGISTRATION)的作用与取值影响,以及如何利用SimpleObjRegistry在运行时转储全系统对象清单用于调试。

1. 模块概览与架构定位

Fw::Obj是 F´ 体系中最基础的模块之一。根据 Fw/Obj/docs/sdd.md 的说明,它包含两个关键类型:

  • Fw::ObjBase:定义 F´ 架构中对象的根类,是所有组件(Component)与端口(Port)的共同基类。它负责存储对象名称,并在已注册对象注册表(object registry)时执行对象注册。
  • Fw::ObjRegistry:对象注册表的纯虚基类。它是一个可选特性,由FW_OBJECT_REGISTRATION宏控制(该宏定义于 config/FpConfig.hpp,原文档写作FpConfig.hpp,在仓库中其路径为config/FpConfig.hpp)。其核心思想是:通过一个注册表跟踪系统中已实例化的所有对象,注册表可以调用Fw::ObjBase的公有接口查询实例信息,而具体如何存储对象指针、如何产出实例数据,由派生类自行实现。

模块的仓库构成非常精简,参见 Fw/Obj/README 与目录结构:

文件职责
Fw/Obj/ObjBase.hppFw::ObjBase与Fw::ObjRegistry的类声明
Fw/Obj/ObjBase.cpp上述两个类的实现
Fw/Obj/SimpleObjRegistry.hpp一个最基础的对象注册表实现(声明)
Fw/Obj/SimpleObjRegistry.cpp基础对象注册表实现(定义)
Fw/Obj/CMakeLists.txt模块构建脚本
Fw/Obj/docs/sdd.md模块软件设计说明(本文主体)

需要说明:原 sdd.md 中多处将根类写作Fw::Object,而当前仓库源码中的类名统一为Fw::ObjBase(见 Fw/Obj/ObjBase.hpp),这是设计文档早期命名与现行实现的差异,本文以源码为准。

2. 核心类 Fw::ObjBase:根类的设计与实现

2.1 类层次中的位置

从源码继承关系可以确认,Fw::ObjBase确实是组件与端口的共同祖先:

  • Fw/Port/PortBase.hpp 中class PortBase : public Fw::ObjBase,所有端口基类都派生自它;
  • Fw/Comp/PassiveComponentBase.hpp 中class PassiveComponentBase : public Fw::ObjBase,被动组件基类同样派生自它,其构造函数在 PassiveComponentBase.cpp 中显式调用Fw::ObjBase(name)完成初始化;
  • Fw/Port/PortBase.cpp 中端口构造时以Fw::ObjBase(nullptr)初始化,并在后续通过setObjName设置名称——这与ObjBase构造函数对空指针的容错逻辑(见下)相呼应。

也就是说,整个 F´ 系统中每一个组件实例、每一个端口实例,本质上都是一个Fw::ObjBase对象,因此把对象注册表放在这一层,就能无差别地跟踪全部系统对象。

2.2 职责一:对象名称的存储与读写

当FW_OBJECT_NAMES == 1时(默认开启,见 config/FpConfig.hpp),Fw::ObjBase会在每个实例内分配一个固定大小的名字缓冲区m_objName,其大小由FW_OBJ_NAME_MAX_SIZE决定(默认 80 字节,见 config/FpConfig.hpp),并提供两个接口:

  • const char* getObjName():返回对象名称指针(ObjBase.hpp);
  • void setObjName(const char* name):将传入字符串拷贝进内部缓冲区(ObjBase.hpp)。

其实现细节在 ObjBase.cpp:

void ObjBase::setObjName(const char* name) { strncpy(this->m_objName, name, sizeof(this->m_objName)); this->m_objName[sizeof(this->m_objName)-1] = 0; // 强制以 '\0' 结尾 }

注意两点工程细节:

  1. 使用strncpy并强制在缓冲区末尾写0,保证超长名称被截断后依然是以 NUL 结尾的合法 C 字符串(FpConfig.hpp注释明确"Larger names will be truncated",即超过 80 字节的名称会被截断);
  2. 构造函数对空指针做了容错:当传入nullptr时自动命名为"NoName"(ObjBase.cpp)。

当FW_OBJECT_NAMES == 0时,名称缓冲区与相关接口全部被编译掉(构造函数为空实现),以节省内存——适用于存储极度受限的嵌入式目标。此时可配合FW_OPTIONAL_NAME(name)宏(config/FpConfig.hpp),它在名称功能关闭时自动把字符串替换为空串,从而避免无意义的字符串常量占用二进制体积。

2.3 职责二:toString 对象描述

在FW_OBJECT_NAMES == 1且FW_OBJECT_TO_STRING == 1(默认开启,见 config/FpConfig.hpp)时,Fw::ObjBase提供一个虚函数:

virtual void toString(char* str, NATIVE_INT_TYPE size);

默认实现(ObjBase.cpp)输出"Obj: <name>"格式的字符串,并对size <= 0的情况使用FW_ASSERT断言保护。该方法的用途是供派生类重写以返回对象自身的描述信息——例如组件可以重写为包含实例号、组件类型等信息的描述。目标缓冲区大小由FW_OBJ_TO_STRING_BUFFER_SIZE配置(默认 255 字节,见 config/FpConfig.hpp)。注意该宏只有在FW_OBJECT_NAMES开启时才会生效,二者存在依赖关系。

3. 对象注册机制 Fw::ObjRegistry 与注册流程

3.1 编译开关与纯虚基类

对象注册是可选的,由FW_OBJECT_REGISTRATION宏控制(默认开启,见 config/FpConfig.hpp):

#ifndef FW_OBJECT_REGISTRATION #define FW_OBJECT_REGISTRATION 1 // 是否允许对象集中注册(更多代码、更强的对象跟踪能力) #endif

当该宏为 1 时,Fw/Obj/ObjBase.hpp 中定义纯虚基类:

class ObjRegistry { public: virtual void regObject(ObjBase* obj) = 0; // 对象注册时被调用的纯虚函数 virtual ~ObjRegistry(); };

regObject是注册表的唯一纯虚接口:它通过ObjBase::setObjRegistry()设置的静态指针被调用,传入对象实例指针,至于如何使用该指针完全由派生注册表决定——这正是原 sdd.md 强调的"实际存储方法与数据产出方式留给派生类"的设计。

3.2 注册调用链:setObjRegistry → init → regObject

Fw::ObjBase内部持有一个静态指针s_objRegistry(ObjBase.hpp),并提供静态函数setObjRegistry(ObjRegistry* reg)来设置它(ObjBase.hpp,实现见 ObjBase.cpp)。

每个对象初始化时调用受保护的init()(ObjBase.hpp),其实现(ObjBase.cpp):

void ObjBase::init() { #if FW_OBJECT_REGISTRATION if (ObjBase::s_objRegistry) { ObjBase::s_objRegistry->regObject(this); } #endif }

由此形成完整的注册调用链:

SimpleObjRegistry 构造(或用户代码)→ ObjBase::setObjRegistry(&registry) → 各组件/端口构造完成后调用 ObjBase::init() → s_objRegistry 非空 → registry->regObject(this) // 对象被登记

原文档与头文件注释还明确给出一个重要限制:setObjRegistry的调用"可能不可重入、非线程安全",且框架自带的SimpleObjRegistry就是不可重入的(ObjBase.hpp)。因此注册表实例化应放在启动早期的单线程上下文中完成。

3.3 配置宏速查表

与Fw::Obj相关的全部配置宏集中在 config/FpConfig.hpp,整理如下:

宏默认值作用位置
FW_OBJECT_NAMES1是否为对象存储名称(更多内存,可用于对象跟踪)FpConfig.hpp
FW_OBJECT_TO_STRING1(依赖FW_OBJECT_NAMES)是否生成toString()方法转储对象内部信息(更多代码)FpConfig.hpp
FW_OBJECT_REGISTRATION1是否启用对象集中注册(更多代码、更强对象跟踪)FpConfig.hpp
FW_OBJ_NAME_MAX_SIZE80对象名缓冲区字节数,超长截断FpConfig.hpp
FW_OBJ_TO_STRING_BUFFER_SIZE255toString()输出缓冲区大小FpConfig.hpp
FW_OBJ_SIMPLE_REG_ENTRIES500简单注册表可存储的对象数量上限FpConfig.hpp
FW_OBJ_SIMPLE_REG_BUFF_SIZE255转储对象名/描述时的缓冲区大小(应 ≥FW_OBJ_NAME_MAX_SIZE)FpConfig.hpp

可见这些宏之间存在依赖链:FW_OBJECT_TO_STRING依赖FW_OBJECT_NAMES,注册表相关容量宏依赖FW_OBJECT_REGISTRATION。为降低嵌入式二进制体积,可将三者同时置 0,但会失去对象跟踪与转储能力。

4. 参考实现 SimpleObjRegistry:默认对象注册表

SimpleObjRegistry是框架提供的最基本注册表实现(Fw/Obj/SimpleObjRegistry.hpp),其头文件注释明确它是"默认实现与示例":实例化时它会用setObjRegistry()把自己注册进ObjBase的静态指针,此后对象随实例化过程逐个登记,并可通过dump()列出注册表中的对象。

4.1 自注册的生命周期

构造函数(SimpleObjRegistry.cpp):

SimpleObjRegistry::SimpleObjRegistry() { ObjBase::setObjRegistry(this); // 将自己设为全局注册表 this->m_numEntries = 0; for (NATIVE_INT_TYPE entry = 0; entry < FW_OBJ_SIMPLE_REG_ENTRIES; entry++) { this->m_objPtrArray[entry] = nullptr; } }

析构函数则调用ObjBase::setObjRegistry(nullptr)解除注册(SimpleObjRegistry.cpp)。存储结构是一个定长指针数组m_objPtrArray[FW_OBJ_SIMPLE_REG_ENTRIES](默认 500 个槽位),regObject实现为顺序追加并带断言保护:

void SimpleObjRegistry::regObject(ObjBase* obj) { FW_ASSERT(this->m_numEntries < FW_OBJ_SIMPLE_REG_ENTRIES); // 槽位耗尽即断言失败 this->m_objPtrArray[this->m_numEntries++] = obj; }

这意味着当系统对象总数超过 500 时,FW_ASSERT会直接触发断言——这是嵌入式框架"宁可失败也不要静默越界"的典型做法。若部署的系统对象较多,应相应调大FW_OBJ_SIMPLE_REG_ENTRIES。

4.2 转储(dump)与清理(clear)

dump()遍历已登记对象,通过Fw::Logger::logMsg打印每个对象的序号、指针地址及描述(SimpleObjRegistry.cpp)。输出内容随编译开关组合变化:

  • FW_OBJECT_NAMES + FW_OBJECT_TO_STRING开启:调用toString(),输出形如Entry: %d Ptr: %p Str: %s;
  • 仅FW_OBJECT_NAMES开启:调用getObjName(),输出Entry: %d Ptr: %p Name: %s;
  • 两者皆关:仅输出Entry: %d Ptr: %p。

当FW_OBJECT_NAMES == 1时还提供按名称定向转储的dump(const char* objName)(SimpleObjRegistry.cpp),用strncmp精确匹配对象名后打印;clear()则简单地将计数清零(SimpleObjRegistry.cpp),实现"软清空"。

5. 在 F´ 工程中的实际应用:拓扑自动生成与调试入口

5.1 拓扑代码生成中的注册表实例化

F´ 的部署工程通常通过 FPP 建模生成拓扑代码,自动生成模板会默认实例化一个SimpleObjRegistry。以 publicTopologyCpp.tmpl 为例:

simpleReg_ptr = new Fw::SimpleObjRegistry();

模板随后在构造完所有组件、完成端口连接并启动活动组件之后调用dumparch()(publicTopologyCpp.tmpl)。而dumparch()与dumpobj()这两个 C 链接函数由 includes1TopologyCpp.tmpl 生成:

extern "C" { void dumparch(); void dumpobj(const char* objName); } void dumparch() { simpleReg_ptr->dump(); } void dumpobj(const char* objName) { simpleReg_ptr->dump(objName); }

也就是说,每个由自动生成模板搭建的 F´ 部署,天然具备两个调试入口:

  • dumparch():转储整个架构中已注册的全部对象(数量上限 500);
  • dumpobj("<name>"):按名称查询并转储单个对象。

这一机制让开发者无需额外打日志,即可在运行时检查所有组件与端口实例是否按预期创建、名称是否正确。实际部署代码中也能看到SimpleObjRegistry的直接引用,例如 RPI/Top/Components.hpp 在树莓派示例部署中#include <Fw/Obj/SimpleObjRegistry.hpp>,其拓扑文件会生成对应extern实例。

5.2 组件/端口如何"被登记"

由于所有组件(PassiveComponentBase等)与端口(PortBase)的基类都是Fw::ObjBase,且构造函数链最终调用ObjBase构造函数、初始化阶段调用init(),因此只要注册表已设置,系统中每个组件与端口实例都会被自动登记,无需组件作者编写任何额外代码。这正是 sdd.md 所说"对象初始化时执行注册"(register objects when they are initialized)在框架层的落地。

6. 使用建议与注意事项

  1. 注册表应尽早、单线程地建立:setObjRegistry不可重入且非线程安全(ObjBase.hpp),推荐在主函数早期、组件实例化之前完成SimpleObjRegistry的创建。
  2. 容量与资源权衡:FW_OBJ_SIMPLE_REG_ENTRIES(默认 500)与FW_OBJ_NAME_MAX_SIZE(默认 80)会随对象数量线性占用内存;小型嵌入式目标可关闭FW_OBJECT_NAMES/FW_OBJECT_TO_STRING/FW_OBJECT_REGISTRATION换取体积,但会失去dumparch()调试能力。
  3. 名称截断是预期行为:setObjName对超长名称静默截断并保证 NUL 结尾,若需要更长的对象名请调整FW_OBJ_NAME_MAX_SIZE,且注意FW_OBJ_SIMPLE_REG_BUFF_SIZE应不小于名称长度,避免转储缓冲区溢出。
  4. 注册表溢出即断言:SimpleObjRegistry槽位耗尽会触发FW_ASSERT,这是框架"显式失败优于静默错误"的工程选择,部署前应核对系统对象总数。
  5. toString是可扩展点:派生类可通过重写toString提供比默认"Obj: <name>"更丰富的对象描述(如实例号、类型、关键状态),从而让dumparch()输出更有诊断价值。

7. 变更日志

根据 Fw/Obj/docs/sdd.md 记录的模块设计文档变更历史:

日期说明
2016-04-24初始版本

该模块自 2016 年随 F´ 早期版本(当时写作 ISF)引入,核心设计——根类 + 可选注册表——沿用至今,是 F´ 对象体系稳定性的直接体现。仓库中ObjBase.hpp/.cpp的版权头(Copyright 2016, California Institute of Technology)与文档时间线一致,进一步印证了其设计演进脉络。

  • 嵌入式
  • 系统编程

【免费下载链接】fprime

F´ - A flight software and embedded systems framework

项目地址:https://gitcode.com/gh_mirrors/fpri/fprime
点击查看免费下载

相关推荐

上一篇:Folia Electron架构剖析:主进程、预加载与渲染器分工指南
下一篇:Nix 官方 Docker 镜像使用与定制指南:从拉取运行到非 root 构建

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询