本文分析FreeCAD中Extension机制的实现。
注1:限于研究水平,文中分析难免有不当之处,欢迎批评指正。
注2:本文档将不定期更新。
一、机制概览
FreeCAD 的文档对象(App::DocumentObject)需要同时支持几何、分组、链接、附件、模式、求解器、可见性等能力。若把所有能力都放进一个继承层次,会造成类数量爆炸、基类臃肿和功能耦合。Extension 机制把“可选能力”拆成可组合的扩展对象:宿主对象负责生命周期、属性和调度;扩展对象负责一组相对独立的领域行为。
Extension 是一种对象级组合(composition)机制,不是动态库插件加载机制,也不是 C++ 多继承的替代语法。一个宿主可以同时挂载多个扩展,扩展通过ExtensionContainer被统一注册、查找、遍历,并参与属性访问、序列化和文档对象生命周期。
典型关系如下:
App::DocumentObject └─(继承/包含 ExtensionContainer 的能力) ├─ GroupExtension ├─ LinkBaseExtension ├─ AttachExtension └─ 其他 DocumentObjectExtension 派生类二、设计动机与目标
2.1 解决继承树膨胀
某个对象可能既是可分组对象,又支持链接和附件。用继承表达所有组合会产生大量“组合类”;扩展允许按需挂载能力,新增能力通常只需新增一个 Extension 类。
2.2 保持核心对象稳定
DocumentObject保留通用的名称、标签、表达式、可见性、文档归属等基础职责;领域特性放在独立扩展中,降低修改核心类带来的回归风险,也方便不同工作台复用同一扩展。
2.3 统一 App/Gui/Python 边界
扩展既可承载数据和计算行为,也可提供 Python 包装对象;DocumentObjectExtension还能声明对应的 ViewProvider Extension 类型。这样,应用层能力可以与 GUI 表现、Python 自动化保持一致的扩展点。
2.4 让属性和文件格式可组合
扩展的属性通过宿主的PropertyContainer接口暴露,用户看到的属性表不需要知道属性来自哪个扩展。ExtensionContainer还负责扩展数据的保存与恢复,使扩展状态能随文档持久化。
三、核心设计原理
3.1 宿主、容器、扩展三层模型
- 宿主(Host):通常是
DocumentObject,拥有对象身份、文档关系和公共属性。 - 容器(Container):
App::ExtensionContainer维护扩展映射,并把扩展属性聚合到宿主的属性接口。 - 扩展(Extension):
App::Extension派生类保存可选能力;扩展通过getExtendedContainer()访问宿主。
ExtensionContainer内部以Base::Type为键保存Extension*,因此一个宿主可按类型精确查找,也可按基类查找所有派生扩展。
3.2 FreeCAD 类型系统,而非 RTTI
Extension使用EXTENSION_TYPESYSTEM_*宏注册 FreeCADBase::Type。派生类在init()中调用initExtensionSubclass(),建立父子类型关系和工厂函数。容器的getExtension()、hasExtension()支持按类型、名称和“是否允许派生类型”查询。
这种类型系统同时服务于 C++、Python 包装和文档恢复:序列化数据中的扩展类型名可以映射回已注册的类型并创建对象。
3.3 组合属性与属性链
扩展使用EXTENSION_PROPERTY_HEADER/EXTENSION_PROPERTY_SOURCE声明属性元数据。扩展初始化时,Extension::initExtension()会遍历扩展属性,把每个属性的容器设置为宿主,然后调用registerExtension()。因此扩展属性可以通过宿主的getPropertyByName()、getPropertyList()、getPropertyMap()等接口访问。
扩展属性元数据通过PropertyData::parentPropertyData形成继承链,既保留扩展自身属性,也能查到父扩展属性。
3.4 运行时委托,而不是隐式魔法
DocumentObject在执行、恢复、设置文档、建立/拆除对象等阶段,显式遍历getExtensionsDerivedFromType<DocumentObjectExtension>(),逐个调用对应回调。扩展只在实现了相关能力时覆盖虚函数,默认实现保持空操作或返回成功。
四、核心组件
4.1App::Extension
文件:src/App/Extension.h/.cpp
职责包括:
- 保存扩展类型
Base::Type与宿主指针; initExtension()完成属性绑定和注册;- 通过
name()、getExtensionTypeId()提供类型信息; - 将扩展属性转发给
PropertyData; - 通过
getExtensionPyObject()延迟创建 Python 包装对象; - 提供属性名称/类型变更的兼容性回调。
未设置扩展类型时,初始化和取名会抛出Base::RuntimeError,这是防止扩展以不完整状态进入容器的保护。
4.2App::ExtensionContainer
文件:src/App/ExtensionContainer.h/.cpp
它是扩展的注册表和属性聚合器,主要接口包括:
registerExtension(type, ext):注册扩展;hasExtension()/getExtension():按类型或名称查询;getExtensionsDerivedFrom():取得某基类下的所有扩展;extensionBegin()/extensionEnd():遍历扩展;getPropertyByName()、getPropertyList()等:聚合扩展属性;Save()/Restore():保存和恢复扩展数据。
注册时会校验扩展的类型关系和宿主关系,避免把不属于该容器的对象登记进去。
4.3App::DocumentObjectExtension
文件:src/App/DocumentObjectExtension.h/.cpp
这是面向文档对象的专用基类,提供:
getExtendedObject():取得DocumentObject宿主;extensionMustExecute()/extensionExecute():参与重计算;onExtendedSettingDocument():对象设置文档后回调;onExtendedDocumentRestored():从文件完整恢复后回调;onExtendedSetupObject()/onExtendedUnsetupObject():创建和移除阶段回调;- 子对象、链接对象和元素可见性的扩展点;
getViewProviderExtensionName():声明自动附着的 GUI 扩展类型。
4.4DocumentObject中的调度点
DocumentObject::executeExtensions()会收集所有DocumentObjectExtension派生扩展并调用extensionExecute();对象重计算流程还会根据扩展的extensionMustExecute()判断是否需要执行。文档设置、恢复、对象建立和拆除等流程也在DocumentObject.cpp中统一转发到扩展回调。
五、关键流程
5.1 类型注册(程序启动/模块初始化)
- FreeCAD 初始化
App::Extension基类类型。 - 各模块调用派生扩展的
init()。 - 宏生成的代码通过
Base::Type::createType()注册名称、父类型和创建函数。 - 后续可按类型名称恢复或创建扩展。
5.2 创建并挂载扩展
典型 C++ 过程可概括为:
autoext=newMyExtension;ext->initExtensionType(MyExtension::getExtensionClassTypeId());ext->initExtension(host);// 绑定属性、保存宿主、注册到容器实际项目通常在对象构造或工厂函数中完成创建,并由宿主/容器负责生命周期。initExtension()会先设置属性容器,再登记到容器映射;类型未初始化时会直接报错。
5.3 属性访问与变更
- 用户或脚本通过宿主名称访问属性。
ExtensionContainer在自身属性和扩展属性中查找。- 找到扩展属性后,属性的容器仍指向宿主,因此变更通知、事务和文档脏标记沿用宿主机制。
- 扩展可在自身
onChanged()或宿主相关回调中处理变化。
5.4 重计算
- 文档将对象加入重计算队列。
DocumentObject判断自身逻辑以及扩展是否需要执行。executeExtensions()按注册的扩展集合调用extensionExecute()。- 任一扩展返回错误对象时,重计算结果带出错误原因;返回
DocumentObject::StdReturn表示成功。
5.5 保存与恢复
保存时,ExtensionContainer::Save()将扩展数据写入对象 XML,并记录扩展类型。恢复时,容器读取类型名,查找已注册类型,创建或定位对应扩展,再调用扩展属性的Restore()。未知扩展或版本变化可通过extensionHandleChangedPropertyName()/extensionHandleChangedPropertyType()参与兼容处理。
5.6 Python 访问
第一次请求扩展的 Python 对象时,getExtensionPyObject()延迟创建包装对象并缓存引用。C++ 扩展与ExtensionPython派生扩展使用不同的 Python 包装类;扩展析构时会使包装对象失效,避免 Python 仍持有已销毁 C++ 对象。
六、应用场景
- 分组能力:
GroupExtension为对象提供组成员管理和树结构行为。 - 链接能力:
LinkBaseExtension统一处理被链接对象、递归解析和变换矩阵。 - 附件/定位:Part 工作台的
AttachExtension为对象提供支持面、偏移和坐标系相关能力。 - 几何或显示辅助:几何、预览、纹理、网格缩略图等能力可独立为扩展,避免污染基础对象。
- 求解器/工作台专用能力:Sketcher、FEM、TechDraw 等模块通过各自 Extension 增加执行、数据或显示行为。
- Python 原型和自动化:Python 扩展可快速给对象增加属性和行为,并通过同一容器机制被 C++/Python 查询。
- 兼容旧文件:扩展自带属性和恢复回调,适合在不改变宿主主类的情况下演进文件格式。
参考资料
src/App/Extension.h、src/App/Extension.cpp:基类、类型注册、属性转发、Python 包装。src/App/ExtensionContainer.h、src/App/ExtensionContainer.cpp:注册表、查找、属性聚合、序列化。src/App/DocumentObjectExtension.h、src/App/DocumentObjectExtension.cpp:文档对象生命周期与执行回调。src/App/DocumentObject.h、src/App/DocumentObject.cpp:扩展容器继承关系、重计算和生命周期调度。src/App/GroupExtension.*、src/App/LinkBaseExtension.*、src/Mod/Part/App/AttachExtension.*:典型应用实现。src/App/*ExtensionPyImp.cpp、*.pyi:Python 包装和类型提示。