- 3.2:改为 4 个类型有 getInterfaces 重写(DLD_FZ/DMF_56/YUANXINGGUAN/FANGXINGXIANG),补全 m_interfaces 四个写入点,删除 ent2db 未纳入支持列表的错误说法,新增 dwgIn 读入接口被工厂 getInterfaces 覆盖的顺序缺陷 - M1/M2/M3:修正修法——工厂改为先建后替(default 分支不能先删)、copyFrom2 判空、copyFrom 走基类后仅在 impl 为空时补建(基类 CopyObject 会经 dwgInFields 重建) - M7:补 XL 的 %d 与 double 实参 ABI 错配;M8:补 ParamEquImpl.h:42 同名宏与 IMP_XDATA_KEY 说明 - 修正参考行号(AecDbParamEquipment.cpp:162)、子类计数(46=45+1)、验证 preset(R230→R190)与 cmake 命令语法;新增 L6 与 subTransformBy 接口未跟随移动
17 KiB
XEquipment(外壳 / Impl 分离机制)分析与重构计划
分析对象:
AecDbXEquipment(外壳,AcDbEntity派生) +XEquipmentImplBase及其 46 个子类(45 个已接入工厂 + 1 个死类 XICHENZHAO_I,Impl 策略层)。 数据流:全部参数化数据存于基类m_mapSpecXData(AecDbObjXDataMap),Impl 通过m_pXDataMap指针反向读写,不拥有数据。 核对基线:main @ 20dccfa。文中行号已按该版本逐条核对;带“核对”字样的条目为本次复核后修正的内容。
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 基类 + 46 个子类声明 + 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。要区分两类调用:inplace 编辑改参数走的是“对图中已有实体二次调用”(ParamEquJson.cpp:345/2015/2186 等),旧 impl 每次泄漏;而 311/8604/9526 是 new 出新对象后的首次调用,m_pImpl 为 NULL,不构成泄漏,不能混为一谈。impl 对象小(一个 map 指针 + 虚表),每次几十字节,但每次编辑都漏,长时间作业会累积,且形成“赋值即丢失”的坏模式。修:不要在“函数入口”直接 DELETE_PTR——switch 的 default 分支会 return NULL,先删会把原本可用的 impl 弄没。正确做法是先 new 出 pImp、走完 switch,再在 :650 处 DELETE_PTR(m_pImpl); m_pImpl = pImp;。
M2(中)copyFrom2 同样不释放旧 impl。当前唯一调用点 ToJson 作用于 clone() 出的新对象所以没暴露,但它是 public API。修法同 M1(先克隆成功再替换);另注意 cloneImpl 在源对象无 impl 时返回 NULL(i_AecDbXEquipment.cpp:572-577),替换前要判空,避免把已有 impl 换成 NULL。
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 里先调 AecDbBDPipeBase::copyFrom(pSrc)。注意基类链里的 CopyObject(AecDbEwPipeBase.cpp:1529)是 dwgOut→dwgIn 往返,虚分发会重新进入 AecDbXEquipment::dwgInFields,那里已经 DELETE_PTR(m_pImpl) + createParamEquImpl(GetType(), false)(:170-178);所以基类返回后 impl 通常已重建,只需 if (!m_pImpl) createParamEquImpl(GetType(), false); 兜底,不要再无条件删一次(否则每次 clone 多建一次几何)。可参考兄弟类 AecDbParamEquipment::copyFrom(AecDbParamEquipment.cpp:162)的“拷 XDataMap + cloneImpl”写法。
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 字节)不一致,过度分配不溢出,但说明这个手写变参没人审。另有同类 ABI 错配:XL 的 num 声明为 double 却用 %d 取(:3677/3692),va_arg(vlist, int) 在 x64 上读的是从未赋值的通用寄存器,同样是 UB。修:调用点统一传 (LPCTSTR)str;sizeof(TCHAR) 改 sizeof(TCHAR);XL 的 %d 改 %f 或把 num 改成 int;或干脆重写为类型安全签名。
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 个参数等)。注意宏在 CoreLibrary/ParamEquImpl.h:42 还有一份完全相同的定义(AecDbParamEquipment 用),只改 XEquipment 这份等于没修干净;键本身由 IMP_XDATA_KEY(inc/define_class_xdata_key.h:5)定义,不需要补定义,只需初始化值并判空。
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 基类是空实现(i_XEquipmentImpl.cpp:9736)。45 个已接入工厂的子类中只有 4 个重写:DLD_FZ(:8843)、DMF_56(:9038)、YUANXINGGUAN(:9538)、FANGXINGXIANG(:9630);其余 41 个类型调用它不产生任何接口(另有死类 XICHENZHAO_I 也未重写)。
- m_interfaces 的写入点共四处:dwgInFields(版本 2 以上从 dwg 读)、clearInterface(i_AecDbXEquipment.cpp:867,inplace 每次调用)、createParamEquImpl(:654)与 draw3dByImp(:745)里的 getInterfaces。
- 后果:(a) 41 个无重写的类型,命令新建设备时 m_interfaces 为空,管嘴夹点、管嘴捕捉拿不到接口(ent2db.cpp:379 已把 AecDbXEquipment 纳入导出列表,转换侧本身没问题);(b) inplace 编辑先 clearInterface 再 createParamEquImpl,41 个类型编辑一次就把从图纸读到的管嘴清空;(c) 版本 1 旧图(当时“读取 json 动态生成”)升级后接口丢失,无迁移代码从 SRC_STRING JSON 重建。
- 另有顺序缺陷:dwgInFields 先 append 读到的接口(:163-175),再 createParamEquImpl(type,false)(:177-178),而工厂末尾会调 getInterfaces(:654)。DLD_FZ/DMF_56/FANGXINGXIANG 的实现是 removeAll 后重建,等于把刚序列化读入的管嘴丢掉(只有 YUANXINGGUAN 用 if (!infs.isEmpty()) return 挡住)。修工厂时要一并解决:getInterfaces 前判空,或给 createParamEquImpl 加 bFromDwg 开关。
- 建议:明确管嘴来源策略——要么给 41 个类型补 getInterfaces 按参数生成(虚函数位置本就是为这个留的),要么在 dwgIn version==1 分支从 JSON 迁移;同时修掉上面的覆盖顺序;并补“新建、保存、重开、inplace 编辑”的管嘴一致性用例。
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。
- subTransformBy 里接口变换被 if (m_pImpl) 包住(i_AecDbXEquipment.cpp:268-285):impl 缺失时 m_interfaces 不跟随移动,管嘴位置与设备参数脱节。建议把接口变换移出该判断。
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 参数数组取项(i_XEquipmentImpl.cpp:812-820 直接 all[idx]),服务端参数顺序一变就取错值且无校验。建议改按 name 查找。
- L6 copyFrom 只拷 m_interfaces(:243-256),不拷 m_nInfActive 等活动管嘴状态,clone 后“当前激活接口”丢失。
4. 重构计划(分阶段,增量推进)
遵循仓库既有约定:每次只改一小批、两个 base(AecDbEwPipeBase / AecDbBDPipeBase)零改动、只编 R190 Debug 验证。
P0 安全修复(低风险高价值,先做)
- M7:5 处 GET_UNIQUE_NAME 的 CString 实参改传 (LPCTSTR);getUniqueName 的 sizeof(TCHAR*) 改 sizeof(TCHAR)。
- M8:DEF_KEY_FUNC 宏值初始化 + m_pXDataMap 判空(XEquipmentImpl.h:50 与 ParamEquImpl.h:42 两份都要改)。
- M1/M2:createParamEquImpl 改为“先建 pImp、成功后替换”(不要入口删,default 分支会丢 impl);copyFrom2 判空 cloneImpl 结果后再替换。
- L1:SetProperties 返回值修正。
- M10:GetType 失败日志。
P1 正确性修复(需回归测试)
- M3:copyFrom 补基类调用 + impl 重建(重点回归:CAD 内 copy/paste、clone、ToJson 提资)。
- M5:draw3dByImp 临时数组 + 失败回退。
- 3.2 管嘴来源策略落地(impl getInterfaces 重写 或 v1 JSON 迁移,先与需求方确认)。
- 3.3 z 语义决策(恢复 pt.z=z 或改注释),回归移动/标高用例。
- 3.4 add2Parts 轴改世界坐标(回归 UCS 非默认时创建设备)。
P2 结构优化(大工程,单独排期)
- 工厂注册表化(消灭 enum/switch/impl 三处同步)。
- part 级几何缓存(消费 getUniqueName),打开大 dwg 性能收益的主要来源。
- L5 GetSpec 改按名查找;L4 投影缓存。
- 死代码清理:XICHENZHAO_I、CHANSHUDIBAN/YUANXINGDIBAN(确认后删)。
- M4:impl 的 m_pXDataMap 访问收敛 + 契约注释。
验证方式
编译:cmake --preset R190 && cmake --build --preset R190 --config Debug --target CoreLibrary_BAL_REMOTE;涉及工厂/接口改动时追加 --target Pool3dCmd(CoreImplement 按 CODEBUDDY.md 的 Debug 约定一起编)。功能回归覆盖:新建各类型设备、保存、重开、inplace 编辑、移动/复制、夹点、剖切、ToJson 提资、HLR;内存方面做长时间 inplace 编辑会话观察内存曲线(验证 M1/M2 泄漏消失),并单独核对“保存后重开,4 个有 getInterfaces 重写的类型管嘴数量是否与保存前一致”(验证 3.2 的覆盖顺序缺陷)。