为自动化创建接口【技术文档】

摘要

自动化接口是一种特殊接口,允许通过简单的脚本语言进行编程并支持宏录制与回放。本文将从技术角度描述这些接口的需求以及创建它们的正确方法。

为自动化制作接口

通过接口声明对象是很好的方式,我们可以从中获得以下好处:客户端应用程序通过接口访问对象而从不直接操作实际的实现 。接口继承、实现继承和扩展是对象建模器功能强大、用途广泛、可增量化的关键特性,它使您能够定制和扩展其提供的对象并支持数据分发。

另一个关键主题是使它们支持自动化和日志记录 。自动化意味着接口可以从脚本语言访问,从而被用户脚本使用。日志记录意味着从交互式用户场景中录制脚本。该脚本可以原样回放或修改后回放,以自动化重复性任务。实现此功能的接口称为自动化接口。这要归功于接口暴露,使得可以通过用解释性语言(如 CATScript、VB Script、Visual Basic for Applications、VB.NET 和 C#)编写的宏来访问接口及其方法。自动化接口之所以具备这种能力,是因为其方法的签名有特定的限制,以匹配脚本语言的调用能力。

警告:此版本不支持日志记录。然而,本文中解释的日志记录主题是有效的。遵守这些主题将使您的接口在未来版本启用日志记录时能够支持它。

接口暴露

到目前为止,我们声明接口的方式已能利用对象建模器的特性,但尚未为暴露做任何事情。我们称这些接口为标准接口。另外还有两种接口处理接口暴露,下面按能力递增顺序解释:

  1. 暴露的接口:可以从脚本语言调用并可用于自动化。您需要将它们包含在 tplib文件中,以指示 mkmk生成类型库。
  2. 可日志化的接口:除了具备暴露接口的能力外,它们还可用于记录用户交互场景以构建脚本。这些脚本可以原样回放用于测试、调试或分析目的,但通常用作编写更全面脚本的框架。

从客户端应用程序程序员的角度来看,标准接口与属于上述两种类型之一的接口没有区别。但是,暴露的和可记录的接口的 IDL 文件包含 #pragma指令,请求生成一个类型库,供脚本语言用于检索这些接口并执行它们所指向的方法的代码。

IDispatch、CATBaseDispatch 和 CATIABase

暴露接口和可记录接口的基础接口是由 CAA 提供并符合 Windows 组件对象模型 (COM) 的 IDispatch。IDispatch 派生自 IUnknown,并提供以下方法,如 IDispatch.idl文件所示:

idl 复制代码
#include "IUnknown.idl"
#pragma ID IDispatch "DCE:00020400-0000-0000-C000000000000046"

interface IDispatch
{
  HRESULT GetTypeInfoCount(out unsigned long oNumberOfTypeInfo);

  HRESULT GetTypeInfo(in unsigned long iindex,
                      in unsigned long ilangId,
                      out unsigned long opTypeInfo);

  HRESULT GetIDsOfNames(in IID forFuture,
                        in WCHAR iArrayOfNames,
                        in unsigned long iNbNames,
                        in unsigned long iLangId,
                        out long oArrayOflong);

  HRESULT Invoke(in long ilongMember,
                 in IID  forFuture,
                 in unsigned long iLangId,
                 in unsigned short iFlags,
                 in DISPPARAMS iPdisparams,
                 in unsigned long oPvaresult,
                 in EXCEPINFO oPexcepinfo,
                 in unsigned long oPuArgErr);
};

所提供的 IDispatch接口与 COM 中的完全相同。CATBaseDispatch 接口派生自 IDispatch,因为CAA和Windows都使用#define语句将此接口视为结构以获得继承能力。

c 复制代码
#define interface struct

CATBaseDispatch 类为 GetTypeInfoCountGetTypeInfoGetIDsOfNamesInvoke 提供了实现,这些方法在 IDispatch.h 头文件中声明为纯虚函数。当您实现一个可暴露、已暴露或可记录的接口时,这避免了您需要自己实现它们,从而有助于代码复用。

让我们看一下 GetIDsOfNamesInvoke。给定一个方法名,GetIDsOfNames 返回其调度 ID,即 DISPID,它是一个长整型(而不是 GUID [1](#1))。DISPID 是方法在接口内的标识符。Invoke 然后使用 DISPID 作为第一个参数来调用相关方法。这称为后期绑定 ,是一种运行时绑定,因为要调用的实际方法是在运行时通过调用 GetIDsOfNames 来确定的,而不是借助构建时机制。这符合解释性语言的需求。

这与使用构建时机制(例如 C++ 使用的机制)有很大不同。接口使用抽象类表示。抽象类在内存中表示为一个可通过指针访问的区域,该指针就是接口指针。该指针指向一个存储指向TIE对象(将接口与其当前实现绑定在一起的对象)方法的指针表。此表称为vtbl,是类虚函数表的简称。当客户端使用接口指针调用方法时,该指针指向接口基类的vtbl,通过适当的偏移量来定位指向该方法的指针,从而调用适当的方法。vtbl在抽象类构建时创建,客户端应用程序中的调用在客户端编译时被转换为适当的调用指令。所以一切都是在构建时创建的。这称为vtbl绑定是一种构建时绑定。

在常见情况下(即对象不分布式时),vtbl绑定比后期绑定快得多。

CATIABase接口派生自CATBaseDispatch接口,并且是您想要使其成为暴露或可日志化接口的所有接口必须派生的基础接口,除了您的集合接口(您必须从CATIACollection派生)。

自动化接口派生关系

CATIABase接口由CATBaseObject类实现,目的有两个:

  1. 主要目的是提供ObjFromId方法,用于在首次遇到这些对象时确定对对象的引用。
  2. 第二个目的是为 Visual Basic with OLE 提供以下方法,以管理未解析对象的检索:
    • get_Application - 检索应用程序对象
    • get_Parent - 检索对象的父对象
    • get_Name - 检索对象的名称
    • GetItem - 管理未解析对象。

CATIACollection接口由CATBaseCollection类实现,并提供以下方法实现:

  • get_Application - 检索应用程序对象
  • get_Parent - 检索对象的父对象
  • GetItem - 管理未解析对象。

类型库

为了在运行时为脚本提供类型信息,COM 提供了类型库。这使得解释性语言能够通过对象的虚函数表访问方法,并确保像 C++ 运行时那样进行运行时类型检查。

类型库是一组 IDL 文件的编译版本。它包含所有接口的描述、所有方法的原型、属性以及它们所需的所有参数及其类型。它是使用 IDL 编译器 [2](#2) 构建的。

您可以在 Windows 上使用 OLE/COM 对象查看器扫描类型库。

发布您的接口

所有接口都使用 IDL(接口定义语言)进行编码。一些 #pragma 指令使您能够声明所描述接口的类型以及要在 IDL 文件中构建的对象。

暴露的接口

它们派生自 CATIABase,并在其 IDL 文件中包含 ID声明。

idl 复制代码
#pragma ID CATIAExposed "DCE:7c1b4ba8-5c25-0000-0280020bcb000000"
#pragma DUAL CATIAExposed
#pragma ID AccessName "DCE:73a1e08a-d8c4-11d0-a59f00a0c9575177"
#pragma ALIAS CATIAExposed AccessName
interface CATIAExposed : CATIABase {
...
};

暴露的接口必须在类型库中声明。

可日志化的接口

要将暴露接口变为可记录的接口,只需在 IDL 文件中添加注释 /*IDLREP*/,最好作为文件的第一行。

idl 复制代码
/*IDLREP*/
#pragma ID CATIJournalizable "DCE:7c1b4ba8-5c25-0000-0280020bcb000000"
#pragma DUAL CATIJournalizable
#pragma ID AccessName "DCE:73a1e08a-d8c4-11d0-a59f00a0c9575177"
#pragma ALIAS CATIJournalizable AccessName
interface CATIJournalizable : CATIABase

...
};

与暴露接口一样,可记录的接口必须在类型库中声明。

为自动化进行声明

如果您想利用自动化的功能,您需要在类型库源文件(后缀为.tplib)中声明接口。以下是此类文件的示例:

idl 复制代码
/*IDLREP*/
#pragma REPID INFITF "DCE:14f197b2-0771-11d1-a5b100a0c9575177"
#pragma REPBEGIN INFITF

#include "CATIApplication.idl"
#include "CATIPageSetup.idl"
#include "CATIWindow.idl"

#pragma REPEND INFITF

其中:

  • INFITF 是类型库名称
  • CATIApplication.idl、CATIPageSetup.idl和 CATIWindow.idl 是包含接口的文件
  • REPID、REPBEGIN 和 REPEND 是用于分别声明类型库名称及其 GUID、要包含在类型库中的接口的开头和结尾的关键字。

将此文件交付到类型为 TYPELIB的模块中,并在该模块的 Imakefile.mk 文件中声明先决条件类型库。

makefile 复制代码
BUILT_OBJECT_TYPE=TYPELIB
LINK_WITH = InfTypelib

IDL 编译器 [2](#2) 从此类源文件构建运行时类型库,并将其存储为 DLL。

如果自动化接口要与 Microsoft VSTA™ 编辑器一起使用,则应生成一个称为主互操作程序集 (Primary Interop Assembly) 的特定文件:

  1. 从 Microsoft Visual Studio™ 中以管理员身份启动"开发人员命令提示符"。

  2. 执行命令 sn -k pia.snk,这将在 C:\Windows\SysWOW64中生成一个密钥文件,用于对主互操作程序集进行签名。

  3. 将此文件交付到您的框架中,通常位于 myFramework\ProtectedInterfaces\pia.snk。

  4. 在 typelib 模块的 Imakefile.mk 中声明此密钥:

    makefile 复制代码
    TPLIB_PIA_SNK=myFramework/ProtectedInterfaces/pia.snk

创建 IDL 文件

属性和方法

属性相当于 C++ 中的数据成员。属性通过属性名从脚本语言访问,但 IDL 文件包含两个带有保留前缀 get_put_ 的函数,分别用于获取和设置属性值。这些函数有且只有一个参数。

遵循宏可移植性的规则

这些规则涉及类型、复杂类型的处理方式、接口继承和签名类型。

支持的参数类型

与 C++ 相比,脚本语言的参数类型支持有更多限制。方法签名的参数必须是以下类型:

暴露类型名称 类型描述 该类型的 IDL 语法
Boolean 可取值 True 和 False boolean
Integer 使用 16 位编码的有符号整数 short
Long 使用 32 位编码的有符号整数 long
Single 使用 32 位编码的浮点数 float
Double 使用 64 位编码的浮点数 double
BSTR 使用 Unicode 编码的字符串 CATBSTR
Variant 可包含上述任何类型 CATVariant
SafeArray(Variant) Variant 数组。此类型仅可用作输入或输入/输出参数 CATSafeArrayVariant
Object OLE 自动化接口 <interface name>
Error code 32 位编码的错误代码 HRESULT
Enum 枚举 Enum

以下 OLE 自动化类型不受支持

暴露类型名称 类型描述
Byte 使用 32 位编码的无符号整数
Currency 以给定货币表示的金额
SafeArray(<type>) 非 Variant 类型的数组
Decimal 使用 16 字节编码的高精度十进制数
Date 日期

复杂类型的处理

复杂类型,即 BSTR、SafeArray(Variant) 和 Variant,在 C++ 中从不直接处理,而只使用 CATUnicodeString、any[] 和 any。提供了在这些类型之间进行转换的函数。例如,CATUnicodeString 类提供了 BuildFromBSTR 方法从 BSTR 构建CATUnicodeString 类实例,以及 ConvertToBSTR 将 CATUnicodeString 类实例转换为 BSTR。CATAutoConversions.h 文件提供了用于转换 Variants 的全局函数。

接口继承

所有暴露的接口必须派生自以下两个接口之一:

  • CATIACollection - 用于集合接口,即描述对象列表的接口。
  • CATIABase - 用于任何其他接口。

这些接口公开了所有实现基本自动化机制的接口所共有的方法。这在 IDispatch、CATBaseDispatch 和 CATIABase 部分进行了解释。

参数方向属性

方法参数类型在方法签名中必须使用 IDL 并带有方向属性进行编码。输入参数使用 in 方向属性声明,而输出参数使用 out 方向属性声明。双向传递的参数使用 inout 声明。例如:

idl 复制代码
HRESULT MyExposedMethod(in    <type> iparameter,
                        inout <type> ioparameter);

MyExposedMethod 有一个 in 参数和一个 inout 参数。

签名类型

接口中公开的方法属于以下四种类型之一:

  1. 属性读取函数 。它们以 get_ 为前缀。此前缀专用于属性读取函数,不得用于任何其他函数。属性读取函数有一个参数,前面带有 out /*IDLRETVAL*/,返回读取的属性值。其签名如下:

    idl 复制代码
    HRESULT get_<propertyName> ( (out /*IDLRETVAL*/) <type> <parameterName> );

    示例:

    idl 复制代码
    HRESULT get_Height(out /*IDLRETVAL*/ long oHeight);

    注意,对于字符串,修饰符必须是 inout,例如:

    idl 复制代码
    HRESULT get_FileName(inout /*IDLRETVAL*/ CATBSTR oString);
  2. 属性写入函数 。它们以 put_ 为前缀。此前缀专用于属性写入函数,不得用于任何其他函数。属性写入函数有一个参数,前面带有 in,用于传递属性值。其签名如下:

    idl 复制代码
    HRESULT put_<propertyName> ( (in) <type> <parameterName> );

    示例:

    idl 复制代码
    HRESULT put_FileName(in CATBSTR iString);
  3. 子程序 (Sub procedures)。它们执行操作,但不返回值。其签名如下:

    idl 复制代码
    HRESULT <subProcName> ( ((in|out|inout)  <type> <parameterName>) * );

    示例:

    idl 复制代码
    HRESULT Update(in iTimeStamp);
  4. 函数程序 (Function procedures) 。与子程序一样,函数程序执行操作,但也返回值。对于 IDL 和 C++,其中一个参数被指定为脚本语言的函数返回值。此参数必须前面带有 out /*IDLRETVAL*/,并且必须是签名中的最后一个参数。

    idl 复制代码
    HRESULT <functionProcName> ( ((in|out|inout) <type> <parameterName>)* , out /*IDLRETVAL*/ <type> <returnedParameterName>);

    示例:

    idl 复制代码
    HRESULT GetLastItemInList(in CATIACollection iList, out /*IDLRETVAL*/ CATIABase oItem);
    
    HRESULT SelectElement(in CATSafeArrayVariant      iFilterType,
                          in CATBSTR                  iMessage,
                          in boolean                  iMaySkipInteractiveSelection,
                          inout /*IDLRETVAL*/ CATBSTR oOutputState            );

不能在同一个接口中放置两个具有相同名称和不同签名的方法:不支持方法重载。

属性

通过使用 #pragma 声明属性以及一对 get_/put_ 方法来创建读/写属性。示例:

idl 复制代码
#pragma PROPERTY Height
  HRESULT get_Height (out /*IDLRETVAL*/ long oHeight);
  HRESULT put_Height (in  CATBSTR iNameValue);

#pragma PROPERTY Name
  HRESULT get_Name (inout /*IDLRETVAL*/ CATBSTR oNameValue);
  HRESULT put_Name (in  CATBSTR iNameValue);

Name 属性使用 #pragma PROPERTY 声明。这两个方法表明它是一个读/写属性。get_Name 方法参数声明为 out,/*IDLRETVAL*/ 强制该参数作为脚本函数的返回值。

通过使用 #pragma 声明属性以及一个单独的 get_ 方法来创建只读属性。示例:

idl 复制代码
#pragma PROPERTY ActiveDocument
  HRESULT get_ActiveDocument (out /*IDLRETVAL*/ CATIADocument oDocument);

Name属性使用 #pragma PROPERTY 声明。get_ 方法表明它是一个只读属性。与读/写属性一样,get_Name 方法参数声明为 out,/*IDLRETVAL*/ 强制该参数作为脚本函数的返回值。

接口编码检查清单

请参考以下检查清单,以确保您编写的接口符合 IDL、COM、CORBA 和 CAA 规则。此检查清单包括 Visual Basic 兼容性要求。

  1. 每个接口使用一个文件,后缀为 .idl。示例: MyInterface.idl

  2. 暴露的接口派生自 CATBaseDispatch

  3. 可记录的接口是一个暴露的接口,第一行带有以下注释:

    idl 复制代码
    /*IDLREP*/
  4. 每个接口方法返回一个 HRESULT

  5. 每个接口方法参数声明为 in、out 或 inout 参数

    • in 在 C++ 中表示 <type> *(如果类型是接口)
    • out 在 C++ 中表示 <type> * &(如果类型是接口)或 <type> &(如果类型是基本类型)。
  6. 任何标识符不能以 put_ 或 get_ 开头,属性除外

  7. 接口不能有可选参数

  8. int 和 void * 类型不可用

  9. 方法不能重载:接口中不能存在两个名称相同但签名不同的方法

  10. 属性使用 #pragma PROPERTY 语句声明

    idl 复制代码
    #pragma PROPERTY PropertyName
      HRESULT get_PropertyName (out /*IDLRETVAL*/ <type> oPropertyValue);
      HRESULT put_PropertyName (in  <type> iPropertyValue);
  11. IDL 文件不能定义常量

  12. 用作方法参数的 CATSafeArray 类型不能超过一维

  13. CATBSTR 类型必须始终用于字符串。对于 CATBSTR 类型的返回值,应始终使用 inout 修饰符

  14. 枚举 (enum) 可以定义,前提是满足以下三个条件:

    • 不使用 GUID 标识
    • 使用 typedef 语句从枚举定义类型
    • 第一个枚举元素未初始化。它由 IDL 编译器 [2](#2) 赋值为 0。
  15. 检查您为参数和返回值声明的类型是否匹配方法的下表。没有其他类型可用:

    Visual Basic IDL C++
    Integer short short
    Long long long
    Single float float
    Double double double
    Byte octet octet
    Boolean boolean boolean
    HRESULT HRESULT HRESULT
    String CATBSTR CATBSTR
    Object CATBaseDispatch * CATBaseDispatch *
    Array CATSafeArray CATSafeArray
    Variant CATVariant CATVariant
  16. 检查您为属性 get_ 和 put_ 方法的参数和返回值声明的类型是否匹配下表。没有其他类型可用: (同上表)

  17. 暴露的或可记录的接口必须具有 ID,必须声明为 dual,并且必须具有暴露的名称:

    idl 复制代码
    #pragma ID CATIAExposed "DCE:7c1b4ba8-5c25-0000-0280020bcb000000"
    #pragma DUAL CATIAExposed
    #pragma ID AccessName "DCE:73a1e08a-d8c4-11d0-a59f00a0c9575177"
    #pragma ALIAS CATIAExposed AccessName
  18. CATVariant 代表 Visual Basic 中的 Variant,以及 IDL 中的 any 类型,但您必须在 IDL 文件中使用 CATVariant。当您不知道 Visual Basic 将使用的参数类型,或者该类型可能变化时,应该使用它。

简要总结

为了从脚本语言可用,IDL 接口必须派生自 CATIABase 或 CATIACollection,而实现它的类必须派生自 CATBaseDispatch 或 CATBaseCollection,后者提供了IDispatch声明的方法的实现。其方法还必须遵守特定的自动化签名限制。

根据您在接口的 IDL 文件中放置的语句,您的接口可以是可暴露的、已暴露的或可记录的。它可以包含客户端应用程序可以在实现该接口的对象实例上使用的属性和方法。

接口编码检查清单帮助您第一次就做对。

历史

版本 日期 说明
1 2000年5月 文档创建

  1. 关于全局唯一标识符 (GUID) ↩︎

  2. IDL 编译器 ↩︎ ↩︎ ↩︎

相关推荐
0思必得05 小时前
[Web自动化] 爬虫之API请求
前端·爬虫·python·selenium·自动化
自动化控制仿真经验汇总7 小时前
多水箱液位闭环控制-EXP-液位-多通道
自动化
赛希咨询7 小时前
人工智能自动化如何提高研究生产力
运维·人工智能·自动化
0思必得07 小时前
[Web自动化] 爬虫之网络请求
前端·爬虫·python·selenium·自动化·web自动化
北京耐用通信8 小时前
耐达讯自动化 Profibus 光纤链路模块:破解变频器通信难题,助力物流自动化升级
人工智能·物联网·网络协议·自动化·信息与通信
宇钶宇夕9 小时前
CoDeSys入门实战一起学习(十九):PLC编程公用元素(四):注释的全场景使用技巧
运维·自动化·软件工程
雨季66611 小时前
构建 OpenHarmony 深色模式快速切换器:用一个按钮掌控视觉舒适度
flutter·ui·自动化
云技纵横11 小时前
如何监控和预警Redis大Key问题?有哪些自动化处理方案?
数据库·redis·自动化
Fleshy数模11 小时前
Python自动化利器:文件与系统操作从入门到精通
java·python·自动化