|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThere are tons of articles and sample code on how to do something with ATL. Usually they teach how to only add feature to your component and you have to dig a lot of tutorials in order to build something rich-featured. In this article, I try to cover how to create COM server, expose it to scripting languages, make it an event source, add a VB-style collection to the object and add an ability to your object to report errors. I didn't set a goal to cover all questions on COM or attributed ATL in this article, so don't expect to find here explanations of every attribute or COM basics. Refer to MSDN for more detailed information. This article is just a quick walk-through on things that will make your COM object more friendly to other programmers. Get ReadyWe will be learning by example. The example is very simple — Windows® Services Manager. The services manager itself will be a COM object we'll write in C++ using attributed ATL, and there will also be a set of scripts in VBScript that will allow us to control services in batch mode. StartCoclasses in the ProjectWhen creating a program, we should think about which classes we will have. Here, we'll have the Manager itself, Services collection and Service. We are talking about COM, so they will be our coclasses.
Creating a projectTo start with ATL project, run Visual Studio .NET IDE and select File/New/Project... command. Select Visual C++ Projects/ATL/ATL Project and enter a name. In this tutorial, I'll use the name "ServicesManager". Don't change any options in ATL Project Wizard. Let it be Attributed and Dynamic-link library. Click Finish — we're done! Now we have a dummy COM object. It can be compiled, but does nothing yet. Open ServicesManager.cpp file. Note Adding coclassesLet's add our coclasses to the project. Right click on ServicesManager project in Solution Explorer and select Add/Add Class. Then select ATL/ATL Simple Object in Add Class - ServicesManager window. Enter ServicesMgr as a name in ATL Simple Object Wizard. Leave all options on the next page as is. Note Dual Interface option is selected. This will help us to provide the functionality of Click Finish in wizard's window and get all needed code for our Now, find [
object,
uuid("2543548B-EFFB-4CB4-B2ED-9D3931A2527D"),
dual,
oleautomation,
nonextensible,
hidden,
helpstring("IServicesMgr Interface"),
pointer_default(unique)
]
__interface IServicesMgr : IDispatch
{
};
Adding these attributes to the interface will make it compatible with OLE automation, hidden in user-oriented object browsers (just to save user's time) and will disallow the user to populate this interface with properties or methods at run-time. Make another note: we do all stuff right in our C++ code. We don't bother with IDL and other things. Repeat the steps above to add coclasses Adding FunctionalityLet's add __interface IServicesMgr : IDispatch
{
[id(1), helpstring("method Start")] HRESULT Start([in] BSTR ServiceName);
[id(2), helpstring("method Stop")] HRESULT Stop([in] BSTR ServiceName);
};
The wizard will also add proper declarations to the coclass and provide you with default implementation of this method. Edit Note the To simplify testing, "implement" these methods that way: STDMETHODIMP CServicesMgr::Start(BSTR ServiceName)
{
Beep(400, 100);
return S_OK;
}
STDMETHODIMP CServicesMgr::Stop(BSTR ServiceName)
{
Beep(1000, 100);
return S_OK;
}
Build the project and run the following script to test it: Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
Mgr.Start("SomeSvc")
MsgBox "Started!"
Mgr.Stop("SomeSvc")
If you did everything right, then you'll hear a beep, then see the message box and then hear a beep again. Note, that we testing our object with script, so it's exposed to scripting languages. Note also that we did it with minimum effort by using Starting and stopping services using their names is a good deal, but how's the user expected to know these names? We should provide him with the ability to iterate names of services in order to obtain all available names. Services CoclassAccording to "Building COM Components That Take Full Advantage of Visual Basic and Scripting" article, we should implement an interface with 2 methods and 1 property — So, our [
object,
uuid("5BB63796-959D-412D-B94C-30B3EB8D97F1"),
dual,
oleautomation,
hidden,
nonextensible,
helpstring("IsmServices Interface"),
pointer_default(unique)
]
__interface IsmServices : IDispatch
{
[propget, id(DISPID_VALUE),
helpstring("Returns a service referred by name or index")]
HRESULT Item([in] VARIANT Index, [out, retval] IsmService** ppVal);
[id(1), helpstring("Returns number of services")]
HRESULT Count([out,retval] LONG* plCount);
[id(DISPID_NEWENUM), helpstring("method _NewEnum")]
HRESULT _NewEnum([out,retval] IUnknown** ppUnk);
};
Note that property We decided that coclass Now class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
BEGIN_COM_MAP(CsmServices)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IsmServices)
END_COM_MAP()
...
};
This will provide us with the default implementation of Now we are able to instantiate CComObject<CsmServices> Services;
Repeat these steps for Then make a typedef CComObject<CsmServices> CServices;
class ATL_NO_VTABLE CServicesMgr :
public IServicesMgr
{
private:
CServices *m_pServices;
public:
CServicesMgr()
{
if (SUCCEEDED(CServices::CreateInstance(&m_pServices)))
m_pServices->AddRef();
}
void FinalRelease()
{
if (m_pServices)
m_pServices->Release();
}
...
};
And finally add __interface IServicesMgr : IDispatch
{
[id(1), helpstring("method Start")] HRESULT Start([in] BSTR ServiceName);
[id(2), helpstring("method Stop")] HRESULT Stop([in] BSTR ServiceName);
[propget, id(3), helpstring("Collection of available services")]
HRESULT Services([out, retval] IsmServices** ppVal);
};
Now add typedef std::vector<_Service> _Services;
class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
...
private:
_Services m_Services;
public:
STDMETHOD(EnumServices)();
...
};
STDMETHODIMP CsmServices::EnumServices()
{
// Populate m_Services here
return S_OK;
}
And implement STDMETHODIMP CServicesMgr::get_Services(IsmServices** ppVal)
{
if (m_pServices)
{
// Make sure we enumerated services
HRESULT hr = m_pServices->EnumServices();
if (SUCCEEDED(hr))
return m_pServices->QueryInterface(ppVal);
else
return hr;
}
return E_FAIL;
}
We populated Now add Enabling Collection IterationIn order to support collection iteration behavior ( Let's create Add a new ATL Simple Object to the project. Name it class ATL_NO_VTABLE CsmServicesEnum
: public CComObjectRoot
, IEnumVARIANT
{
BEGIN_COM_MAP(CsmServicesEnum)
COM_INTERFACE_ENTRY(IEnumVARIANT)
END_COM_MAP()
...
public:
STDMETHOD(Next)(unsigned long celt,
VARIANT *rgvar, unsigned long *pceltFetched);
STDMETHOD(Skip)(unsigned long celt);
STDMETHOD(Reset)();
STDMETHOD(Clone)(IEnumVARIANT **ppenum);
};
And don't forget to add typedef CComObject<CsmServicesEnum> CServicesEnum;
Our enumerator must hold a copy of services and the current state of enumeration: class ATL_NO_VTABLE CsmServicesEnum
: public CComObjectRoot
, IEnumVARIANT
{
...
private:
_Services m_Services;
int m_Idx;
public:
CsmServicesEnum()
: m_Idx(0)
{
}
void CloneServices(const _Services *pServices)
{
m_Services.assign(pServices->begin(), pServices->end());
m_Idx = 0;
}
...
};
Then STDMETHODIMP CsmServices::_NewEnum(IUnknown** ppUnk)
{
CServicesEnum *pEnum;
CServicesEnum::CreateInstance(&pEnum);
pEnum->AddRef();
pEnum->CloneServices(&m_Services);
HRESULT hr = pEnum->QueryInterface(ppUnk);
pEnum->Release();
return hr;
}
Now we can implement methods of our enumerator. STDMETHODIMP CsmServicesEnum::Next(unsigned long celt,
VARIANT *rgvar, unsigned long *pceltFetched)
{
if (pceltFetched)
*pceltFetched = 0;
if (!rgvar)
return E_INVALIDARG;
for (int i = 0; i < celt; i++)
VariantInit(&rgvar[i]);
unsigned long fetched = 0;
while (m_Idx < m_Services.size() && fetched < celt)
{
rgvar[fetched].vt = VT_DISPATCH;
// Create and initialize service objects
CService *pService;
CService::CreateInstance(&pService);
pService->AddRef();
pService->Init(m_Services[m_Idx]);
HRESULT hr = pService->QueryInterface(&rgvar[fetched].pdispVal);
pService->Release();
if (FAILED(hr))
break;
m_Idx++;
fetched++;
}
if (pceltFetched)
*pceltFetched = fetched;
return (celt == fetched) ? S_OK : S_FALSE;
}
STDMETHODIMP CsmServicesEnum::Skip(unsigned long celt)
{
unsigned long i = 0;
while (m_Idx < m_Services.size() && i < celt)
{
m_Idx++;
i++;
}
return (celt == i) ? S_OK : S_FALSE;
}
STDMETHODIMP CsmServicesEnum::Reset()
{
m_Idx = 0;
return S_OK;
}
STDMETHODIMP CsmServicesEnum::Clone(IEnumVARIANT **ppenum)
{
CServicesEnum *pEnum;
CServicesEnum::CreateInstance(&pEnum);
pEnum->AddRef();
pEnum->CloneServices(&m_Services);
HRESULT hr = pEnum->QueryInterface(ppenum);
pEnum->Release();
return hr;
}
In order to test our enumerator, implement STDMETHODIMP CsmService::get_Name(BSTR* pVal)
{
*pVal = m_Service.Name.AllocSysString();
return S_OK;
}
STDMETHODIMP CsmService::get_DisplayName(BSTR* pVal)
{
*pVal = m_Service.DisplayName.AllocSysString();
return S_OK;
}
Now we can write a simple test script: Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
WScript.Echo Mgr.Services.Count
Dim Service
For Each Service In Mgr.Services
WScript.Echo Service.DisplayName
Next
Only one thing left to complete with collections support. This is the STDMETHODIMP CsmServices::get_Item(VARIANT Index, IsmService** ppVal)
{
_Service svc;
*ppVal = 0;
if (VT_BSTR == Index.vt)
{
// Reference by string handle
CString SvcHandle(Index);
if (!GetService(SvcHandle, &svc))
return E_FAIL;
}
else
if (Index.vt & (VT_BYREF | VT_VARIANT))
{
// Reference by VARIANT (Dim i; For i = 0 to x Next; in VBScript)
LONG i = Index.pvarVal->lVal;
if (!GetService(i, &svc))
return E_FAIL;
}
else
{
// Reference by integer index
LONG i = V_I4(&Index);
if (!GetService(i, &svc))
return E_FAIL;
}
// Create service
CService *pService;
CService::CreateInstance(&pService);
pService->AddRef();
pService->Init(svc);
HRESULT hr = pService->QueryInterface(ppVal);
pService->Release();
return hr;
}
The code above uses overloaded function Now we can write the following code to work with our collection: Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
For i = 0 To Mgr.Services.Count - 1
WScript.Echo Mgr.Services.Item(i).Name
Next
Congratulations, we added collections support to our COM object. You can use similar technique to add another collection. Reporting ErrorsWhat if the user specified invalid service handle or index value? What if there're some problems with service manager on our machine? The right solution is to add to our object an ability to report errors. To report errors, our objects should implement First of all, we'll write an error-reporting function that will handle all deals with template<class ErrorSource>
HRESULT ReportError(ErrorSource* pes, ULONG ErrCode, UINT ResourceId = -1)
{
ICreateErrorInfo *pCrErrInfo;
IErrorInfo *pErrInfo;
if (SUCCEEDED(CreateErrorInfo(&pCrErrInfo)))
{
// Set all needed information for Err object in VB or active scripting
CString Descr;
if (-1 != ResourceId)
Descr.LoadString(ResourceId);
pCrErrInfo->SetDescription(Descr.AllocSysString());
pCrErrInfo->SetGUID(__uuidof(ErrorSource));
CString Source = typeid(ErrorSource).name();
pCrErrInfo->SetSource(Source.AllocSysString());
if (SUCCEEDED(pCrErrInfo->QueryInterface(IID_IErrorInfo,
reinterpret_cast<void**>(&pErrInfo))))
{
// Set error information for current thread
SetErrorInfo(0, pErrInfo);
pErrInfo->Release();
}
pCrErrInfo->Release();
}
// Report error via result code
return MAKE_HRESULT(1, FACILITY_ITF, ErrCode);
}
This is a template function. It will use type information to deduct interface GUID of the source and source type name (this could be obtained with In order to implement [
...
support_error_info("IServicesMgr"),
...
]
class ATL_NO_VTABLE CServicesMgr;
// ...
[
...
support_error_info("IsmService"),
...
]
class ATL_NO_VTABLE CsmService;
// ...
[
...
support_error_info("IsmServices"),
...
]
class ATL_NO_VTABLE CsmServices;
Now, let's define error codes and how we'll return them. For class ATL_NO_VTABLE CServicesMgr :
public IServicesMgr
{
...
private:
enum
{
errNoServices = 0x100
};
...
};
STDMETHODIMP CServicesMgr::Start(BSTR ServiceName)
{
if (m_pServices)
{
CString SvcName(ServiceName);
return m_pServices->Start(SvcName);
}
else
return ReportError(this, errNoServices);
}
STDMETHODIMP CServicesMgr::Stop(BSTR ServiceName)
{
if (m_pServices)
{
CString SvcName(ServiceName);
return m_pServices->Stop(SvcName);
}
else
return ReportError(this, errNoServices);
}
For class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
...
private:
enum
{
errCannotEnumServices = 0x200,
errCannotStart,
errCannotStop,
errInvalidIndex,
errInvalidHandle,
errCannotOpenServiceManager,
errCannotEnumerateServices,
errOutOfMemory,
errCannotOpenService,
errCannotQueryStatus,
errOperationFailed
};
...
};
Then STDMETHODIMP CsmServices::get_Item(VARIANT Index, IsmService** ppVal)
{
_Service svc;
*ppVal = 0;
if (VT_BSTR == Index.vt)
{
// Reference by string handle
CString SvcHandle(Index);
if (!GetService(SvcHandle, &svc))
return ReportError(this, errInvalidHandle);
}
else
if (Index.vt & (VT_BYREF | VT_VARIANT))
{
// Reference by VARIANT (Dim i; For i = 0 to x Next; in VBScript)
LONG i = Index.pvarVal->lVal;
if (!GetService(i, &svc))
return ReportError(this, errInvalidIndex);
}
else
{
// Reference by integer index
LONG i = V_I4(&Index);
if (!GetService(i, &svc))
return ReportError(this, errInvalidIndex);
}
// Create service
CService *pService;
CService::CreateInstance(&pService);
pService->AddRef();
pService->Init(svc);
HRESULT hr = pService->QueryInterface(ppVal);
pService->Release();
return hr;
}
We can test error reporting with this script: Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
Err.Clear
On Error Resume Next
WScript.Echo Mgr.Services.Item("qwe").Name ' "qwe" doesn't exist
MsgBox Err.Source
MsgBox Err.Number
MsgBox Err.Description
Firing EventsThe last thing we'll add to our services manager is an ability to notify the client with events. We'll add First, we create a brand new event interface: // Service operation progress codes
[
export,
helpstring("Operation progress codes")
]
enum ServiceProgress
{
spContinuePending = SERVICE_CONTINUE_PENDING,
spPausePending = SERVICE_PAUSE_PENDING,
spPaused = SERVICE_PAUSED,
spRunning = SERVICE_RUNNING,
spStartPending = SERVICE_START_PENDING,
spStopPending = SERVICE_STOP_PENDING,
spStopped = SERVICE_STOPPED
};
// IServicesMgrEvents
[
dispinterface,
nonextensible,
hidden,
uuid("A51F19F7-9AF5-4753-9B6F-52FC89D69B18"),
helpstring("ServicesMgr events")
]
__interface IServicesMgrEvents
{
[id(1), helpstring("Notifies about lenghtly operation on service")]
HRESULT ServiceOperationProgress(ServiceProgress ProgressCode);
};
Note that we also added an enumeration that will be visible for users in VB.NET, so they could use special value names instead of numbers. Now specify [
...
event_source("com"),
...
]
class ATL_NO_VTABLE CServicesMgr :
public IServicesMgr
{
...
__event __interface IServicesMgrEvents;
void Fire_ServiceOperationProgress(ServiceProgress Code)
{
__raise ServiceOperationProgress(Code);
}
...
};
After doing all this stuff, we can easily notify a client with service status by calling HRESULT CsmServices::WaitPendingService(SC_HANDLE hService,
DWORD dwPendingState, DWORD dwAwaitingState)
{
// ...
while (dwPendingState == ServiceStatus.dwCurrentState)
{
// ...
if (m_pMgr)
m_pMgr->Fire_ServiceOperationProgress
(static_cast<ServiceProgress>(ServiceStatus.dwCurrentState));
// ...
}
// ...
}
To test events handling, we'll use the following script: Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr", "Mgr_")
Mgr.Start("Alerter")
Sub Mgr_ServiceOperationProgress(ProgressCode)
WScript.Echo ProgressCode
End Sub
Better run this script using cscript.exe, not wscript.exe, so the output will be done in Points of InterestYou can find more about handling events in scripts by reading "Scripting Events" article in MSDN (Andrew Clinick, 2001). This was really an interesting thing for me. There's also a great article "Building COM Components That Take Full Advantage of Visual Basic and Scripting" (by Ivo Salmre, 1998, MSDN). In this article, you'll find basic information about the features your COM server needs to be seamlessly used in C++, VB and VBScript languages. If you want to debug similar objects, then just write a script in VBScript, set cscript.exe as debugging command and path to the script as command arguments. Then place breakpoints where needed and run the project. This is the easiest way to debug such COM objects. HistoryVersion 1.0 so far.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||