前面几篇文章已经分别讨论了自定义特征、命令/工作台接入和 mkmk 构建体系。本文换一个更底层的角度:CAA 里的对象到底是怎样被“看见”和“扩展”的。
很多 C++ 开发者刚接触 CAA 时,会自然地把问题想成“我写一个类,继承某个基类,然后 new 出来”。但在 CATIA V5 的 CAA 世界里,更常见的思路不是直接继承使用,而是:对象保持自己的运行时身份,外部代码通过接口去请求能力;某个 late type 能不能支持某个接口,由 dictionary、TIE/BOA 和模块加载机制共同决定。
如果说自定义特征回答的是“我要定义什么模型对象”,命令/工作台回答的是“用户如何触发功能”,那么对象模型和接口机制回答的是“CATIA 运行时如何找到你的能力”。理解这一层之后,再看 CATISpecObject_var spFeature = object;、TIE_xxx(MyClass)、.dico 里的三列注册信息,就不会像在看一堆魔法宏。
1. 先建立一条主线
CAA 的核心思路可以压缩成一条链路:
调用方拿到一个对象
-> 请求某个接口
-> 运行时根据对象 late type 和接口查 dictionary
-> 加载声明的模块
-> 创建 TIE/BOA 或扩展对象
-> 返回可调用的接口指针
这和普通 C++ 虚函数调用很不一样。普通 C++ 依赖编译期类型和继承层级,CAA 更像 COM 风格的组件模型:你不关心对象的真实 C++ 类名,而是问它“你是否支持这个接口”。
典型代码会长这样:
CATISpecObject_var spSpecObject = iObject;
if (!!spSpecObject)
{
// Use CATISpecObject services here.
}
或者显式写成:
CATISpecObject *specObject = NULL;
HRESULT rc = iObject->QueryInterface(IID_CATISpecObject, (void **)&specObject);
if (SUCCEEDED(rc) && specObject != NULL)
{
// Use CATISpecObject services here.
specObject->Release();
specObject = NULL;
}
前一种 _var handler 写法更常见,也更不容易忘记 Release。后一种写法适合在调试接口查询、阅读旧代码或解释引用计数时使用。
2. 核心词汇表
| 名称 | 常见形式 | 作用 |
|---|---|---|
CATBaseUnknown |
接口和很多实现类的共同基础 | 提供 CAA/COM 风格对象基础能力,支持接口查询和引用计数语义。 |
| Interface | class MyInterface : public CATBaseUnknown |
对外暴露的能力契约,只声明纯虚函数,不保存业务状态。 |
| IID | IID_MyInterface |
接口的唯一标识,运行时通过它判断调用方请求的是哪个接口。 |
| Handler | MyInterface_var |
CAA 智能指针风格包装,简化接口查询、引用计数和空值判断。 |
| Implementation class | MyCompanyEMeasure |
真正实现接口函数的 C++ 类。 |
CATImplementClass |
CATImplementClass(MyClass, CodeExtension, CATBaseUnknown, LateType) |
把 C++ 类声明给 CAA 元对象系统。 |
| TIE | TIE_MyInterface(MyClass) |
把接口调用转发到实现类,生成运行时可返回的接口对象。 |
| BOA | BOA_MyInterface(MyClass)或 CATImplementBOA |
另一种接口适配方式,通常按向导或本地样例使用。 |
| Dictionary | CNext/code/dictionary/*.dico |
声明 late type、接口和承载模块之间的映射关系。 |
| IID file | CNext/code/dictionary/*.iid |
给自定义接口分配稳定 IID。 |
这张表里最容易混淆的是“接口”和“实现类”。接口是别人能请求到的能力名称;实现类是你写业务逻辑的地方。一个实现类可以实现多个接口,一个 late type 也可以通过不同实现类挂接多个接口。
3. 接口不是普通基类
一个 CAA 接口通常放在 framework 的 PublicInterfaces、ProtectedInterfaces 或 PrivateInterfaces 中。公开给其他 framework 使用的接口放 PublicInterfaces;只给受保护依赖使用的放 ProtectedInterfaces;只在本 framework 内部使用的放 PrivateInterfaces。
一个最小接口可以写成这样:
#ifndef MyCompanyIMeasure_H
#define MyCompanyIMeasure_H
#include"MyCompanyModel.h"
#include"CATBaseUnknown.h"
classCATISpecObject_var;
#ifndef LOCAL_DEFINITION_FOR_IID
extern ExportedByMyCompanyModel IID IID_MyCompanyIMeasure;
#else
extern"C"const IID IID_MyCompanyIMeasure;
#endif
classExportedByMyCompanyModel MyCompanyIMeasure : public CATBaseUnknown
{
CATDeclareInterface;
public:
virtual HRESULT SetInput(const CATISpecObject_var &iInput)= 0;
virtual HRESULT GetInput(CATISpecObject_var &oInput)= 0;
virtual HRESULT SetLength(double iLength)= 0;
virtual HRESULT GetLength(double &oLength)= 0;
};
CATDeclareHandler(MyCompanyIMeasure, CATBaseUnknown);
#endif
几个细节值得养成习惯:
-
接口继承
CATBaseUnknown。 -
接口类中使用
CATDeclareInterface。 -
接口函数返回
HRESULT,业务输出通过输出参数返回。 -
接口类通常没有构造函数、析构函数和数据成员。
-
文件底部用
CATDeclareHandler声明MyCompanyIMeasure_var。 -
IID 名称、接口类名、handler 名称要保持一致,后面
.iid、.dico和 TIE 都会依赖这些名字。
如果接口是客户自定义 API,要特别注意二进制兼容性。已经发布给其他模块使用的接口,不要随手改函数签名、参数顺序或虚函数顺序。更稳妥的做法是新增 MyCompanyIMeasure2 之类的新接口,而不是破坏旧接口。
4. 实现类负责真正干活
接口只定义契约,真正访问属性、创建几何、更新状态的是 implementation class。假设我们要给 late type MyCompanyMeasure 挂一个 MyCompanyIMeasure 接口,实现类可以放在模块的 LocalInterfaces 和 src 中。
头文件示意如下:
#ifndef MyCompanyEMeasure_H
#define MyCompanyEMeasure_H
#include"CATBaseUnknown.h"
#include"CATISpecObject.h"
classMyCompanyEMeasure : public CATBaseUnknown
{
CATDeclareClass;
public:
MyCompanyEMeasure();
virtual ~MyCompanyEMeasure();
HRESULT SetInput(const CATISpecObject_var &iInput);
HRESULT GetInput(CATISpecObject_var &oInput);
HRESULT SetLength(double iLength);
HRESULT GetLength(double &oLength);
private:
MyCompanyEMeasure(const MyCompanyEMeasure &);
MyCompanyEMeasure &operator=(const MyCompanyEMeasure &);
};
#endif
实现文件里有两组关键宏:一组把类注册成 CAA class,另一组把接口绑到实现类。
#include"MyCompanyEMeasure.h"
#include"MyCompanyIMeasure.h"
#include"CATISpecAttrAccess.h"
#include"CATISpecAttrKey.h"
#include"CATIGSMAttributes.h"
CATImplementClass(
MyCompanyEMeasure,
CodeExtension,
CATBaseUnknown,
MyCompanyMeasure);
#include"TIE_MyCompanyIMeasure.h"
TIE_MyCompanyIMeasure(MyCompanyEMeasure);
MyCompanyEMeasure::MyCompanyEMeasure()
: CATBaseUnknown()
{
}
MyCompanyEMeasure::~MyCompanyEMeasure()
{
}
CATImplementClass 的几个参数大致可以这样理解:
| 参数 | 示例 | 含义 |
|---|---|---|
| C++ 类 | MyCompanyEMeasure |
当前实现类。 |
| 类种类 | Implementation、 CodeExtension、DataExtension |
该类在 CAA 元对象系统中的角色。 |
| 父类 | CATBaseUnknown |
C++ 继承基础。 |
| late type | MyCompanyMeasure或 CATNull |
这个扩展挂在哪种运行时类型上。 |
普通可直接实例化的类常见写法是 Implementation, CATBaseUnknown, CATNull。挂在某个 StartUp、容器或已有对象 late type 上的扩展,常见写法是 CodeExtension 或 DataExtension,具体采用哪一种要优先跟随 CAA 向导和同类官方样例。
在本机安装库的 CAADoc 示例中,CAACircleSweepTg 这类自定义几何 StartUp 的多个能力就是通过 CodeExtension 挂上去的:同一个 late type 同时支持业务接口、CATIBuild、CATIMf3DBehavior、CATIAttrBehavior、CATIEdit 和 CATIIcon 等接口。
5. TIE 的作用:把接口调用转给实现类
TIE_MyCompanyIMeasure(MyCompanyEMeasure) 看起来像一行简单宏,但它做的事情很关键:为接口生成一个适配层,让运行时请求 MyCompanyIMeasure 时,可以创建一个接口对象,并把接口函数调用转发到 MyCompanyEMeasure。
可以把它理解成这种关系:
调用方持有 MyCompanyIMeasure
-> 实际拿到 TIE object
-> TIE object 转调 MyCompanyEMeasure::SetInput
-> MyCompanyEMeasure 访问 feature 属性或执行业务逻辑
因此,如果你只写了实现类函数,但忘记 include TIE_接口名.h 或忘记调用 TIE_接口名(实现类),外部代码通常无法通过接口查询拿到你的能力。
生成的 TIE_*.h 里还经常能看到 BOA_接口名 宏。BOA 和 TIE 都是接口适配方式,不建议在没有明确理由时随意切换。团队项目中最稳妥的原则是:
-
新接口按 CAA 向导生成的方式走。
-
官方样例使用 TIE 的地方继续使用 TIE。
-
已有项目使用 BOA 的接口不要局部改成 TIE,除非你明确知道对象生命周期和二进制兼容影响。
6. Dictionary 决定接口能不能被找到
实现类和 TIE 写好之后,还需要 dictionary 告诉运行时:哪个 late type 支持哪个接口,这个接口实现在哪个库里。
典型 .dico 内容如下:
MyCompanyMeasure MyCompanyIMeasure libMyCompanyModel
MyCompanyMeasure CATIBuild libMyCompanyModel
MyCompanyMeasure CATIMf3DBehavior libMyCompanyModel
MyCompanyMeasure CATIEdit libMyCompanyUI
MyCompanyMeasure CATIIcon libMyCompanyUI
CATFeatCont MyCompanyIFactory libMyCompanyModel
三列分别是:
| 列 | 示例 | 作用 |
|---|---|---|
| late type | MyCompanyMeasure |
被扩展的对象类型,可以是 StartUp late type,也可以是容器或其他运行时类型。 |
| interface | MyCompanyIMeasure |
调用方要请求的接口。 |
| library | libMyCompanyModel |
承载实现类和 TIE 的模块名。 |
注意库名通常写 lib 前缀,不写 .dll。这和 command header 里填写命令模块名的习惯不同,容易混淆。
自定义接口还需要在 .iid 文件中声明稳定 IID:
{2f7b6bb0-31c8-4b78-9c4a-9e16c0c1a001} MyCompanyIMeasure
IID 一旦发布就不要随便改。调用方编译时认的是接口名和头文件,运行时匹配的是 IID 和 dictionary 映射。IID 改了,旧模块可能还能编译,却在运行时查询不到接口。
7. 接口查询的两种写法
CAA 代码里常见两种接口获取方式。
第一种是 _var handler:
MyCompanyIMeasure_var spMeasure = iFeature;
if (!!spMeasure)
{
double length = 0.0;
HRESULT rc = spMeasure->GetLength(length);
if (SUCCEEDED(rc))
{
// Use length.
}
}
这种写法简洁,适合大多数业务代码。!!spMeasure 是 CAA 代码中很常见的非空判断形式。
第二种是显式 QueryInterface:
MyCompanyIMeasure *measure = NULL;
HRESULT rc = iFeature->QueryInterface(
IID_MyCompanyIMeasure,
(void **)&measure);
if (SUCCEEDED(rc) && measure != NULL)
{
double length = 0.0;
rc = measure->GetLength(length);
measure->Release();
measure = NULL;
}
这种写法更啰嗦,但能清楚暴露三件事:
-
接口查询可能失败,要检查
HRESULT。 -
成功返回的接口指针需要释放。
-
查询失败不一定代表对象不存在,可能是
.dico没注册、模块没加载、IID 不一致、late type 不匹配,或者实现模块没有进入运行时视图。
8. StartUp、late type 和接口扩展的关系
在自定义特征开发中,OSM 里定义的 feature 名通常就是 late type。例如:
feature MyCompanyMeasure MechanicalElement@`MechMod.feat` #startup {
specobject InputFeature #in
double Length #out
}
这个 MyCompanyMeasure 后续会出现在三个地方:
CATImplementClass(MyCompanyEMeasure, CodeExtension, CATBaseUnknown, MyCompanyMeasure);
MyCompanyMeasure MyCompanyIMeasure libMyCompanyModel
MyCompanyMeasure="Company Measure";
第一处是 C++ 扩展绑定,第二处是接口 dictionary 注册,第三处是 NLS 显示名称。三者名字不一致时,问题会非常绕:模型实例可能创建成功,规格树也能显示,但接口查不到;或者接口能查到,却不是挂在你以为的 StartUp 上。
建议把 late type 当作自定义特征的“运行时身份证”。凡是 .CATfct、C++ 扩展、dictionary、NLS、图标、工厂创建代码里引用它,都要保持同一个拼写。
9. 一个接口从创建到可用的最小清单
假设要让 MyCompanyMeasure 支持 MyCompanyIMeasure,最小闭环如下:
| 步骤 | 文件 | 要点 |
|---|---|---|
| 1 | PublicInterfaces/MyCompanyIMeasure.h |
声明接口、IID、CATDeclareInterface、CATDeclareHandler。 |
| 2 | CNext/code/dictionary/MyCompanyModel.iid |
为接口分配 GUID。 |
| 3 | MyCompanyModel.m/LocalInterfaces/MyCompanyEMeasure.h |
声明实现类,使用 CATDeclareClass。 |
| 4 | MyCompanyModel.m/src/MyCompanyEMeasure.cpp |
CATImplementClass,include TIE_*.h,调用 TIE_接口名(实现类)。 |
| 5 | CNext/code/dictionary/MyCompanyModel.dico |
注册 late type / interface / libModule。 |
| 6 | Imakefile.mk |
链接接口、对象模型、特征模型等所需模块。 |
| 7 | IdentityCard |
声明所需 framework prerequisites。 |
| 8 | mkmk |
编译接口、生成 TIE、编译实现模块。 |
| 9 | mkCreateRuntimeView |
确保 dictionary、资源和 DLL 进入运行时视图。 |
| 10 | CATIA 测试 | 用 _var 或 QueryInterface 验证接口可查询并可调用。 |
这里第 8 步很关键:TIE_MyCompanyIMeasure.h 通常是根据接口生成的文件。如果生成头文件不存在,先别急着手写,应该检查接口放置位置、mkmk 生成步骤和 framework 公开头文件导出是否正常。
10. 实现函数里如何访问特征属性
自定义特征接口最常见的工作,是封装 StartUp 属性访问。不要让业务代码到处写字符串属性名,最好集中在接口实现类里。
示意代码如下:
HRESULT MyCompanyEMeasure::SetInput(const CATISpecObject_var &iInput)
{
HRESULT rc = E_FAIL;
CATISpecObject_var spFeature(this);
CATISpecAttrAccess_var spAttrAccess = spFeature;
CATIGSMAttributes_var spAttributes(this);
if (!!spAttrAccess && !!spAttributes)
{
CATISpecAttrKey_var spKey = spAttrAccess->GetAttrKey("InputFeature");
if (!!spKey)
{
if (!!iInput)
{
rc = spAttributes->SetAttrSpecObject(spKey, iInput);
}
else
{
rc = spAttributes->UnsetAttrValue(spKey);
}
}
}
return rc;
}
这个例子刻意把属性名 InputFeature 收敛在实现类内部。命令、factory、更新行为和外部模块只调用 SetInput,不直接知道 OSM 属性字符串。这样以后属性读写策略变化时,修改面会小得多。
实际项目中还要继续补几类保护:
-
输入对象类型检查。
-
空输入的业务含义。
-
属性 key 获取失败时的错误返回。
-
更新前后是否需要触发 dirty/update。
-
对
#in、#out、#neutral属性使用合适的读写接口。
11. 为什么一个对象能支持很多接口
CAA 的设计鼓励把能力拆成多个窄接口,而不是做一个巨大基类。例如一个几何特征可能同时具备:
| 接口 | 作用 |
|---|---|
| 自定义业务接口 | 设置输入、半径、方向、模式等业务参数。 |
CATIBuild |
更新时生成几何结果。 |
CATIMf3DBehavior |
告诉机械模型这是 shape、solid、datum 还是 volume。 |
CATIInputDescription |
描述输入,用于 replace、reroute、依赖分析等场景。 |
CATIReplace/ CATIReplaceUI |
支持替换输入和替换界面行为。 |
CATIAttrBehavior |
控制属性行为。 |
CATIEdit |
双击或上下文菜单编辑时启动 UI。 |
CATIIcon |
规格树图标。 |
这些接口不一定由同一个 C++ 类实现。模型侧接口可以在 libMyCompanyModel,UI 接口可以在 libMyCompanyUI。这样做的好处是模型模块不必依赖 UI 模块,批处理或无界面场景也能加载模型能力。
dictionary 可以自然表达这种拆分:
MyCompanyMeasure MyCompanyIMeasure libMyCompanyModel
MyCompanyMeasure CATIBuild libMyCompanyModel
MyCompanyMeasure CATIMf3DBehavior libMyCompanyModel
MyCompanyMeasure CATIEdit libMyCompanyUI
MyCompanyMeasure CATIIcon libMyCompanyUI
这也是 CAA 项目划分模块时很重要的依据:不要因为“都是同一个特征的能力”就把模型、命令、面板、图标、编辑器全放进一个库里。
12. Extension 类型怎么选
CATImplementClass 的第二个参数经常让人困惑。实际项目里,不要孤立背宏名,而要看这个类的运行方式。
| 场景 | 常见写法 | 判断依据 |
|---|---|---|
| 普通实现类,可直接由代码实例化 | Implementation |
不挂在某个 late type 上,第四个参数常为 CATNull。 |
| 给已有 late type 增加接口能力 | CodeExtension |
dictionary 中通过 late type 查询接口,官方 CAADoc 示例大量使用。 |
| 给 StartUp 或数据对象增加数据相关接口 | DataExtension |
特征模型或向导可能生成这种形式,按本地框架约定使用。 |
如果你不确定,先找同一 framework 里最接近的官方样例或团队旧代码。CAA 的宏和 dictionary 是一套运行时约定,局部“看起来也能编译”的改法,可能在运行时加载、对象生命周期或接口查询上留下隐患。
13. 模块边界:Model 和 UI 尽量分开
建议把 CAA 扩展按职责拆成几个模块:
MyCompanyModel.m
- StartUp 业务接口实现
- factory
- CATIBuild
- replace/input/attribute behavior
MyCompanyUI.m
- command
- dialog/state command
- CATIEdit
- CATIIcon
- workbench/add-in entry
这样拆分后,模型能力可以在批处理、文件打开、update、数据迁移等无界面场景下独立加载;UI 只在用户交互时加载。很多“在 CATIA 界面里能用,但批处理一跑就崩”的问题,本质上是模型代码错误依赖了 UI 模块。
对应的 LINK_WITH 也要保持单向、克制:
-
Model 模块可以依赖 ObjectModeler、MechanicalModeler、GSM、数学/拓扑等模型 API。
-
UI 模块可以依赖 ApplicationFrame、Dialog、Visualization 和自己的 Model 模块。
-
Model 模块不要反向依赖 UI 模块。
-
Public 接口所在 framework 的 IdentityCard 要声明调用方需要的 prerequisites。
14. 调试接口查不到的问题
接口查询失败是 CAA 开发中最常见也最容易绕远的问题。建议按下面顺序排查。
| 现象 | 优先检查 |
|---|---|
| 编译时找不到接口头文件 | IdentityCard prerequisite、公开/受保护接口位置、mkGetPreq、公开头文件导出。 |
编译时找不到 TIE_*.h |
接口是否被 mkmk 处理、接口是否有 CATDeclareInterface、IID 是否声明、生成目录是否刷新。 |
| 链接时报未解析外部符号 | LINK_WITH是否缺接口所在模块或实现模块,TIE 宏是否放在某个 .cpp 中。 |
运行时 _var 为空 |
.dico是否注册 late type/interface/lib,IID 是否一致,late type 拼写是否一致。 |
| 运行时提示库加载失败 | DLL 是否进入 runtime view,依赖库是否可见,模块名是否写错。 |
| CATIA 中界面能启动但模型更新失败 | UI 模块能加载不代表 Model 接口可查,检查模型侧 dictionary 和 CATIBuild。 |
| 换机器后才失败 | runtime view、dictionary、环境变量、prerequisite concatenation 是否一致。 |
一个很实用的调试办法是把问题拆成两段:
对象本身是否存在?
-> 能否 QueryInterface 到系统接口,例如 CATISpecObject?
自定义接口是否存在?
-> 能否 QueryInterface 到 MyCompanyIMeasure?
如果系统接口能查到,自定义接口查不到,重点看 .dico、.iid、TIE 和实现模块加载。如果系统接口也查不到,先确认你手里的对象是不是预期对象,选择路径、container、StartUp 实例或文档上下文可能就已经错了。
15. 常见设计建议
-
接口命名尽量表达业务能力,而不是实现方式,例如
MyCompanyIMeasure好过MyCompanyIAttrAccessImpl。 -
接口函数保持小而稳定,避免把 UI 类型、对话框类型、临时实现细节暴露到 PublicInterfaces。
-
不要在接口中返回裸内部对象让调用方随意改状态,优先提供明确的 get/set 或服务函数。
-
已发布接口需要扩展时,优先新增接口版本,而不是修改旧接口虚函数布局。
-
dictionary 中模型接口和 UI 接口分库注册,避免模型层被迫加载 UI。
-
使用
_varhandler 管理接口生命周期,只有在必要时才使用裸指针和显式Release。 -
HRESULT不要被忽略,尤其是属性访问、接口查询、工厂创建和 update 相关代码。
-
late type、StartUp 名、NLS key、图标名、dictionary 第一列要放在一起核对。
-
修改
.dico、.iid、NLS、图标、CATfct 后,记得刷新 runtime view 再进 CATIA 测试。
16. 和前几篇文章如何串起来
现在可以把几篇文章连成一条更完整的开发路径:
自定义特征开发
-> 定义 StartUp 和属性
-> late type 成为运行时身份
对象模型与接口扩展
-> 给 late type 挂业务接口、CATIBuild、CATIEdit 等能力
-> 通过 dictionary 和 TIE 让运行时能找到实现
命令、工作台与 Add-in 开发
-> 用户点击命令
-> 命令调用 factory 创建特征
-> 命令通过接口设置输入和参数
mkmk 编译体系
-> 编译接口、生成 TIE、链接模块
-> 复制 dictionary、资源和 DLL 到 runtime view
从这个视角看,一个 CAA 功能不是“一个类”或“一个 DLL”,而是一组相互配合的运行时声明:StartUp 声明数据形状,接口声明能力边界,实现类承载逻辑,dictionary 负责发现,构建系统负责生成和部署,命令负责把能力交到用户手里。
理解这套机制之后,CAA 开发会少很多玄学感。遇到问题时也可以沿着链路逐段定位:对象有没有创建,late type 对不对,接口有没有声明,IID 是否稳定,TIE 有没有生成,实现模块有没有链接,dictionary 有没有进入运行时视图,最后再看业务逻辑本身。
评论