com简介
COM组件是一组接口的集合,由Win32动态链接库(DLL)或可执行文件(EXE)的形式发布的可执行二进制代码组成。
com是独立于语言的组件体系结构,可以用C++、Java和VB等任意一种语言编写com对象。
COM组件的优点有:
通过接口对功能分类;
功能扩充,只需添加接口就行;
可轻松实现进程间调用、分布式调用;
具有封装、继承、多态的面向对象特征。
COM是规范,以该规范实现的DLL可视为COM组件。COM组件接口实现是统一的虚拟函数表(VTBL)形式,其包含组件函数的一组指针,由此可获取想要通信的组件函数的内存地址.
# define interface struct
使用 C++ 的纯抽象基类来实现 COM 接口
在 C++ 里, COM 接口被定义成一个纯虚类. 只有虚函数表的类. 但他仍然是类, 就像普通的继承方法一样继承就可以. 这些类的声明, 你包含 ole2.h 头文件, 基本的COM 接口就差不多了. 通常你的接口会继承 IUnknown, 或 IDispatch ,然后再继承自己的接口, 实现自己的接口. 自己重头写是很慢的, VC 的ATL, MFC 都对COM/ACTIVEX/OLE 的使用开发有很多支持. 当然, 明白底层也很重要的. 你要找书看, 推荐看 <<COM 本质论>>. 然后看 ATL 相关的, 如<<ATL深入解析>>等.
Vs和C++开发com组件
com组件编译会生成_i.c、_i.h文件,该文件由IDL接口声明文件生成而来。使用com包含该_i.h文件即可,头文件中包含com组件的所有声明。
1.新建ATL项目(你也可以是其他项目,只要是dll就行,可以支持MFC、ATL、COM等)
2.添加接口类 (ATL简单对象),注意要添加ProgID,目的是为了后面通过CLSIDFromProgID函数调用com
3.添加接口方法,设置参数和返回值 (在类视图中,对接口类添加方法)
4.实现接口方法
延伸:
1. 在调用的程序中怎使用com组件的自定义结构?
在com组件程序的idl文件的library中定义结构,同时需要加上uuid,如
[uuid(73B97ACE-1F6F-427c-9800-E904F97D39B0)]
struct Station
{
int StationType;
int StationID;
int CPUNum;
};
然后就可以在调用方直接使用Station结构了。
Vs和C++调用C++com组件
在c++调用com组件前,需要注册com组件,有手动注册和代码自动注册两种办法。
手动注册:
在注册com组件后,我们程序以uuid为唯一标识创建com组件实例,而uuid是com开发者编写时确定,uuid保证了其自身的唯一性,一定不会与别的组件撞车。
前提是以管理员身份运行cmd窗口,注册com.即regsvr32 comtest.dll
C:\windows\system32> e:
E:\> cd E:\study\qt\fstest\test\test
E:\study\qt\fstest\test\test> regsvr32 comtest.dll
代码自动注册
- 打开VC的属性界面进行设置,表示以管理员身份运行。
2.编写代码注册dll到本地电脑,执行WinExec函数
WinExec("regsvr32 E:\\study\\com\\testdll\\testdll\\ATLCom.dll", SW_SHOW);
获得接口声明
即先把dll复制过来,并在使用#import 方式导入文件。
第一种办法:com提供者编译生成com组件的dll/exe时生成的_i.h 文件 测试会报错
com组件编译会生成_i.c、_i.h文件,该文件由IDL接口声明文件生成而来。使用com包含(include)该_i.h文件即可,头文件中包含com组件的所有声明。
如:#include"../ATL Dll/ATLDll_i.h"
第二种办法:使用.tlb 文件生成 实测可行
编译com组件生成EXE/DLL文件时,除了会生成_i.h和_i.c以外还会额外生成一个.tlb文件。而该文件可以使用#import 方式作为模块导入,之后直接编译,ms会为我们生成带有声明的.tli和.tlh文件,在使用的地方同样包含tlh文件即可。
具体做法如下:
首先将tlb文件复制到我们需要使用的项目下,使用import包含,代码如下:
#include<atlcomcli.h>
//生成的tlh文件会自己加上namespace因此使用no_namesapce
//named_guids 指示编译器以旧样式定义和初始化 GUID 变量,格式为 LIBID_MyLib 、、 CLSID_MyCoClass IID_MyInterface 和 DIID_MyDispInterface
#import "ATLDll.tlb" no_namespace
int _tmain(int argc, _TCHAR* argv[])
{
return 0;
}
之后直接编译生成一次项目,会在中间目录得到.tli和.tlh文件,在使用的地方同样包含tlh文件即可。
第三种办法:使用com组件的二进制dll/exe生成:(实测可行,推荐使用这种办法)
以上两种情况都需要由com组件的提供者提供除开二进制执行模块以外的文件,这当然是不够好的,因此com提供直接从二进制生成接口相关声明的特性。
直接复制dll/exe到你的项目目录下,并且使用import导入,之后直接生成一次。 生成后在中间目录得到组件的tli和.tlh文件,再在使用的地方同样包含tlh文件即可。
#include <iostream>
#import "ATL Dll.dll" no_namespace
#include"./x64/Debug/atl dll.tlh"
int main()
{
return 0;
}
调用创建com组件实例
创建时获取该ID的办法有两种:
第一种办法:
直接使用"一"中生成的头文件声明中具有,直接使用,但需要使用__uuidof()关键字,其中uuid的名字来源于atl对象名,在头中声明如:
CLSID clsid = __uuidof(Simple); //对应library的uuid
延伸:IID iid = __uuidof(ISimple);//对应interface的uuid
#import " comtest.dll" no_namespace
#include"./Debug/comtest.tlh"
CoInitialize(NULL);
CComPtr<Ione> m_FirstClass; //Ione 为com组件的接口
HRESULT hr = S_OK;
hr = m_FirstClass.CoCreateInstance(__uuidof(one));//one为com组件的library
long result = 0;
result = m_FirstClass->add(4, 5 );
m_FirstClass.Release();//释放资源,不能省
m_FirstClass = NULL;
CoUninitialize();
建议这种
或者
#import " comtest.dll" no_namespace
#include"./Debug/comtest.tlh"
CoInitialize(NULL);
IonePtr m_FirstClass;
HRESULT hr = S_OK;
hr = m_FirstClass.CreateInstance(__uuidof(one));
long result = 0;
result = m_FirstClass->add(4, 5 );
m_FirstClass.Release();//释放资源
m_FirstClass = NULL;
CoUninitialize();
第二种办法:
使用 ProgID 查找出uuid,该名字为组件接口的别名,可以用于查找。
HRESULT hr = CLSIDFromProgID(OLESTR("Sim"), &clsid);
HRESULT hr1 = CLSIDFromString(OLESTR("{5e5c1802-d1fd-44af-8ac3-4453401b39db}"), &clsid1);
HRESULT hr = CLSIDFromProgID(OLESTR("ATLCom.Simple"), &clsid);//实测失败
第一个参数:
可以是ProgID 如OLESTR("Sim")
也可以是Dll名+Com对象名 HRESULT hr = CLSIDFromProgID(OLESTR("ATLCom.Simple"), &clsid);//但实测失败
延伸
HRESULT hr1 = CLSIDFromString(OLESTR("{5e5c1802-d1fd-44af-8ac3-4453401b39db}"), &clsid1);
#import " comtest.dll" no_namespace
#include"./Debug/comtest.tlh"
CoInitialize(NULL);
CLSID clsid;
CLSIDFromProgID(OLESTR("ggg"), &clsid);//ggg为csharp接口类的ProgId,创建csharp记得写上ProgId
CComPtr<Icsharp> pGetRes;//智能指针
pGetRes.CoCreateInstance(clsid);
float c = pGetRes->add(1,2);
pGetRes.Release();//小心哦!
CoUninitialize();
或者建议这种
或者
CoInitialize(NULL);
CLSID clsid;
HRESULT hr = CLSIDFromProgID(OLESTR("ggg"), &clsid);
Icsharp *ptr;
hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER,__uuidof(Icsharp), (LPVOID*)&ptr);
float c = ptr->add(1, 2);
ptr->Release();
CoUninitialize();
延伸:
CoCreateInstance的声明: 用指定的类标识符创建一个Com对象
HRESULT __stdcall CoCreateInstance(
const CLSID& clsid,
IUnknown* pIUnknownOuter,
DWORD dwClsContext,
const IID& iid,
void** ppv
);
CoCreateInstance有四个输入参数和一个输出参数。第一个参数是待创建组件的CLSID。第二个参数是用于聚合组件的。第三个参数的作用是限定所创建的组件的执行上下文。第四个参数iid为组件上待使用的接口的IID。CoCreateInstance将在最后一个参数中返回此接口的指针。
CoCreateInstance的第三个参数dwClsContext(类上下文)可以控制所创建的组件是在与客户相同的进程中运行,还是在不同的进程中运行,或者是在另外一台机器上运行。其取值为下列各值的组合:
CLSCTX_INPROC_SERVER
客户希望创建在同一进程中运行的组件。为能够同客户在同一进程中运行,组建必须是在DLL中实现的。
CLSCTX_INPROC_HANDLER
客户希望创建进程中处理器。一个进程中处理器实际上是一个只实现了某个组件一部分的进程中组件。该组件的其他部分将由本地或远程服务器上的某个进程外组件实现。
CLSCTX_LOCAL_SERVER
客户希望创建一个在同一机器上的另外一个进程中运行的组件。本地服务器是由EXE实现的。
CLSCTX_REMOTE_SERVER
客户希望创建一个在远程机器上运行的组件。此标志需要分布式COM正常工作。
数据结构交互
调用方和com组件的数据结构交互,com组件是通过使用VARIANT结构体进行传递数据的。
VARIANT结构体
普通的结构,如int
VARIANT varChunk;
VariantInit(&varChunk);
varChunk.vt = VT_INT;
varChunk.intVal = 1;
VariantClear(&varChunk);
如果是指针
int *ai = new int(1);
VARIANT varChunk;
VariantInit(&varChunk);
varChunk.vt = VT_INT;
varChunk.pintVal = ai;
VariantClear(&varChunk);
delete ai;
字符串
BSTR bstr = SysAllocString(_T("中文"));
VARIANT varChunk;
VariantInit(&varChunk);
varChunk.vt = VT_BSTR;
varChunk.bstrVal = bstr;
VariantClear(&varChunk);
SysReleaseString(bstr);
普通类型的数组
创建新的数组描述符,分配和初始化数组的数据,并返回指向新数组描述符的指针
SAFEARRAY * SafeArrayCreate(
[in] VARTYPE vt,
[in] UINT cDims,
[in] SAFEARRAYBOUND *rgsabound
);
[in] vt数组的基类型 (数组) 的每个元素的 VARTYPE
[in] cDims数组的维数
[in] rgsabound一个边界向量 (每个维度) 为数组分配的一个
在VARIANT的vt成员的 值如果包含VT_ARRAY|...,那么它所封装的就是一个SafeArray.也说明vt可以通过或逻辑运算
用于创建一维数组
SAFEARRAY * SafeArrayCreateVector(
[in] VARTYPE vt,
[in] LONG lLbound,
[in] ULONG cElements
);
SAFEARRAY
就是所谓的安全数组
typedef struct tagSAFEARRAY
{
USHORT cDims;
USHORT fFeatures;
ULONG cbElements;
ULONG cLocks;
PVOID pvData;
SAFEARRAYBOUND rgsabound[ 1 ];
} SAFEARRAY;
typedef struct tagSAFEARRAYBOUND
{
ULONG cElements;
LONG lLbound;
} SAFEARRAYBOUND;
ULONG cElements表示的是元素的数目(更准确的说是在本维中的数目);LONG lLbound表示的是一个逻辑起点序号,实际访问内存的时候,安全数组会将程序指定的序号减去lLbound,比如你将其设置为10000,a[10000]这相当于A[0],a[999]数组越界,所以在没有特殊要求的情况下,lLbound一般为0。rgsaBound[1]表示的是一维数组,二维数组要定义为rgsaBound[2]
将数据元素存储在数组中的指定位置
HRESULT SafeArrayPutElement(
[in] SAFEARRAY *psa,
[in] LONG *rgIndices,
[in] void *pv
);
[in] psa:SafeArrayCreate 创建的数组描述符
[in] rgIndices:数组的每个维度的索引向量,最右 (最小有效) 维度为 rgIndices[0]。 最左侧的维度存储在 rgIndices[psa->cDims – 1]。
[in] pv:要分配给数组的数据
检索数组的单个元素,一个一个地读
HRESULT SafeArrayGetElement(
[in] SAFEARRAY *psa,
[in] LONG *rgIndices,
[out] void *pv
);
[in] psa:SafeArrayCreate 创建的数组描述符
[in] rgIndices:数组的每个维度的索引向量
[out] pv:数组的元素。
递增数组的锁计数,并检索指向数组数据的指针,一次性直接读到缓冲区
HRESULT SafeArrayAccessData(
[in] SAFEARRAY *psa,
[out] void HUGEP **ppvData
);
递减数组的锁计数,使 SafeArrayAccessData检索的指针失效,与SafeArrayAccessData配对使用,使用了SafeArrayAccessData,需要调SafeArrayUnaccessData使SAFEARRAY 指针失效
HRESULT SafeArrayUnaccessData(
[in] SAFEARRAY *psa
);
[in] psa:SafeArrayCreate 创建的数组描述符
例子
在调用方
//(1). 定义变量
int uIsRead = 10;
DOUBLE bVal[] = { 0,1,2,3,4,5,6,7,8,9 };
//(2). 创建SafeArray描述符:
SAFEARRAY *psa;
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].cElements = uIsRead;
rgsabound[0].lLbound = 0;
psa = SafeArrayCreate(VT_R8, 1, rgsabound);
//(3). 放置数据元素到SafeArray:
for (long index = 0; index <= uIsRead; index++)
{
SafeArrayPutElement(psa, &index, &bVal[index]);
}
//(4). 封装到VARIANT内:
VARIANT varChunk;
VariantInit(&varChunk);
varChunk.vt = VT_R8;
varChunk.parray = psa;
int iRet = ptr->InitializeOMCC(bstrText, varChunk);
VariantClear(&varChunk);
在com实现
STDMETHODIMP CSimple::InitializeOMCC(BSTR strProjectFilePath, VARIANT stnInfoList, LONG *ret)
{
DOUBLE buf[10];
SAFEARRAY *psa = stnInfoList.parray;
for (long ind = 0; ind < 10; ind++)
{
::SafeArrayGetElement(psa, &ind, buf + ind);
}
return S_OK;
}
自定义结构体数组
在c++中自定义结构体可以用字节数组CByteArray来存储到COleVariant类型中。COleVariant类型可以和VARIANT类型通用。
在调用方
CoInitialize(NULL);
CLSID clsid = __uuidof(Simple);
IID iid = __uuidof(ISimple);
ISimple *ptr;
HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, iid, (LPVOID*)&ptr);
vector<StnInfo> stnInfoList;
StnInfo info;
for (size_t i = 0; i < 3; i++)
{
info.StationType = i;
info.StationID = i;
info.CPUSlotNum = i;
stnInfoList.push_back(info);
}
int nCount = stnInfoList.size();
int nsize = nCount * sizeof(StnInfo);
CByteArray baInput;
baInput.SetSize(nsize);
char *szdata = new char[nsize];
for (size_t i = 0; i < nCount; i++)
{
memcpy(szdata+i*sizeof(StnInfo), &stnInfoList[i], sizeof(StnInfo));
}
for (size_t i = 0; i < nsize; i++)
{
baInput.SetAt(i,szdata[i]);
}
delete[] szdata;
COleVariant oleva(baInput);
CString strPrjPath =_T( "E:\\中文\\com\\testdll\\testdll\\Debug");
BSTR bstrText = strPrjPath.AllocSysString();
int iRet = ptr->InitializeOMCC(bstrText, oleva);
SysReleaseString(bstrText);
CoUninitialize();
在com组件
typedef struct tagStnInfo
{
int StationType;
int StationID;
int CPUSlotNum;
}StnInfo;
STDMETHODIMP CSimple::InitializeOMCC(BSTR strProjectFilePath, VARIANT stnInfoList, LONG *ret)
{
// TODO: 在此处添加实现代码
VARIANT HUGEP *pbstr;
SAFEARRAY *psa = stnInfoList.parray;
SafeArrayAccessData(psa, (void HUGEP**)&pbstr);
int uCount = (psa->rgsabound->cElements) /sizeof(StnInfo);
for (int i = 0; i < uCount; i++)
{
int StationID = ((StnInfo*)((char*)(&pbstr[i]) - 4 * i))->StationID;
int CPUSlotNum = ((StnInfo*)((char*)(&pbstr[i]) - 4 * i))->CPUSlotNum;
int StationType = ((StnInfo*)((char*)(&pbstr[i]) - 4 * i))->StationType;
}
SafeArrayUnaccessData(psa);
return S_OK;
}
Com接口继承及实现
- 添加IFun2接口,继承ISimple接口
在idl文件interface ISimple : IDispatch后面添加
[
object,
uuid(B9D75722-1950-405b-8BB6-9788A6DB1DB2),
helpstring("IFun2 Interface"),
pointer_default(unique)
]
interface IFun2: ISimple
{
};
- 将library ATLComLib中的coclass Simple函数改为
coclass Simple
{
interface ISimple;
[default] interface IFun2;
};
3.在Simple.h头文件
class ATL_NO_VTABLE CSimple :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimple, &CLSID_Simple>,
public IDispatchImpl<ISimple, &IID_ISimple, &LIBID_ATLComLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
改成
class ATL_NO_VTABLE CSimple :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSimple, &CLSID_Simple>,
public IDispatchImpl<IFun2, &IID_ISimple, &LIBID_ATLComLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
在BEGIN_COM_MAP和END_COM_MAP中间加上COM_INTERFACE_ENTRY(IFun2),
BEGIN_COM_MAP(CSimple)
COM_INTERFACE_ENTRY(ISimple)
COM_INTERFACE_ENTRY(IFun2)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
4.重新添加新的接口函数即可。