Files
cad-agent/ai.memory/project_mcp_frame_dll_loader.md
T
gjm 2fa27b1161 fix(mcp_frame): c++_dll 版本校验默认只比大版本,R24.3 单独分组
- acrxGetApiVersion 返回值编码为 (major<<16)|minor,据此拆分大/小版本
  (实测 R22.0=0x160000、R23.0=0x170000)。
- 同一大版本内二进制兼容(R24.0/R24.1/R24.2 可共用 ARX;2019/R23.0 编的 ARX
  可跑在 2020/R23.1 上),故默认只比大版本,避免为每个小版本各编一份。
- 唯一例外 R24.3:换了 MSVC toolset,与 R24.0~R24.2 不兼容,
  对 major==24 再按 minor>=3 分组判定。
- 同步更新 CODEBUDDY.md 与 ai.memory 说明。
2026-10-09 23:42:05 +08:00

4.4 KiB
Raw Blame History

name, description, type
name description type
MCP 基座 c++_dll 加载与执行的坑 CadProject 基座(cad_mcp_frame)加载/执行 c++_dll 工具的两个非显而易见坑——运行时 acrxGetApiVersion 有状态不可用于版本比对、写数据库需文档锁——及「同一 DLL 只加载一次」的模块缓存决定 project

MCP 基座 c++_dll 加载与执行的坑

来源:2026-10-09 排查「配置 26 个工具、DLL 导出 24 个,却只注册 9 个」的结论。 相关提交:fix(mcp_frame): 修复 c++_dll 工具大量注册失败与写库被文档锁拦截。

坑 1:运行时 acrxGetApiVersion() 不能用于版本比对

  • 事实:acrxGetApiVersion() 在基座与插件里都是 rxapi.lib(成员 libinit.obj)的 acrxGetApiVersionImpl,有状态:它读/写一个进程内全局,并与入参寄存器 ECX 做 XOR 后 回写;首次调用返回硬编码 0x170000(即 (ARX<<16)|SUB_ARX,对应 R230),之后每次调用 返回值都被污染、随调用次数变化。
  • 后果:AutoCAD 以 ARX 加载基座时会先调用一次该函数;此后基座再调用得到的是脏值, 而 plugins DLL(LoadLibrary 加载、无人调用过)首次返回正确值 → 两者不等 → 绝大多数 DLL 工具被判为「版本不匹配」而被 FreeLibrary 跳过(表现为只注册 9/26)。
  • How to apply:基座侧版本一律用编译期常量 ARX/SUB_ARX;不要把运行时 acrxGetApiVersion() 的返回值当版本用。实现见 mcp_configParser.cpp::LoadDllTool。

返回值编码 + 「只比大版本」的兼容判定

  • 返回值 = (major << 16) | minor(大版本=高 16 位,小版本=低 16 位)。实测(反汇编各版本 SDK 的 rxapi.lib):R22.0 → 0x160000(0x16=22)、R23.0 → 0x170000(0x17=23); 故 R23.1 → 0x170001,R24.0 → 0x180000。
  • 判定规则:默认只比大版本——同一大版本内二进制兼容(如 R24.0/R24.1/R24.2 可共用同一 ARX;AutoCAD 2019=R23.0 编的 ARX 可跑在 2020=R23.1 上)。唯一例外是 R24.3:它换了 MSVC toolset,与 R24.0~R24.2 不兼容,故对 major==24 再按 minor>=3 分一次组。
  • 若「大小版本全比」,会误拒用同大版本其它小版本 SDK 编的 DLL,逼着为每个小版本各编一份,浪费。

坑 2:DLL 工具写数据库必须先锁文档

  • 事实:工具执行链 TcpServerLoop → PostMessage(WM_MCP_EXECUTE_TASK) → ExecuteTaskInMainThread 运行在 AutoCAD 主线程的窗口消息处理里,此时处于「应用上下文」。在应用上下文对数据库 做 kForWrite 打开会返回 eLockViolation(表现为 openWriteSpace 拿不到模型空间,报 cannot open target space for write);读操作不受影响,所以之前一直没暴露。
  • How to apply:凡会写库的 DLL 工具都要锁文档。现已在 mcp_Tool_DllProxy::Execute(mcp_tool.cpp)统一加 acDocManager->lockDocument(pDoc, AcAp::kWrite) / unlockDocument——一处覆盖所有 DLL 工具(含 highlight_entities),且不波及走 sendStringToExecute 的 LISP 工具。 新增写库工具无需各自加锁,也不要在工具内部重复加锁。 (同级 envi-code 工程在对话框里写库也是 lockDocument(pDoc, AcAp::kWrite) 这个写法。)

架构决定:同一 DLL 只加载一次(模块缓存)

  • mcp_configParser.cpp 用 g_loadedDllModules(规范化路径 → 句柄)缓存:同一 DLL 全进程 只 LoadLibrary 一次,后续工具复用句柄并跳过版本校验;mcp_Tools::clear() 时统一 FreeLibrary。
  • Why:一个 DLL 常导出多个工具,原实现每个工具都 LoadLibrary + 版本校验,既重复,又放大 了坑 1(每次校验都调用一次有状态的版本函数)。
  • How to apply:mcp_Tool_DllProxy 已不再持有/释放模块句柄,只负责销毁 DLL 内的工具 对象。若改回「每个工具各持一个引用」,必须保证 LoadLibrary / FreeLibrary 次数配平, 否则模块会被提前卸载、代理析构时调用已卸载代码而崩溃。

诊断日志

  • LoadDllTool 仅在首次加载时写一行 arx_version=X dll_version=Y 到 <模块目录>/mcp_load.log,每次加载清空重写(mcpLoadReset)。属临时诊断,稳定后可删。