Merge pull request '修正 XEquipment 重构计划的核对错误并补充遗漏项' (#74) from company-desktop-computer/xequipment-plan-fix into main
Reviewed-on: #74 Reviewed-by: gjm <littleoldpan@163.com>
This commit was merged in pull request #74.
This commit is contained in:
commit
19131f3055
1 file changed
+20
-16
+20
-16
@@ -1,7 +1,8 @@
|
||||
# XEquipment(外壳 / Impl 分离机制)分析与重构计划
|
||||
|
||||
> 分析对象:`AecDbXEquipment`(外壳,`AcDbEntity` 派生) + `XEquipmentImplBase` 及其 45 个设备子类(Impl 策略层)。
|
||||
> 分析对象:`AecDbXEquipment`(外壳,`AcDbEntity` 派生) + `XEquipmentImplBase` 及其 46 个子类(45 个已接入工厂 + 1 个死类 XICHENZHAO_I,Impl 策略层)。
|
||||
> 数据流:全部参数化数据存于基类 `m_mapSpecXData`(`AecDbObjXDataMap`),Impl 通过 `m_pXDataMap` 指针反向读写,不拥有数据。
|
||||
> 核对基线:main @ 20dccfa。文中行号已按该版本逐条核对;带“核对”字样的条目为本次复核后修正的内容。
|
||||
|
||||
## 0. 涉及文件
|
||||
|
||||
@@ -10,7 +11,7 @@
|
||||
| `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/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 的外部调用方)。
|
||||
@@ -38,11 +39,11 @@
|
||||
| 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)。
|
||||
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。
|
||||
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 里先调基类,再 DELETE_PTR(m_pImpl) + createParamEquImpl(GetType(), false)。参考兄弟类 AecDbParamEquipment::copyFrom(AecDbParamEquipment.cpp:176)的写法。
|
||||
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 定义处注释固化契约。
|
||||
|
||||
@@ -50,11 +51,11 @@ M5(中)draw3d 中途失败时缓存处于“部分更新”状态。draw3dBy
|
||||
|
||||
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);或干脆重写为类型安全签名。
|
||||
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 个参数等)。
|
||||
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 判空返回默认值。
|
||||
|
||||
@@ -77,15 +78,17 @@ subGetGripPoints 把 m_pGripAppData 当数组写下标 i。基类 AecDbEwPipeBas
|
||||
|
||||
### 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 迁移;并补“新建、保存、重开”的管嘴一致性用例。
|
||||
- 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 分析呼应)
|
||||
|
||||
@@ -99,17 +102,18 @@ subGetGripPoints 把 m_pGripAppData 当数组写下标 i。基类 AecDbEwPipeBas
|
||||
- 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 查找。
|
||||
- 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)零改动、只编 R230 Debug 验证。
|
||||
遵循仓库既有约定:每次只改一小批、两个 base(AecDbEwPipeBase / AecDbBDPipeBase)零改动、只编 R190 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)。
|
||||
2. M8:DEF_KEY_FUNC 宏值初始化 + m_pXDataMap 判空(XEquipmentImpl.h:50 与 ParamEquImpl.h:42 两份都要改)。
|
||||
3. M1/M2:createParamEquImpl 改为“先建 pImp、成功后替换”(不要入口删,default 分支会丢 impl);copyFrom2 判空 cloneImpl 结果后再替换。
|
||||
4. L1:SetProperties 返回值修正。
|
||||
5. M10:GetType 失败日志。
|
||||
|
||||
@@ -131,4 +135,4 @@ subGetGripPoints 把 m_pGripAppData 当数组写下标 i。基类 AecDbEwPipeBas
|
||||
|
||||
### 验证方式
|
||||
|
||||
编译 cmake --preset R230 --config Debug(CoreLibrary/CoreImplement + Pool3dCmd);功能回归覆盖 新建各类型设备、保存、重开、inplace 编辑、移动/复制、夹点、剖切、ToJson 提资、HLR;内存方面做长时间 inplace 编辑会话观察内存曲线(验证 M1/M2 泄漏消失)。
|
||||
编译:`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 的覆盖顺序缺陷)。
|
||||
Reference in new issue
Block a user