新增 XEquipment 外壳/Impl 机制分析与重构计划文档

This commit is contained in:
gjm committed 2026-10-25 23:10:15 +08:00
1 parent d329428791
commit aabbd3cc1d
1 file changed
+134
+134
View File
@@ -0,0 +1,134 @@
# XEquipment(外壳 / Impl 分离机制)分析与重构计划
> 分析对象:`AecDbXEquipment`(外壳,`AcDbEntity` 派生) + `XEquipmentImplBase` 及其 45 个设备子类(Impl 策略层)。
> 数据流:全部参数化数据存于基类 `m_mapSpecXData`(`AecDbObjXDataMap`),Impl 通过 `m_pXDataMap` 指针反向读写,不拥有数据。
## 0. 涉及文件
| 文件 | 行数 | 职责 |
| --- | --- | --- |
| `inc/implement/AecDbXEquipment.h` | ~280 | 外壳类声明(设备类型 enum + 成员声明) |
| `CoreLibrary/AecDbXEquipment.cpp` | 6 | 仅 #include 分发:NDEBUG 时包含实现文件,否则链 CoreImplement.lib |
| `CoreLibrary/Implement/i_AecDbXEquipment.cpp` | 1001 | 外壳实现(构造/析构/dwgIn-Out/copyFrom/夹点/绘制) |
| `CoreLibrary/Implement/XEquipmentImpl.h` | 458 | Impl 基类 + 45 个子类声明 + DECLARATION_SUBCLASS / DEF_KEY_FUNC 宏 |
| `CoreLibrary/Implement/i_XEquipmentImpl.cpp` | 9832 | Impl 实现(匿名 namespace 几何工具 + 各设备 draw3d / getUniqueName / GetSpec) |
关联设施:`inc/AecDbEwPipeBase.h/.cpp`(m_mapSpecXData、m_pGripAppData 数组、管嘴基类设施)、`AecRxBase/AecDbObjXDataMap.cpp`(GetAt 语义)、`Pool3dCmd/ParamEquJson.cpp`(createParamEquImpl 的外部调用方)。
## 1. 机制概述(现状)
数据与行为彻底分离:外壳 `AecDbXEquipment : AecDbBDPipeBase : AecDbEwPipeBase : ... : AcDbEntity`。
- 工厂:createParamEquImpl(type, bRedraw) 用 _CREATE_ 宏 switch 49 个 case,new 出 X<组>::<类>(&m_mapSpecXData),随后 setType 重写 KEY_TYPE、getInterfaces、可选 draw3dByImp()。
- 绘制链:draw3dByImp() -> RemoveAllX3dPart() -> impl->draw3d(AcDbVoidPtrArray&) -> 各设备用匿名 namespace 几何工具(makeXSolid_by_point / makeXSolid_by_region / drawXCylinder / drawXRevolved / add2Parts 等)生成 AcDbX3dSolid*,包装成 AecDbX3dPart*,所有权经 ar 转移给外壳 m_arX3DPartPtrs 缓存。Draw3d / GetSectionDraw / subGetGeomExtents / HLR 全部从这个 3D buffer 取数。
- 克隆:cloneImpl(pXData) -> m_pImpl->makeSelf(pXData)(每个子类 new 同类型对象、指向新 xdata map)。
- 序列化:dwgOutFields 只写 version + m_interfaces,参数全靠基类 m_mapSpecXData 的 dwgIn/Out;dwgInFields 读完后 DELETE_PTR(m_pImpl) 再按 GetType() 重建。dxfIn/dxfOut 均为空实现。
- copyFrom2 才是“真正的深拷贝”(拷 xdata + 克隆 impl);虚函数 copyFrom 只拷 m_interfaces。
- getUniqueName:impl 侧把全部参数格式化后 MD5,外壳再拼上 X3dPart 数量。缓存键的基础设施已存在,但当前没有缓存层消费它。
## 2. 内存管理分析
### 2.1 m_pImpl 生命周期(new/delete 配对)
| 位置 | 操作 | 评价 |
| --- | --- | --- |
| ~AecDbXEquipment | DELETE_PTR(m_pImpl) | 正确 |
| dwgInFields | DELETE_PTR 后重建 | 正确 |
| copyFrom2 | m_pImpl = cloneImpl(...),无 DELETE | M2 |
| createParamEquImpl 内部 | m_pImpl = pImp,无 DELETE | M1 |
| ToJson | clone() + copyFrom2 | 新对象 m_pImpl==NULL,暂时安全 |
M1(中)createParamEquImpl 覆盖前不释放旧 impl。ParamEquJson.cpp:311/345/8604/9526/1967/2015/2114/2138/2186 等多处对“已在图中、已带 impl”的设备二次调用(inplace 编辑改参数就是这条路),旧 impl 每次泄漏。impl 对象小(仅一个指针成员),每次几十字节,但每次编辑都漏,长时间作业会累积,且形成“赋值即丢失”的坏模式。修:函数入口加 DELETE_PTR(m_pImpl)。
M2(中)copyFrom2 同样不释放旧 impl。当前唯一调用点 ToJson 作用于 clone() 出的新对象所以没暴露,但它是 public API。修法同 M1。
M3(高)copyFrom 切断基类拷贝链且不重建 impl。AecDbXEquipment::copyFrom 只做 m_interfaces = pSrcEqu->m_interfaces,没有调用 AecDbBDPipeBase::copyFrom(基类链 AecDbBDPipeBase::copyFrom -> AecDbEwPipeBase::copyFrom -> AecDbCurveBase::copyFrom + CopyObject 负责深拷 m_mapSpecXData 等),也没有重建 impl。后果:任何走 copyFrom 的路径(AcDbEntity::clone() 就是 create+copyFrom;部分 reactor / 夹点编辑路径)得到的设备是空壳:KEY_TYPE 丢失(GetType()=0)、m_pImpl=NULL、X3dPart 空。目前只有 ToJson 用 clone() 且紧跟 copyFrom2 兜底 + 手动补 X3dPart 才没爆。修:copyFrom 里先调基类,再 DELETE_PTR(m_pImpl) + createParamEquImpl(GetType(), false)。参考兄弟类 AecDbParamEquipment::copyFrom(AecDbParamEquipment.cpp:176)的写法。
M4(低,契约风险)impl 与 xdata 的所有权分离是隐式契约。impl 只存 AecDbObjXDataMap*(指向外壳 m_mapSpecXData 地址)。makeSelf 克隆时传入新外壳 map 指针,契约成立;但任何“把 impl 指针直接搬给另一个外壳”的做法都会读写旧外壳数据。目前无此路径,属潜在雷区。建议在 XEquipmentImplBase 定义处注释固化契约。
M5(中)draw3d 中途失败时缓存处于“部分更新”状态。draw3dByImp 先 RemoveAllX3dPart 再让 impl 往里 add;impl 的 draw3d 里有大量 if (!pSolid) return;(如 XGC 参数不足、TESTXDATA 失败直接 return),此时部分 part 已入缓存、其余丢失,设备显示残缺且无任何提示。修:draw3dByImp 改用临时数组,成功后一次性提交,失败保留旧缓存并记日志。
M6(低)ToJson 的 clone+copyFrom2+手动补 part 流程脆弱。i_AecDbXEquipment.cpp:770-810:clone()(此刻是空壳)-> copyFrom2(补 xdata+impl)-> Clean3DBuffer -> 手动把 this 的 X3dPart CopyFrom 到 p -> 移原点 -> 转方向 -> 又把 Pos/Dir 写回 xdata 抵消前面的移动。步骤间有隐含依赖。建议抽 makeTempClone() 辅助函数集中管理并注释“为什么不能直接用 clone()”。
M7(高,UB)getUniqueName 的变参 %s 分支是未定义行为。匿名 namespace 的 getUniqueName(i_XEquipmentImpl.cpp:455-540)手写 va_list 模拟 MD5 输入:%s 分支 ss = va_arg(vlist, TCHAR *),而 5 个调用点传的是 CString 对象:XGC:2287、BS:3154、XL:3692、GUANXIANG_I:8747、YUANXINGLABAKOU:9730。CString 是非平凡类型,MSVC x64 下经隐藏引用传递,va_arg(TCHAR*) 读到的是对象地址/内部指针字节而非字符串缓冲区,哈希输入是不确定的垃圾字节,可能读到非法内存。另 mem_size += ss.GetLength() * sizeof(TCHAR *)(每字符 8 字节)与实际写入 sizeof(TCHAR)(2 字节)不一致,过度分配不溢出,但说明这个手写变参没人审。修:调用点统一传 (LPCTSTR)str;sizeof(TCHAR*) 改 sizeof(TCHAR);或干脆重写为类型安全签名。
### 2.2 XDataMap 读取语义相关
M8(高)DEF_KEY_FUNC 宏返回未初始化值。宏体是 type v; m_pXDataMap->GetAt(key, v); return v;。实证 AecDbObjXDataMap::GetAt(AecDbObjXDataMap.cpp:1125/1163/1216/1245/1293)只在“键存在且 nVt 类型匹配”时才写出参,否则原样返回 FALSE。于是键缺失/类型不符时 GetH1() 之类返回栈上未初始化的 double / AcGePoint3d / AcGeVector3d,NaN 或垃圾坐标直接进入 sin/cos/normalize/setToAlignCoordSys,产出远离原点的实体或让 ASM 布尔失败。修:宏内值初始化(type v = {};)并对 m_pXDataMap 判空。影响所有 DEF_KEY_FUNC getter(XHG 14 个、SG 13 个、ZG 10 个参数等)。
M9(低)m_pXDataMap 未判空。draw3d 大都有 if (!m_pXDataMap) return;,但 getter 没有;外部若用 NULL map 构造 impl,getter 直接崩。修:getter 判空返回默认值。
M10(低)GetType() 静默失败返回 0。int nType = 0; GetAt(KEY_TYPE, nType); 键缺失/类型不符则返回 0,createParamEquImpl(0) 走 default,m_pImpl=NULL,设备变空壳(后续都有判空不崩,但无声失败)。建议失败时 acutPrintf 日志。
### 2.3 夹点缓存(核查结论:安全,无需改动)
subGetGripPoints 把 m_pGripAppData 当数组写下标 i。基类 AecDbEwPipeBase 构造里 m_pGripAppData = new AecGripData[GRIP_LENGTH](256,AecDbEwPipeBase.cpp:406),析构 delete[](:425),且有 count >= GRIP_LENGTH 前置检查,不越界。仅建议把 GRIP_LENGTH 检查挪到更明显的位置(现在 count > eCommand19 - eCommand0 的二次裁剪逻辑有点绕)。
## 3. 代码逻辑分析
### 3.1 type 到 impl 工厂的覆盖缺口与同步成本
- header 21 个枚举组,switch 覆盖 18 组 49 case:
- FLUME(PARSHALL 水槽)、WEIR(三角堰/储留孔堰/包壁堰)无 impl 类,预期由独立实体类(AecDbRiverGroove 等)处理,确认无遗漏即可;
- CHANSHUDIBAN::YUANXINGDIBAN 有枚举、无 impl 类、无 case,死枚举,创建时 default 返回 NULL;
- XICHENZHAO_I impl 类存在(XEquipmentImpl.h:408)但无对应枚举组、无 case,死代码。
- 新增设备要同步维护三处(enum 链、switch case、impl 类),易漏。建议改注册表:static std::map<int, FnMaker> + 各命名空间静态注册对象,消灭手工同步;若维持 switch,至少 default: assert(false) 加日志。
- _CREATE_ 宏每个 case 都 setType(&m_mapSpecXData, type) 重写 KEY_TYPE:dwgIn 场景多余;inplace 场景会静默把 type 改成传入值,调用方传错 type 会污染 xdata 且无感知。建议宏里去掉 setType,由调用方显式 SetType。
### 3.2 管嘴(m_interfaces)生命周期缺口
- impl 侧 getInterfaces 基类是空实现且 45 个子类无一重写;createParamEquImpl / draw3dByImp 调用它但不产生任何接口。
- 全仓库对 AecDbXEquipment::m_interfaces 的写入只有 dwgInFields 一处(版本 2 以上从 dwg 读)。
- 后果:(a) 命令新建设备(未经 dwg 往返)m_interfaces 为空,管嘴夹点、管嘴捕捉、ent2db 转换(ent2db.cpp 甚至没把 AecDbXEquipment 纳入支持列表)都拿不到接口;(b) 版本 1 旧图(当时“读取 json 动态生成”)升级后接口丢失,无迁移代码从 _SRC_STRING_ JSON 重建。
- 建议:明确管嘴来源策略,要么在各设备 impl 重写 getInterfaces 按参数生成(虚函数位置本就是为这个留的),要么在 dwgIn version==1 分支从 JSON 迁移;并补“新建、保存、重开”的管嘴一致性用例。
### 3.3 变换语义
- subTransformBy 调 m_pImpl->transformBy(xform)(基类实现只更新 xdata 的 Pos):注释写“只移动X Y”但 pt.z = z; 被注释掉,实际 z 也变换,3D 移动/旋转会改标高,与注释矛盾。随后 AecDbBDPipeBase::subTransformBy 变换 X3dPart 缓存,两者各管一半(参数 vs 缓存),不重复,设计合理;需要决策的是 z 的语义:建议恢复 pt.z = z 或改注释,并与 SetElev 对齐。
- ToJson 里 vtDir.angleTo(kXAxis, kZAxis):vtDir 为零向量时行为未定义,SetPosAndDir / GetInsert 都不校验 Dir。
### 3.4 绘制性能(与 bit1.diagsession 分析呼应)
- draw3dByImp 每次参数变化全量重画:RemoveAllX3dPart + impl->draw3d 重建全部 part(每个 part 是 region 到 extrude/loft/revolve/sweep 的 ASM 运算)。大图纸 N 设备乘每设备 10 到 40 个 part,打开/编辑时 ASM/tbb 占用显著(此前 diagsession 分析中 tbb 25.5% 采样的大头)。
- getUniqueName(参数 MD5 加 part 数)已提供缓存键基础设施但没有消费方。建议 part 级缓存(按 uniqueName 失效),或参数编辑时按“受影响 part”增量重画(二期)。
- add2Parts 用 acdbCurDwg()->ucsxdir()/ucsydir() 作为 part 的轴:设备加入非当前 dwg(块/外部参照)或之后改 UCS 再重画,轴的语义会漂移。建议 part 轴改用世界坐标或实体自身数据库的 UCS。
### 3.5 其它逻辑问题
- L1 SetProperties 恒返回 false:即使成功设置“标高”也 return false,调用方按返回值判断成败会被误导。应返回 true。
- L2 dxfIn/dxfOut 空实现:DXF 读写静默丢失全部数据。若无需求建议注释标明“不支持 DXF”。
- L3 GetDraw2dables 两个分支的所有权:与基类行为一致暂无 double-free,建议加注释标明所有权约定。
- L4 GetSectionDraw 每次重建投影:3D buffer 空时触发全量重画再投影,剖切命令反复调用开销大,可按 uniqueName 缓存。
- L5 GetSpec 的魔法下标:pickItemInsert(all, spec, 8/12/3/21/23...) 按固定下标从 JSON 参数数组取项,服务端参数顺序一变就取错值且无校验。建议改按 name 查找。
## 4. 重构计划(分阶段,增量推进)
遵循仓库既有约定:每次只改一小批、两个 base(AecDbEwPipeBase / AecDbBDPipeBase)零改动、只编 R230 Debug 验证。
### P0 安全修复(低风险高价值,先做)
1. M7:5 处 GET_UNIQUE_NAME 的 CString 实参改传 (LPCTSTR);getUniqueName 的 sizeof(TCHAR*) 改 sizeof(TCHAR)。
2. M8:DEF_KEY_FUNC 宏值初始化 + m_pXDataMap 判空。
3. M1/M2:createParamEquImpl、copyFrom2 入口 DELETE_PTR(m_pImpl)。
4. L1:SetProperties 返回值修正。
5. M10:GetType 失败日志。
### P1 正确性修复(需回归测试)
6. M3:copyFrom 补基类调用 + impl 重建(重点回归:CAD 内 copy/paste、clone、ToJson 提资)。
7. M5:draw3dByImp 临时数组 + 失败回退。
8. 3.2 管嘴来源策略落地(impl getInterfaces 重写 或 v1 JSON 迁移,先与需求方确认)。
9. 3.3 z 语义决策(恢复 pt.z=z 或改注释),回归移动/标高用例。
10. 3.4 add2Parts 轴改世界坐标(回归 UCS 非默认时创建设备)。
### P2 结构优化(大工程,单独排期)
11. 工厂注册表化(消灭 enum/switch/impl 三处同步)。
12. part 级几何缓存(消费 getUniqueName),打开大 dwg 性能收益的主要来源。
13. L5 GetSpec 改按名查找;L4 投影缓存。
14. 死代码清理:XICHENZHAO_I、CHANSHUDIBAN/YUANXINGDIBAN(确认后删)。
15. M4:impl 的 m_pXDataMap 访问收敛 + 契约注释。
### 验证方式
编译 cmake --preset R230 --config Debug(CoreLibrary/CoreImplement + Pool3dCmd);功能回归覆盖 新建各类型设备、保存、重开、inplace 编辑、移动/复制、夹点、剖切、ToJson 提资、HLR;内存方面做长时间 inplace 编辑会话观察内存曲线(验证 M1/M2 泄漏消失)。