ObjectARX 与 GRX 对象系统深度解析:从 AcRxObject 到 GcSmartPtr
标签: ObjectARX, GRX, AutoCAD, C++, 智能指针, 内存管理
引言
在 CAD 二次开发领域,ObjectARX(AutoCAD Runtime eXtension)是最强大的 C++ API。然而,其对象系统设计之精妙往往让开发者感到困惑。本文将深入剖析 ObjectARX 的核心对象系统——从 AcRxObject 运行时类型系统 到 AcDbObject 资源管理,并对比国产 CAD 平台 GRX(GstarCAD Runtime eXtension)的实现差异,特别是 GRX 中智能指针的设计哲学。
一、RxObject 的引入原因:为什么要重新发明 RTTI?
1.1 历史背景:1990 年代的 C++ 困境
ObjectARX 诞生于 1990 年代,当时的 C++ 标准尚未成熟:
- 没有标准的 RTTI:
type_info在不同编译器间不兼容 - 没有反射机制:无法运行时获取类信息
- 跨 DLL 边界问题:MFC/VC++ 的
CRuntimeClass不跨 DLL - 没有智能指针:裸指针管理是常态
1.2 五大核心问题
| 问题 | C++ 原生局限 | ObjectARX 解决方案 |
|---|---|---|
| 跨 DLL 类型失效 | dynamic_cast 依赖编译器 RTTI |
自定义全局 AcRxClass 字典 |
| 无法动态创建对象 | new 需要编译时确定类型 |
运行时工厂 AcRxClass::create() |
| 类型转换不安全 | static_cast 危险,dynamic_cast 有开销 |
自定义 isKindOf() + cast() |
| 扩展需继承 | 违反开闭原则 | 协议扩展 queryX() |
| 版本兼容难题 | 无法处理未知类 | 版本号 + 代理对象机制 |
1.3 设计哲学:元对象系统
AcRxObject 本质上是一个元对象系统(Meta-Object System),类似于 Qt 的 moc 机制,但完全在 C++ 宏层面实现:
// 运行时类描述符,不依赖编译器
class AcRxClass {
const ACHAR* name(); // 类名
AcRxClass* myParent(); // 父类
AcRxObject* create(); // 工厂方法
// ...
};
二、RxObject 的基本协议
2.1 核心组件
// 基类:所有 ARX 对象的根
class AcRxObject {
public:
virtual AcRxClass* isA() const = 0; // 获取类描述符
virtual bool isKindOf(const AcRxClass*) const; // 类型检查
AcRxObject* queryX(const AcRxClass* protocol); // 协议扩展
};
// 类描述符:运行时类型信息
class AcRxClass {
const ACHAR* name() const; // 类名
AcRxClass* myParent() const; // 父类
AcRxObject* create() const; // 创建实例
void addX(AcRxClass* protocol, AcRxObject* impl); // 添加协议
};
2.2 关键宏机制
ACRX_DECLARE_MEMBERS:在类声明中使用,生成运行时方法
class MyClass : public AcRxObject {
public:
ACRX_DECLARE_MEMBERS(MyClass); // 展开为 isA(), desc(), cast() 等
};
ACRX_DXF_DEFINE_MEMBERS:在实现文件中定义类信息
ACRX_DXF_DEFINE_MEMBERS(
MyClass, // 类名
AcRxObject, // 父类
AcDb::kDHL_CURRENT, // DWG 版本
AcDb::kMReleaseCurrent, // 维护版本
AcDbProxyEntity::kNoOperation, // 代理标志
MYCLASS, // DXF 名称
"MyApplication\n" // 应用程序名
)
2.3 核心 API 速查表
| 方法 | 用途 | 示例 |
|---|---|---|
isA() |
获取对象的运行时类 | pObj->isA()->name() → "AcDbLine" |
isKindOf() |
检查是否继承自某类 | pObj->isKindOf(AcDbCurve::desc()) |
desc() |
获取类描述符(静态) | AcDbLine::desc() |
cast() |
安全类型转换 | AcDbLine::cast(pObj) |
create() |
动态创建对象 | AcDbLine::desc()->create() |
queryX() |
协议扩展查询 | pObj->queryX(IMyProtocol::desc()) |
2.4 使用示例
示例 1:运行时类型识别
// 从 DWG 读取到未知对象
AcDbObjectId objId;
AcDbObject* pObj;
acdbOpenObject(pObj, objId, AcDb::kForRead);
// 运行时识别类型
AcRxClass* pClass = pObj->isA();
acutPrintf(L"类型: %s\n", pClass->name()); // "AcDbLine"
// 检查是否是曲线类
if (pObj->isKindOf(AcDbCurve::desc())) {
AcDbCurve* pCurve = AcDbCurve::cast(pObj);
if (pCurve) {
double length = pCurve->getDistAtParam(1.0);
}
}
pObj->close();
示例 2:动态对象创建(DWG 反序列化)
// 从 DWG 文件读取到 "AcDbLine" 字符串
const wchar_t* className = L"AcDbLine";
// 通过类名获取类描述符
AcRxClass* pClass = acrxFindAcRxClass(className);
if (pClass) {
// 动态创建实例(不需要知道 AcDbLine 的定义!)
AcRxObject* pObj = pClass->create();
// 类型安全转换
AcDbLine* pLine = AcDbLine::cast(pObj);
if (pLine) {
// 使用 pLine...
}
}
示例 3:协议扩展机制
// 定义协议接口
class ICustomRender : public AcRxObject {
public:
ACRX_DECLARE_MEMBERS(ICustomRender);
virtual void render() = 0;
};
// 实现扩展
class CustomRenderImpl : public ICustomRender {
ACRX_DECLARE_MEMBERS(CustomRenderImpl);
void render() override {
acutPrintf(L"自定义渲染!\n");
}
};
// 注册扩展(无需修改 AcDbEntity 源码!)
void init() {
CustomRenderImpl* pImpl = new CustomRenderImpl;
AcDbEntity::desc()->addX(ICustomRender::desc(), pImpl);
}
// 使用扩展
void use(AcDbEntity* pEnt) {
ICustomRender* pRender = ICustomRender::cast(
pEnt->queryX(ICustomRender::desc()));
if (pRender) {
pRender->render();
}
}
三、DbObject 的资源管理
3.1 对象所有权模型
ObjectARX 使用数据库托管模式:
┌─────────────────────────┐
│ 你的代码 │
│ new AcDbLine() │ ← 创建
│ pSpace->appendAcDbEntity(objId, pLine) │ ← 移交所有权
│ pLine->close() │ ← 释放访问权
└─────────────────────────┘
↓
┌─────────────────────────┐
│ AutoCAD 数据库 │
│ • 管理对象生命周期 │
│ • 处理序列化 (DWG/DXF) │
│ • 文档关闭时统一释放 │
└─────────────────────────┘
3.2 黄金法则
| 场景 | 操作 | 后果(违规) |
|---|---|---|
| 对象已添加到 DB | 调用 close() |
delete 导致崩溃或双重释放 |
| 对象未添加到 DB | 必须 delete |
内存泄漏 |
| 临时访问对象 | acdbOpenObject() + close() |
裸指针悬挂 |
| 持久引用 | 使用 AcDbObjectId |
指针失效 |
3.3 Smart Pointer:RAII 救星
头文件: dbobjptr.h 和 dbobjptr2.h
// 基础版:自动 open/close
AcDbObjectPointer<AcDbLine> pLine(objId, AcDb::kForRead);
if (pLine.openStatus() == Acad::eOk) {
// 使用 pLine->methods()
double len = pLine->length();
} // 析构时自动 close()
// 增强版:避免打开冲突
AcDbSmartObjectPointer<AcDbEntity> pEnt(objId, AcDb::kForRead);
// 如果对象已以兼容模式打开,不会重复打开
预定义 typedef(方便使用):
AcDbEntityPointer // AcDbObjectPointer<AcDbEntity>
AcDbBlockTablePointer // AcDbObjectPointer<AcDbBlockTable>
AcDbLayerTablePointer // AcDbObjectPointer<AcDbLayerTable>
// ... 更多在 dbobjptr.h
3.4 对象生命周期状态图
创建 (new) → 未托管 → 添加到 DB (appendAcDbEntity)
↓ ↓
必须 delete() 调用 close()
↓ ↓
释放内存 数据库托管
↓
文档关闭时销毁
四、GRX 中的 GcDbObject 对象实现
4.1 GRX 与 ObjectARX 的关系
GRX(GstarCAD Runtime eXtension)是浩辰 CAD 平台的 C++ API,完全兼容 ObjectARX,但内部实现有差异:
| 特性 | ObjectARX | GRX |
|---|---|---|
| 运行时系统 | AcRxObject |
GcRxObject |
| 数据库对象 | AcDbObject |
GcDbObject |
| 智能指针 | AcDbObjectPointer |
GcDbObjectPointer, GcSmartPtr |
| 计数器 | 需配合数据库管理 | Impl 自带引用计数 |
4.2 GcDbObject 的核心优势:内置计数器
关键区别:GcDbObject 的 Impl 实现自带引用计数器,这带来了根本性改进:
// GRX 中 GcDbObject 可直接使用智能指针
GcDbObjectPointer<GcDbLine> pLine(objId);
pLine->doSomething(); // 安全,计数器自动管理
// 而 GcRxObject 不同:
// ❌ 直接使用智能指针可能导致内存泄漏
GcRxObjectPointer<GcRxSomething> pObj; // 不推荐!
// ✅ 必须使用 createObject 创建才能使用智能指针
GcRxObjectImpl* pImpl = GcRxObjectImpl::createObject();
// 现在可以安全使用智能指针
4.3 重要警告
GcDbObject 对象,因为 Impl 自带计数器,所以就可以直接使用智能指针。GcRxObject 不行,只有创建对象使用
GcRxObjectImpl::createObject()才能使用智能指针,否则可能内存泄漏。
这是 GRX 开发中最容易踩的坑!
五、GRX 的 GcSmartPtr 的设计和使用
5.1 GcSmartPtr 的设计哲学
GRX 提供了比 ObjectARX 更现代化的智能指针系统 GcSmartPtr,其设计目标:
- 线程安全:内部使用原子引用计数
- 零开销抽象:编译期优化,运行时无额外开销
- 兼容 STL:可与标准库容器配合使用
- 自动生命周期:RAII 完全自动化
5.2 与 ObjectARX 智能指针对比
| 特性 | ObjectARX AcDbObjectPointer |
GRX GcSmartPtr |
|---|---|---|
| 引用计数 | 外部管理(配合 DB) | 内置原子计数器 |
| 线程安全 | 否(单线程假设) | 是(原子操作) |
| 语法 | 模板类 | 更像 std::shared_ptr |
| 性能 | 有开销检查 | 零开销(优化后) |
| 使用范围 | 仅 AcDbObject | 更广(包括 GcRxObject) |
5.3 使用示例
基本用法
// 创建对象并使用智能指针管理
GcSmartPtr<GcDbLine> pLine = new GcDbLine(startPt, endPt);
// 添加到数据库
GcDbObjectId objId;
pBlockTableRecord->appendAcDbEntity(objId, pLine.get());
// 无需手动 close,智能指针自动处理
// 但要注意:添加到 DB 后所有权转移,智能指针应保持引用
与标准库容器配合
#include <vector>
std::vector<GcSmartPtr<GcDbEntity>> entities;
// 安全存储
GcSmartPtr<GcDbLine> pLine = new GcDbLine(...);
entities.push_back(pLine);
// 自动释放,无需手动管理
entities.clear(); // 所有对象自动引用计数减1
与 GcRxObject 的配合(重要!)
// ❌ 错误:直接使用 new GcRxSomething
GcSmartPtr<GcRxSomething> pRx = new GcRxSomething(); // 危险!
// ✅ 正确:使用 createObject
GcRxObjectImpl* pImpl = GcRxObjectImpl::createObject();
GcSmartPtr<GcRxSomething> pRx(pImpl);
// 现在可以安全使用
5.4 最佳实践总结
GRX 开发 checklist:
| 对象类型 | 创建方式 | 智能指针 | 注意事项 |
|---|---|---|---|
GcDbObject |
new |
✅ GcDbObjectPointer |
直接可用 |
GcDbObject |
new |
✅ GcSmartPtr |
添加到 DB 后所有权转移 |
GcRxObject |
new |
❌ 禁止 | 必须使用 createObject |
GcRxObject |
GcRxObjectImpl::createObject() |
✅ GcSmartPtr |
安全使用 |
六、总结与建议
6.1 ObjectARX 要点
- AcRxObject 是元对象系统,解决跨 DLL 类型识别和动态创建
- AcDbObject 使用数据库托管模式,
close()而非delete - Smart Pointer 使用
AcDbObjectPointer或AcDbSmartObjectPointer - 协议扩展
queryX()比继承更灵活
6.2 GRX 要点
- 完全兼容 ObjectARX API,但内部实现不同
- GcDbObject 内置计数器,可直接使用智能指针
- GcRxObject 必须使用
createObject才能配合智能指针 - GcSmartPtr 更现代化,支持 STL 和线程安全
6.3 迁移建议
如果从 ObjectARX 迁移到 GRX:
// ObjectARX 代码
AcDbObjectPointer<AcDbLine> pLine(objId, AcDb::kForRead);
// GRX 等效代码(更简洁)
GcDbObjectPointer<GcDbLine> pLine(objId); // 模式自动推断
// 或使用 GcSmartPtr(推荐新代码)
GcSmartPtr<GcDbLine> pLine = GcDbLine::create(...);