开发ActiveX控件有两种方式,一是MFC,二是ATL,而后者是专门用于COM组件开发
书
- Visual C++实践与提高-COM和COM+篇
- COM原理与应用
- COM技术内幕
- COM本质论
网络教程
Introduction to COM - What It Is and How to Use It.
https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a
https://blog.csdn.net/q5806622/article/category/2846441
https://blog.csdn.net/woshinia/article/details/22300089
https://www.cnblogs.com/jyz/archive/2009/03/08/1406229.html
http://blog.e-works.net.cn/395307/articles/16044.html
https://blog.csdn.net/zj510/article/category/2510453
ATL创建过程
运行VC++2010,新建”ATL Project”项目
ATL Simple Object 增加COM类
鼠标右键单击接口,弹出菜单中添加方法或属性
方法
- 方法
- 参数特性
- in 指示一个参数从调用过程传入被调用过程
- out 特性标示从被调用过程返回到调用过程(从服务器到客户端)的指针参数
- retval 特性指定接收该成员的返回值的参数
- 参数特性
[in]类型表明参数是一个输入参数,所以这个参数不会向外界返回结果
[out]类型表明参数是个输出参数,所以这个参数会向外界返回结果,参数类型只能是指针类型
[out,retval]类型表明参数是个输出参数, retval 必须与 out 联用,并且在参数类表中只有最后一个参数可以被指定为 [out,retval] 属性,包装类会用这个参数的类型作为包装类方法的返回值
我们在 vc 中引入一个COM组件, vc 会分析 com 组件的类型库信息
假设有一个方法说明如下
[id(1), helpstring(“method Foo”)] HRESULT Foo([in] long lIn, [out] BSTR bstrOut, [out,retval] BSTR bstrResult);
那么vc生成的包装类的方法可能是
BSTR Foo(long lIn , BSTR& bstrOut);
我们看到,由于 bstrResult 具有 [out,retval] 属性,所以生成的包装类的方法返回值不再是 HRESULT类型,而是 BSTR 类型,这样,包装类就会更加易于使用
属性
- 属性
- 函数类型
- Get
- Put
- PropPut值传递
- PropPutRef引用传递
- 函数类型
ATL代码
ATL_NO_VTABLE
ATL_NO_VTABLE可以让编译器不产生VTable,并且不设置VPointer的值。 基类虚函数表会被派生类覆盖掉,所以我们可以对基类使用 ATL_NO_VTABLE 避免产生虚函数表! ATL是通过多重继承来实现COM组件的,继承层次中的每个类都有自己的虚函数表,所以在继承层次很深的情况下,虚函数表会变得非常宏大,如果用ATL_NO_VTABLE宏来阻止生成虚函数表,就会有限的减少组件的长度。
novtable不是去掉了虚函数表,要知道,虚函数表可是COM二进制复用的基础,怎么会去掉呢?这个宏,只是阻止了在多步构造对象时对虚函数表的调整。一般C++在构造一个派生类的时候,会从最初的祖先开始构造,这时该虚函数表指向最初的祖先的虚函数表,然后一步步构造派生类,虚函数表也不断的调整指向相应的派生类的虚函数表,这样能保证各个派生类构造时,调用的是该派生类实现的虚函数,ATL_NO_VTABLE只是阻止了这个过程,在构造过程中虚函数表没有做相应的调整,所以在构造函数中不能调用虚函数,以免产生错误的调用。
CComObjectRootEx类
线程模型,CComObjectRootEx类提供了处理非聚合和聚合对象的对象引用计数管理的方法。您可以通过设置ThreadModel为CComSingleThreadModel,CComMultiThreadModel或CComMultiThreadModelNoCS来明确选择线程模型。您可以通过设置ThreadModel为CComObjectThreadModel或CComGlobalsThreadModel来接受服务器的默认线程模型。
接口映射表
ATL提供了BEGIN_COM_MAP
、END_COM_MAP
、COM_INTERFACE_ENTRY
与COM_INTERFACE_ENTRY2
这4个宏来创建接口映射表。
DECLARE_PROTECT_FINAL_CONSTRUCT
DECLARE_PROTECT_FINAL_CONSTRUCT ()
//内部聚合对象增加引用计数然后将计数减少到0,则保护您的对象不被删除。
#define DECLARE_PROTECT_FINAL_CONSTRUCT()\
void InternalFinalConstructAddRef() {InternalAddRef();}\
void InternalFinalConstructRelease() {InternalRelease();}
禁用警告
#define ATLPREFAST_SUPPRESS(x) __pragma(warning(push)) __pragma(warning(disable: x))
#define ATLPREFAST_UNSUPPRESS() __pragma(warning(pop))
#pragma warning(push)
#pragma warning(disable:4705)
#pragma warning(disable:4706)
#pragma warning(disable:4707)
//一些代码
#pragma warning(pop)
深入解析C++编程中alignof 与uuidof运算符的使用
alignof 运算符
C++11 引入 alignof 运算符,该运算符返回指定类型的对齐方式(以字节为单位)。为实现最大的可移植性,应使用 alignof 运算符,而不是特定于 Microsoft 的 alignof 运算符。
返回一个 size_t 类型的值,该值是类型的对齐要求。
__uuidof 运算符
检索 GUID 并附加到表达式。
OBJECT_ENTRY_AUTO(__uuidof(Statistic01), CStatistic01)
__uuidof
运算符,检索 GUID 并附加到表达式。
OBJECT_ENTRY_AUTO
将ATL对象输入对象映射,更新注册表,并创建对象的实例。
IUnknown
IUnknown接口是COM的核心,因为所有其他的COM接口都必须从IUnknown继承。
class IUnknown{
public:
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0;
virtual ULONG __stdcall AddRef() = 0;
virtual ULONG __stdcall Release() = 0;
};
临界区类
ATL将Windows临界区封装了一下,即CComCriticalSection和CComAutoCriticalSection类。
类型库
作为C/C++程序员,要使用其他程序员编写的动态库或静态库,需要头文件。通过头文件,才能知道动态库或静态库里有什么函数,函数的参数、返回值情况……同样的,客户端程序要使用COM组件,也需要类似头文件功能的东西,这就是类型库。
可以编写文本格式的odl或idl文件,然后使用midl.exe将其编译为二进制的tlb文件——这就是类型库文件。
Dual和Custom
接口:双重,自定义
实际上Custom接口是从IUnknown继承下来的,而Dual接口是从IDispatch继承下来的(IDispatch从IUnknown继承下来)。
Dual:接口同时支持IDispatch方式和vtable方式
vtable调用方式,指的是直接通过接口指针的虚函数表。比如
CComPtr<IMyCar> spCar;
spCar.CoCreateInstance(CLSID_MyCar, NULL, CLSCTX_INPROC_SERVER);
spCar->Run();
Custom:没有支持IDispatch,只支持vtable。
如果是custom接口,那么只能通过vtable的方式了,就是通过接口指针来调用成员函数,而不能用IDispatch的invoke了。
那什么时候用Dual,什么时候用Custom呢?
当com组件只在C++里面被调用的时候,用custom;如果com组件可能被其他语言调用,那么就用dual。
另外,通过IDispatch::Invoke(),效率会比vtable调用方式低很多,因为它要经过很多转换。如果是C++调用环境,尽可能使用vtable的方式,如果是其他语言就没办法了,只能是IDispatch方式。
COM线程模型
STA接口
STA的规则很简单,但是需要小心的遵守这些规则:
- 每一个STA COM 对象只能存在于一个线程中 (在一个STA套间内)
- 每一个线程都需要初始化COM库
- 在套间之间传递com对象指针的时候,需要列集(marshal)
- 每一个STA套间必须拥有一个消息循环,用来处理从其他进程或者当前进程的其他套间过来的消息。(后面一句没有理解,就不翻译了,以免误导。)其实,我个人感觉如果一个STA套间创建了一个COM对象,只要这个COM对象不传递到其他线程,消息循环是可以省略的。但是如果COM对象需要传递到其他进程,那么就必须创建一个消息循环。
- COM对象本身并不需要调用COM的初始化函数;相反,他们会把他们的线程模型放在注册表中的一个叫做InprocServer32的键下面。后面的也不是很了解。以后弄明白了再说。
Apartment类型(单元),也就是STA。
COM的线程模型,其实指的是两方面:一个是客户程序的线程模式,一个是组件所支持的线程模式。客户程序的线程模式只有两种,单线程公寓(STA)和多线程公寓(MTA)。组件所支持的线程模式有四种:Single(单线程)、Apartment(STA)、Free(MTA)、Both(STA+MTA)。
- STA客户程序调用STA COM组件
- MTA客户程序调用STA COM组件
跨线程传递对象,消息循环
STA客户创建STA对象,然后传递到另外一个线程
Marshal和Unmarshal
通常中文翻译成列集和散集
CoMarshalInterThreadInterfaceInStream
CoGetInterfaceAndReleaseStream
消息循环
STA对象的线性调用其实就是通过Windows的消息循环来实现的。
给我们的主调STA客户加上消息循环
// TestCom.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <atlbase.h>
#include <thread>
#include <vector>
#include <windows.h>
#include "../MyCom/MyCom_i.h"
#include "../MyCom/MyCom_i.c"
LRESULT CALLBACK WndProc_Notify(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
{
return DefWindowProc(hWnd, wMsg, wParam, lParam);
}
void CreateWnd(void)
{
WNDCLASS wc = { 0 };
wc.style = 0;
wc.lpfnWndProc = WndProc_Notify;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
// wc.hInstance = g_hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetSysColorBrush(COLOR_WINDOW);
wc.lpszMenuName = NULL;
wc.lpszClassName = TEXT("NOTIFY_MSG_LOOP");
RegisterClass(&wc);
HWND g_hNotifyMsgLoop = CreateWindowExW(0,
wc.lpszClassName,
wc.lpszClassName,
WS_OVERLAPPEDWINDOW,
0,
0,
200,
200,
NULL,
NULL,
NULL,
0);
// ShowWindow(g_hNotifyMsgLoop, SW_HIDE);
}
void Test(LPSTREAM pStream)
{
CreateWnd();
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"STA calling thread (used passed in com object): %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);
CoInitialize(NULL);
CComPtr<ICircle> spCircle;
HRESULT hr = CoGetInterfaceAndReleaseStream(pStream, IID_ICircle, (LPVOID*)&spCircle); // unmarshal to get a com object
if (SUCCEEDED(hr))
{
spCircle->Draw(CComBSTR(L"yellow"));
}
CoUninitialize();
}
int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"Main thread: %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);
{
CComPtr<ICircle> spCircle;
spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC);
spCircle->Draw(CComBSTR(L"red"));
std::vector<std::thread> vThreads;
for (int i = 0; i < 5; i++)
{
LPSTREAM pStream = nullptr;
CoMarshalInterThreadInterfaceInStream(IID_ICircle, spCircle, &pStream); // marshal
vThreads.push_back(std::thread(Test, pStream)); // pass a stream instead of com object
}
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
for (auto& t: vThreads)
{
t.join();
}
}
CoUninitialize();
return 0;
}
如果想要把一个STA对象往另外一个线程传递,就需要:
列集/散列, (marshal/unmarshal)
创建STA对象的线程一定要有个windows消息循环。
COM系统发现有其他线程(套间)调用COM对象的方法,COM系统就会往创建COM对象的线程发送消息,但是如果那个线程没有消息循环来接收,那就永远都处理不了了。
MTA客户,跨线程传递COM对象
MTA调用STA对象的时候,STA本来就在一个default STA里面运行的。MTA客户获得的不过是一个代理而已。
当MTA客户在调用STA对象的时候,本来就涉及到2个套间:MTA和default STA。
MTA套间里的线程直接调用STA对象,不需要做marshal,MTA里面的线程调用的是STA对象的代理,系统会把这些调用转发给default STA,default STA默认就有个消息循环了,这个是由COM系统创建的,不需要程序员干涉。
如果我们直接把MTA创建的STA对象指针传递给STA线程,调用对象方法的时候,会得到一个错误:
RPC_E_WRONG_THREAD The application called an interface that was marshalled for a different thread.
那么这个地方就碰到一个问题,MTA创建的STA对象存在于一个default STA中,而我们的STA线程如果直接访问COM对象的话,又涉及跨套间的问题了。所有就得mashal,在MTA线程里面,把STA对象(代理)mashal一下,再传给STA线程,会发现调用正常了。而且STA的运行线程就是default STA里面的那个线程。这样就可以实现串行调用了。
总之对于STA COM 对象,所有方法的调用总是串行的,不会并发,不需要考虑同步问题。
STA客户调用STA对象,是通过消息循环来保证串行的。就是通过往创建对象的套间发送消息,套间线程来接收消息。
MTA客户调用STA对象, COM系统会处理marshal,并且在default STA里面创建默认消息循环。
MTA接口
跟STA相比,
COM系统不会帮助序列号,需要程序员自己来处理并发同步问题;
不需要消息循环。
另外,一个进程里面只能有一个MTA套间,MTA套间里面可以有多个线程。
Threading model中选择Free
MTA组件需要自己实现同步
传递MTA COM对象给STA套间线程
目前的COM设计允许一个没有显式初始化COM的线程成为MTA套间的一部分。只有当另外一个线程已经调用过CoInitializeEx(NULL, COINIT_MULTITHREADED)后,一个没有初始化COM的线程会在开始使用COM属于MTA套间。(有一个可能性是:COM自己会初始化成MTA但是客户并没有显式初始化;比如,一个STA线程在一个标记为”ThreadingModel=Free”的CLSID上调用CoGetClssObject/CoCreateInstance[x],COM会隐式创建一个MTA套间)
ok,针对我们的情况,辅助线程尽管自己没有初始化COM,但是它也是MTA套间里面的一个线程。所以,也不需要marshal。
STA套间调用MTA对象
- 当在STA套间里面创建MTA对象时,系统会自动创建一个MTA套间。
- MTA对象在MTA套间里面运行。
- 运行线程由系统来创建,系统会创建几个运行线程,并且挑选某个线程为当前调用服务。
运行线程
一个STA对象只能属于一个STA套间,那么一个STA对象一定是在一个线程里面运行的。所以STA对象不需要考虑并发,因为它永远是串行运行的。
MTA套间调用MTA对象:COM对象都是运行在创建它的线程里面
STA套间调用MTA对象:MTA对象是运行在系统创建的线程里面
COM连接点
COM里面的连接点就好像是C语言的回调函数,只不过它是基于面向对象实现的。连接点的作用也就是COM对象将一些事件通知客户(调用者)。
创建连接点
- 用ATL创建一个工程(DLL COM) CAtlComTest
- 然后创建一个COM接口,ATL简单对象,IMyCar. 在options那里选上connection points(连接点)。
- 然后在class view那里找到_IMyCarEvents(在CAtlComTestLib下面),右键点击Add Method.如图,增加一个方法OnStop。
- 之后在CMyCar上点击右键,选择Add Connection Point(添加连接点)。
在CProxy_IMyCarEvents里面可以看到多了个函数。如下
#pragma once
template<class T>
class CProxy_IMyCarEvents :
public ATL::IConnectionPointImpl<T, &__uuidof(_IMyCarEvents)>
{
public:
HRESULT Fire_OnStop( FLOAT Distance)
{
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection = static_cast<IDispatch *>(punkConnection.p);
if (pConnection)
{
CComVariant avarParams[1];
avarParams[0] = Distance;
avarParams[0].vt = VT_R4;
CComVariant varResult;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, &varResult, NULL, NULL);
}
}
return hr;
}
};
使用这个连接点
IMyCar增加一个方法
STDMETHODIMP CMyCar::Run()
{
// TODO: Add your implementation code here
this->Fire_OnStop(1000);
return S_OK;
}
然后写个客户程序
// TestCom.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <atlbase.h>
#include <atlcom.h>
#include "../MyCOM/MyCOM_i.h"
#include "../MyCOM/MyCOM_i.c"
#include <iostream>
#include <thread>
using namespace std;
class CSink :
public CComObjectRoot,
public _IMyCarEvents
{
BEGIN_COM_MAP(CSink)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(_IMyCarEvents)
END_COM_MAP()
public:
virtual ~CSink(){}
STDMETHODIMP GetTypeInfoCount(UINT *pctinfo) { return E_NOTIMPL; }
STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) { return E_NOTIMPL; }
STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) { return E_NOTIMPL; }
STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
printf("sink, id: %d, parm: %f", dispIdMember, pDispParams->rgvarg[0].fltVal);
return S_OK;
}
};
CComModule m_commodule;
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(0, COINIT_APARTMENTTHREADED);
{
CComPtr<IMyCar> spCar;
spCar.CoCreateInstance(CLSID_MyCar, NULL, CLSCTX_INPROC_SERVER);
CComObject<CSink>* sinkptr = nullptr;
CComObject<CSink>::CreateInstance(&sinkptr);
DWORD cookies = 0;
AtlAdvise(spCar, sinkptr, __uuidof(_IMyCarEvents), &cookies);
spCar->Run();
}
CoUninitialize();
return 0;
}
发现CSink里面的invoke被调用到了
其中pDispParams->rgvarg[0].fltVal就是COM对象触发这个连接点的时候传进来的参数1000.
主要过程是:
创建sink对象
挂载sink对象到一个COM对象上
COM对象调用Run函数
Run函数内部触发了一个连接点
调用者的对应函数会被调用,这里是Invoke。
总结一下:
如果COM对象需要支持连接点,那么这个对象类需要从连接点和连接点容器继承下来;(一个类可以有多个连接点)
创建接收对象(sink),接收对象需要从CComObjectRoot(或者类似的其他类)和连接点接口继承下来;
使用AtlAdvise来将一个sink对象挂载到相应的COM对象上。(当COM对象释放的时候,会相应释放所有拥有的sink对象)
这样,当COM对象需要触发一个事件的时候,就可以遍历所有sink对象,一个一个来触发。
整个过程也还是蛮简单的。其实仔细看看这个结构,这活脱脱就是一个观察者模式的典型例子。sink是观察者,具体COM对象是被观察者。当具体COM对象有事件需要触发的时候,就通过m_vec来通知所有的观察者(sink)。IConnectionPoint::m_vec是一个数组,存放所有的观察者。
IDispatch接口 - GetIDsOfNames和Invoke
GetIDsOfNames
这个函数的主要功能就是:把COM接口的方法名字和参数(可选)映射成一组DISPID。
Invoke
Invoke是IDispatch里面非常重要的一样函数,方法调用就靠这个函数了。
CComDispatchDriver智能指针
#include "stdafx.h"
#include <thread>
#include <atlbase.h>
#include <atlcom.h>
#include <algorithm>
#include <vector>
#include <memory>
#include "../MyCom/MyCom_i.h"
#include "../MyCom/MyCom_i.c"
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
CComDispatchDriver dsp;
dsp.CoCreateInstance(CLSID_MyCar);
CComVariant rt;
dsp.GetPropertyByName(L"Gas", &rt);
LONG total = rt.lVal;
CComVariant p1;
p1.vt = VT_I4;
p1.lVal = 12;
CComVariant p2;
p2.vt = VT_I4 | VT_BYREF;
LONG Gas = 0;
p2.byref = &Gas;
dsp.Invoke2(L"AddGas", &p1, &p2, NULL);
CComVariant totalGas;
dsp.GetPropertyByName(L"Gas", &totalGas);
total = totalGas.lVal;
dsp.Release();
CoUninitialize();
return 0;
}
看一下就知道怎么通过CComDispatchDriver来调用支持IDispatch接口的COM组件了。其实CComDispatchDriver内部还是通过GetIDsOfNames和Invoke等函数来调用COM组件的方法的。
ATL:连接点及接收事件的两种方法
COM组件有三个最基本的接口类,分别是IUnknown、IClassFactory、IDispatch
COM规范规定任何组件、任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是 QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。QueryInterface用于查询组件实现的其它接口,说白了也就是看看这个组件的父类中还有哪些接口类,AddRef用于增加引用计数,Release用于减少引用计数。引用计数也是COM中的一个非常重要的概念。大体上简单的说来可以这么理解,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。另一方面,一个组件也不是只给你一个人用的,可能会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,所以只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道已经没有人用它了,它的使用已经结束了,那它就把它自己给释放了。引用计数是COM编程里非常容易出错的一个地方,但所幸VC的各种各样的类库里已经基本上把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过AddRef,我们只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了 QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN 以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。 IUnknown的这三个函数的实现非常规范但也非常烦琐,容易出错,所幸的事我们可能永远也不需要自己来实现它们。
IClassFactory的作用是创建COM组件。我们已经知道COM组件实际上就是一个类,那我们平常是怎么实例化一个类对象的?是用‘new’命令!很简单吧,COM组件也一样如此。但是谁来new它呢?不可能是客户程序,因为客户程序不可能知道组件的类名字,如果客户知道组件的类名字那组件的可重用性就要打个大大的折扣了,事实上客户程序只不过知道一个代表着组件的128位的数字串而已,这个等会再介绍。所以客户无法自己创建组件,而且考虑一下,如果组件是在远程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象,这个对象就是类厂。每个组件都必须有一个与之相关的类厂,这个类厂知道怎么样创建组件,当客户请求一个组件对象的实例时,实际上这个请求交给了类厂,由类厂创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及远程创建组件时特别有用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复杂的操作都交给类厂对象去做了。IClassFactory最重要的一个函数就是CreateInstance,顾名思议就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了,只有某些特殊情况下才会由我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多的控制机会,而VB给我们这样的机会则是太少太少了。
IDispatch叫做调度接口。它的作用何在呢?这个世上除了C++还有很多别的语言,比如VB、 VJ、VBScript、JavaScript等等。可以这么说,如果这世上没有这么多乱七八糟的语言,那就不会有IDispatch。:-) 我们知道COM组件是C++类,是靠虚函数表来调用函数的,对于VC来说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如 VBScript、JavaScript。不行的原因在于它们并不支持指针,连指针都不能用还怎么用多态性啊,还怎么调这些虚函数啊。唉,没办法,也不能置这些脚本语言于不顾吧,现在网页上用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本语言所调用,既然虚函数表的方式行不通,我们只能另寻他法了。时势造英雄,IDispatch应运而生。:-) 调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅此而已。当然实际的过程远比这复杂,仅给一个编号就能让别人知道怎么调用一个函数那不是天方夜潭吗,你总得让别人知道你要调用的函数要带什么参数,参数类型什么以及返回什么东西吧,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看一看MS的类库里实现 Invoke的代码就会惊叹它实现的复杂了,因为你必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事,而且可能永远也没这样的机会。:-)
ATL 调用COM对象
创建COM对象一般有三种方法,正常创建一个对象,使用CoCreateInstance函数。若在远程系统中创建一个对象,使用CoCreateInstanceEX函数。而创建多个同一CLSID的对象时,使用CoGetClassObject函数。
1.先简单的使用CoCreateInstance函数创建一个COM对象。
//要加载生成的文件和这个c文件。
#include "ATLProject1_i.h"
#include "ATLProject1_i.c"
int _tmain(int argc, _TCHAR* argv[])
{
//声明的是接口的指针
ITryCOM *it = NULL;
//声明一个HRESULT变量
HRESULT hr;
//初始化COM,并告诉Windows以单线程的方式创建COM对象
hr = CoInitialize(0);
//使用SUCCEDED宏判断是否初始化成功。
if(SUCCEEDED(hr)){
//加载COM对象
hr = CoCreateInstance(CLSID_TryCOM,NULL,CLSCTX_INPROC_SERVER,IID_ITryCOM,(void**)&it);
//检测是否加载成功
if(SUCCEEDED(hr)){
long ReturnValue ;
printf("Find DLL\n");
int a= 1;
int b = 2;
it->Add(a,b,&ReturnValue);
printf("%d",ReturnValue);
//对于com对象,使用后都要使用release进行释放
it->Release();
}
//关闭当前线程的COM库,卸载所有dll,并释放资源。
CoUninitialize();
}
system("pause");
return 0;
}
2.使用CoGetClassObject来创建COM对象。
//要加载生成的文件和这个c文件。
#include "ATLProject1_i.h"
#include "ATLProject1_i.c"
int _tmain(int argc, _TCHAR* argv[])
{
//声明的是接口的指针
ITryCOM *it = NULL;
//一个工厂对象的指针
IClassFactory *ifp = NULL ;
//声明一个HRESULT变量
HRESULT hr;
//初始化COM,并告诉Windows以单线程的方式创建COM对象
hr = CoInitialize(0);
//使用SUCCEDED宏判断是否初始化成功。
if(SUCCEEDED(hr)){
//加载厂类
hr = CoGetClassObject(CLSID_TryCOM,CLSCTX_INPROC_SERVER ,NULL,IID_IClassFactory,(void**)&ifp);
//检测是否加载成功
if(SUCCEEDED(hr)){
hr = ifp->CreateInstance(NULL,IID_ITryCOM,(void**)&it);//使用工厂创建COM对象
ifp->Release();//释放
if(SUCCEEDED(hr)){
long ReturnValue ;
printf("Find DLL\n");
int a= 1;
int b = 2;
it->Add(a,b,&ReturnValue);
printf("%d",ReturnValue);
//对于com对象,使用后都要使用release进行释放
it->Release();//释放
}
}
//关闭当前线程的COM库,卸载所有dll,并释放资源。
CoUninitialize();
}
system("pause");
return 0;
}
3.忽略远程创建实例函数EX,最后在来考虑一下不使用CoCreateInstance or CoGetClassObject,直接从dll中得到DllGetClassObject,接着生成类对象及类实例(本方法适合于你想用某个组件,却不想在注册表中注册该组件)。之前的两个函数能调用,是因为其在系统中通过注册表对其唯一的ID进行了DLL的注册,所以调用这两个函数,使其能从系统中获得对应的DLL。最后是一个不注册dll的方法。
//要加载生成的文件和这个c文件。
#include "ATLProject1_i.h"
#include "ATLProject1_i.c"
int _tmain(int argc, _TCHAR* argv[])
{
//定义一个这样的函数指针
typedef HRESULT (__stdcall * pfnGCO) (REFCLSID, REFIID, void**);
//声明一个函数指针
pfnGCO fnGCO = NULL;
//加载dll
HINSTANCE hdllInst = LoadLibrary("D:/360data/重要数据/我的文档/Visual Studio 2012/Projects/TryDll/TryDll/ATLProject1.dll");
//在dll中寻找DllGetClassObject函数,并将其赋值给fnGCO指针,是fnGCO指针可以进行创建工厂类的操作
fnGCO = (pfnGCO)GetProcAddress(hdllInst, "DllGetClassObject");
if (fnGCO != 0)
{
//工厂对象
IClassFactory* pcf = NULL;
//释放fnGCO函数,即DllGetClassObject函数来获得一个工厂类对象,然后就是正常使用工厂类对象创建COM对象的操作了
HRESULT hr= (fnGCO) (CLSID_TryCOM, IID_IClassFactory, (void**)&pcf);
if (SUCCEEDED(hr) && (pcf != NULL))
{
ITryCOM* iTry = NULL;
hr = pcf->CreateInstance(NULL, IID_ITryCOM, (void**)&iTry);
if (SUCCEEDED(hr) && (iTry != NULL))
{
long ReturnValue ;
printf("Find DLL\n");
int a= 1;
int b = 2;
iTry->Add(a,b,&ReturnValue);
printf("%d",ReturnValue);
iTry->Release();
}
pcf->Release();
}
}
//释放加载的dll
FreeLibrary(hdllInst);
system("pause");
return 0;
}
总结一下在VC中调用COM组件的方法
一、最简单最常用的一种,用#import导入类型库,利用VC提供的智能指针包装类
二、引入midl.exe产生的.h,_i.c文件,利用CoCreateInstance函数来调用
三、不用CoCreateInstance,直接用CoGetClassObejct得到类厂对象接口,然后用该接口的方法CreateInstance来生成实例。
四、不用CoCreateInstance or CoGetClassObject,直接从dll中得到DllGetClassObject,接着生成类对象及类实例(本方法适合于你想用某个组件,却不想在注册表中注册该组件)
在MFC中调用
在MFC中除了上面的几种方法外,还有一种更方便的方法,就是通过ClassWizard利用类型库生成包装类,不过有个前提就是com组件的接口必须是派生自IDispatch
具体方法:
1、按Ctrl+W调出类向导,按Add Class按钮弹出新菜单,选From a type libarary,然后定位到simpCOM.dll,接下来会出来该simpCOM中的所有接口,选择你想生成的接口包装类后,向导会自动生成相应的.cpp和.h文件.
这样你就可以在你的MFC工程中像使用普通类那样使用COM组件了.
c++简单的ATL COM开发和调用实例
1、在MFC中调用有一种很方便的方法,就是通过ClassWizard利用类型库生成包装类,不过有个前提就是com组件的接口必须是派生自IDispatch。
具体方法:
(1)VS2010新建一个MFC基于对话框程序,调出类向导,点击“添加类”下拉菜单,选择“类型库中的MFC类”(From a type libarary)。
(2)选“注册表”,然后在“可用类型库”中定位到FirstCOMLib<1.0>,接下来会出来该库中的所有接口,选择你想生成的接口包装类IFirstClass后点击“完成”,向导会自动生成相应的.h文件,这样你就可以在你的MFC工程中像使用普通类那样使用COM组件了.1.0>
tlb
类型库文件,一般是COM技术所生成的东东的库,是IDL语言编写的.idl文件编译以后产生的东东,生成这个文件是为了让其他非C语言的程序使用的
tlb是在生成com时产生的,提供了该com的接口函数。在其他程序中想要引用已经有的COM中的方法
就可以import “*.tlb” 之后就可以调用里面的方法和函数了。打开看一下.tlb文件就明白了,
里面是一些须函数。
生成tlb文件:
- 使用命令midl XXX.idl来成成tlb文件即可
- 直接使用VC6或者Visual Studio打开dll文件,注意在打开文件对话框中一定要选择Resource方式,VC6默认是auto.找到资源中的TypeLib,其中的文件可以Export成bin,这个bin就是tlb,保存的时候使用将扩展名指定为tlb即可。
使用Visual Studio的Object Viewer可以直接对tlb文件进行查看。
使用tlb
#import "ATLProject1.tlb" no_namespace
, 然后重新编译生成*.tlh
与*.tli
。
Visual Studio有很多内置的支持将类型库导入到C++项目中,并使用这些定义的对象。例如,您可以使用#import指令:
#import "CANoe.tlb"
这将导入类型库,并将其转换为头文件和实现文件 - 也会导致实现文件与您的项目和头文件一起构建,因此这里有很多魔术。 然后,为类型库中定义的类型和对象提供了大量针对智能指针包装的typedef。例如,如果有一个名为Application的CoClass实现了IApplication接口,那么可以这样做:
ApplicationPtr app(__uuidof(Application));
这会在运行时导致创建coclass应用程序并绑定到变量app,您可以像这样调用它:
app->DoSomeCoolStuff();
通过检查COM调用的结果来完成错误处理,并根据需要抛出相应的_com_error
异常,这意味着您需要安全地编写异常。
更简单的方法是使用 #include 语句将.h和_i.c项目包含在.cpp文件中。
要创建接口指针,其中一种更安全的方法是使用CComPTR,如下所示:
CComPtr myPtr;
myPtr.CoCreateInstance(__uuidof("ClassNamehere"));
myPtr->Method(....);
实现的事件处理接口
其他
tlb、oxc、dll文件
vc6.0项目中:
Comments:
regsvr32 /s /c "$(TargetPath)"
echo regsvr32 exec. time > "$(OutDir)\regsvr32.trg"
Outputs:
$(OutDir)\regsvr32.trg
使用
可以include "xx_i.h"、include "xx_i.c"
文件
也可以#import "xx.tlb" no_namespace