1. 程式人生 > >最簡單的例子 (1)

最簡單的例子 (1)

連線點,connection point,在COM裡面也是挺重要的。簡單講,COM裡面的連線點就好像是C語言的回撥函式,只不過它是基於面向物件實現的。連線點的作用也就是COM物件將一些事件通知客戶(呼叫者)。

先來做一個簡單的連線點例子吧,之後再慢慢探討。

用ATL建立一個工程(DLL COM),取名MyCom,

然後建立一個COM介面,IMyCar. 在options那裡選上connection points。

然後在class view那裡找到_IMyCarEvents(在MyComLib下面),右鍵點選Add Method.如圖,增加一個方法OnStop。

增加完之後,可以看到IDL檔案裡面多了個方法,如下圖:

library MyComLib
{
	importlib("stdole2.tlb");
	[
		uuid(2CF347A8-63ED-4CE0-8A6D-F98D60C98B8C)		
	]
	dispinterface _IMyCarEvents
	{
		properties:
		methods:
            
            [id(1)] HRESULT OnStop([in] FLOAT Distance);
    };
	[
		uuid(DA6770F3-CBB6-4F34-A137-2B02A27AB219)		
	]
	coclass MyCar
	{
		[default] interface IMyCar;
		[default, source] dispinterface _IMyCarEvents;
	};
};

之後在CMyCar上點選右鍵,選擇Add Connection Point。

選擇_IMyCarEvents,點選>按鈕。然後Finish.

這樣,我們就增加了一個連線點,在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類

其實,這個東西比較固定,反正新手那麼寫就行了。其他的都可以忽略,主要就是Invoke函式,當COM物件fire一個事件的時候,Invoke函式會被呼叫。然後在這個函式裡面處理可以獲取事件的id:dispIdMember以及引數。這樣呼叫者,也就是事件的接收者就可以處理這些事件了,並且返回結果。

如何建立和掛載sink

首先,我們需要建立一個sink例項。就通過下面2行。先宣告一個null指標,然後呼叫CComObject<CSink>::CreateInstance來建立一個sink物件。等CreateInstance返回後,成功的話,sinkptr就指向一個CComSink<CSink>物件。

        CComObject<CSink>* sinkptr = nullptr;
        CComObject<CSink>::CreateInstance(&sinkptr);

   
然後將這個sink物件掛載到對應的COM物件上。下面的2行程式碼就是將sinkptr掛載到spCar物件上面。cookies的作用檢視MSDN.簡單講cookies是一個傳出引數,代表這次掛載,這個值是唯一的。如果想取消這個掛載,就可以用這個cookies和AtlUnadvise來取消連線。
        DWORD cookies = 0;
        AtlAdvise(spCar, sinkptr, __uuidof(_IMyCarEvents), &cookies);
細心一點的同學可能會問,這個地方是不是有個記憶體洩漏?
sinkptr指向了一個CComObject<CSink>物件,然後sinkptr本身死亡前又沒有釋放,那豈不是記憶體洩漏?

這是個很好的問題,我們先看一下AtlAdvise的實現:

ATLINLINE ATLAPI AtlAdvise(
	_Inout_ IUnknown* pUnkCP,
	_Inout_opt_ IUnknown* pUnk,
	_In_ const IID& iid,
	_Out_ LPDWORD pdw)
{
	if(pUnkCP == NULL)
		return E_INVALIDARG;

	CComPtr<IConnectionPointContainer> pCPC;
	CComPtr<IConnectionPoint> pCP;
	HRESULT hRes = pUnkCP->QueryInterface(__uuidof(IConnectionPointContainer), (void**)&pCPC);
	if (SUCCEEDED(hRes))
		hRes = pCPC->FindConnectionPoint(iid, &pCP);
	if (SUCCEEDED(hRes))
		hRes = pCP->Advise(pUnk, pdw);
	return hRes;
}
我們可以看到pUnk被傳到了pCP->Advise(pUnk, pdw);裡面。再看看相關程式碼:
template <class T, const IID* piid, class CDV>
IConnectionPointImpl<T, piid, CDV>::~IConnectionPointImpl()
{
	IUnknown** pp = m_vec.begin();
	while (pp < m_vec.end())
	{
		if (*pp != NULL)
			(*pp)->Release();
		pp++;
	}
}

template <class T, const IID* piid, class CDV>
STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(
	_Inout_ IUnknown* pUnkSink,
	_Out_ DWORD* pdwCookie)
{
	T* pT = static_cast<T*>(this);
	IUnknown* p;
	HRESULT hRes = S_OK;
	if (pdwCookie != NULL)
		*pdwCookie = 0;
	if (pUnkSink == NULL || pdwCookie == NULL)
		return E_POINTER;
	IID iid;
	GetConnectionInterface(&iid);
	hRes = pUnkSink->QueryInterface(iid, (void**)&p);
	if (SUCCEEDED(hRes))
	{
		pT->Lock();
		*pdwCookie = m_vec.Add(p);
		hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
		pT->Unlock();
		if (hRes != S_OK)
			p->Release();
	}
	else if (hRes == E_NOINTERFACE)
		hRes = CONNECT_E_CANNOTCONNECT;
	if (FAILED(hRes))
		*pdwCookie = 0;
	return hRes;
}
我們可以看到pUnkSink被放到了m_vec裡面(當然,如果中間過程出錯的話,直接被release了)。然後在解構函式裡面,我們可以看到m_vec裡面的所有元素都會呼叫release來釋放。

而spCar的實現類CMyCar有個基類叫做CProxy_IMyCarEvents,CProxy_IMyCarEvents有個基類是IConnectionPointImpl。

那麼當spCar在析構的時候,自然就會呼叫IConnectionPointImpl的解構函式。也就是說會把所有sink物件全部釋放掉。

換句話說,一旦將sink物件成功掛載到了COM物件,那麼sink物件的生命週期就由對應的COM物件來管理。

我們可以做個簡單測試,在上面的例子程式碼裡面,當spCar析構的時候,也就是程式碼要跑出大括號(spCar->Run();下面)的時候,sink物件也應該被釋放。跟了一把,確實是這樣。


ok,我們現在不用再擔心記憶體洩漏問題了。一旦掛載成功,sink物件就託管給對應的COM物件了,如果對應的COM物件析構了,那麼所有它管理的sink物件也就釋放了。

上面的例子,其實很傻,它的主要過程是:

1. 建立sink物件

2. 掛載sink物件到一個COM物件上

3. COM物件呼叫Run函式

4. Run函式內部觸發了一個連線點

5. 呼叫者的對應函式會被呼叫,這裡是Invoke。

細細看上面的例子,其實Invoke是在Run函式中被呼叫的。這個好像沒有什麼意義。真正的回撥應該是呼叫者呼叫完之後做其他事情了,然後等有回撥來的時候,對應的函式會被觸發。

anyway,本文中的例子是個非常簡單的例子,只是為了展示一個連線點。連線點的威力根本沒有發揮,至於真正的連線點應該怎樣?待續。。。