Files
cad-agent/ai.memory/project_mcp_frame_dll_loader.md
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

62 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: MCP 基座 c++_dll 加载与执行的坑
description: CadProject 基座(cad_mcp_frame)加载/执行 c++_dll 工具的两个非显而易见坑——运行时 acrxGetApiVersion 有状态不可用于版本比对、写数据库需文档锁——及「同一 DLL 只加载一次」的模块缓存决定
type: 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`)。属临时诊断,稳定后可删。