Files
cad-agent/ai.memory/project_mcp_frame_dll_loader.md
T

51 lines
3.6 KiB
Markdown
Raw Normal View History

---
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<<16)|SUB_ARX`;**不要**把运行时
`acrxGetApiVersion()` 的返回值当版本用。实现见 `mcp_configParser.cpp::LoadDllTool`。
## 坑 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`)。属临时诊断,稳定后可删。