2026-10-09 22:37:31 +08:00
|
|
|
|
---
|
|
|
|
|
|
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)。
|
2026-10-09 23:42:05 +08:00
|
|
|
|
- **How to apply**:基座侧版本一律用**编译期常量** `ARX`/`SUB_ARX`;**不要**把运行时
|
2026-10-09 22:37:31 +08:00
|
|
|
|
`acrxGetApiVersion()` 的返回值当版本用。实现见 `mcp_configParser.cpp::LoadDllTool`。
|
|
|
|
|
|
|
2026-10-09 23:42:05 +08:00
|
|
|
|
### 返回值编码 + 「只比大版本」的兼容判定
|
|
|
|
|
|
|
|
|
|
|
|
- 返回值 = **`(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,逼着为每个小版本各编一份,浪费。
|
|
|
|
|
|
|
2026-10-09 22:37:31 +08:00
|
|
|
|
## 坑 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`)。属临时诊断,稳定后可删。
|