Introduction
There 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 Ready
We 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.
Start
Coclasses in the Project
When 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.
ServicesMgr
coclass will provide the user with a set of operations on Services
collection and services identified by name. Services
collection will provide the user with abilities to iterate services using foreach
statement. Service
coclass will represent a single service.
Creating a project
To 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 [module...]
lines there. This is an attribute. It defines the library block. This means that we have DllMain
, DllRegisterServer
and DllUnregisterServer
functions without writing any line of code.
Adding coclasses
Let'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 ServicesMgr
both to languages like C++ that use VTBL binding of methods and scripting languages that use IDispatch
interface to communicate with objects.
Click Finish in wizard's window and get all needed code for our ServicesMgr
coclass.
Now, find IServicesMgr
interface declaration in ServicesMgr.h file and add the following attributes to this interface: oleautomation
, hidden
and nonextensible
, so it will look like this:
[
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 smServices
(we cannot use Services
name, because it is a name of some system namespace) and smService
. Add attribute noncreatable
to both smServices
and smService
coclasses. This will prevent the user from creation of these objects.
Adding Functionality
Let's add Start()
and Stop()
methods to our ServicesMgr
coclass. Right click on IServiceMrg
node in Class View and select Add/Add Method. Set method name to Start
and add a BSTR [in]
parameter with name ServiceName
. Do the same to add Stop()
method. You should get the following code:
__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 helpstring
attributes to give more helpful hint for the user.
Note the id
attribute near each method. It sets the dispatch ID of the method. By using this attribute, we don't need to write any dispatching stuff by hand � everything will be done by the compiler.
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 dual
attribute, deriving our interface from IDispatch
and using id
attribute for the methods.
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 Coclass
According 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 � _NewEnum()
method, property Item
and Count()
method. These methods have special dispatch ID codes, so the caller will know what to expect from them. Note an underscored _NewEnum()
method. This means the method won't be visible for the user.
So, our IsmServices
should have these methods and property:
[
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 Item
and method _NewEnum()
use special DISPID
identifiers. This is important.
We decided that coclass smServices
will perform services enumeration, but on the other hand, coclass ServicesMgr
that will provide Services
as property, has methods for starting and stopping services. Then it's a good idea to delegate Start()
and Stop()
methods to smServices
. But this will lead us to a bit tricky declaration of smServices
coclass.
Now smServices
implements IsmServices
interface. Remove this declaration and replace it with the following:
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 IDispatch
interface and expose both IDispatch
and IsmServices
interfaces to the client.
Now we are able to instantiate smServices
coclass ourselves by using this construction (this will implement IUnknown
interface for smServices
):
CComObject<CsmServices> Services;
Repeat these steps for smService
coclass.
Then make a typedef
and add a declaration to ServicesMgr
coclass:
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 Services
property to ServicesMgr
coclass:
__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 EnumServices()
method to our smServices
coclass (not in interface!):
typedef std::vector<_Service> _Services;
class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
...
private:
_Services m_Services;
public:
STDMETHOD(EnumServices)();
...
};
STDMETHODIMP CsmServices::EnumServices()
{
return S_OK;
}
And implement get_Services()
method of ServicesMgr
:
STDMETHODIMP CServicesMgr::get_Services(IsmServices** ppVal)
{
if (m_pServices)
{
HRESULT hr = m_pServices->EnumServices();
if (SUCCEEDED(hr))
return m_pServices->QueryInterface(ppVal);
else
return hr;
}
return E_FAIL;
}
We populated CsmServices
coclass with methods without touching the interface that will be used by clients for enumeration. Clients won't need to call EnumServices()
directly.
Now add Start()
and Stop()
methods to smServices
coclass using the same way and move their implementation from ServicesMrg
coclass.
Enabling Collection Iteration
In order to support collection iteration behavior (For Each ... Next
), we should implement _NewEnum()
method of CsmServices
. The method should return a new object enumerating the collection. This object should implement IEnumVARIANT
interface.
Let's create CsmServicesEnum
class. This class will copy the list of services from CsmServices
and will give the user an ability to iterate it. List of services should be copied because if the user will run two enumerations simultaneously, we'll need to handle them independently.
Add a new ATL Simple Object to the project. Name it smServicesEnum
. It doesn't need a custom interface, so remove IsmServicesEnum
interface declaration and change the declaration of CsmServicesEnum
class and populate it with IEnumVARIANT
interface methods:
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
to be able to instantiate the object:
typedef CComObject<CsmServicesEnum> CServicesEnum;
Next()
method will fetch celt
elements of the collection, Skip()
will skip a number of items, Reset()
method will reset enumeration state to initial, and Clone()
method should create a copy of the current state of enumeration.
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 _NewEnum()
method of smServices
will look like this:
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;
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 Name
and DisplayName
properties of smService
coclass.
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 Item
property.
STDMETHODIMP CsmServices::get_Item(VARIANT Index, IsmService** ppVal)
{
_Service svc;
*ppVal = 0;
if (VT_BSTR == Index.vt)
{
CString SvcHandle(Index);
if (!GetService(SvcHandle, &svc))
return E_FAIL;
}
else
if (Index.vt & (VT_BYREF | VT_VARIANT))
{
LONG i = Index.pvarVal->lVal;
if (!GetService(i, &svc))
return E_FAIL;
}
else
{
LONG i = V_I4(&Index);
if (!GetService(i, &svc))
return E_FAIL;
}
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 GetService()
. This function searches for service record using either integer index or service handle. Refer to smServices.cpp for details.
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 Errors
What 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 ISupportErrorInfo
interface and use SetErrorInfo
function to supply information about an error to the caller.
First of all, we'll write an error-reporting function that will handle all deals with SetErrorInfo
function for us and will return a special result code.
template<class ErrorSource>
HRESULT ReportError(ErrorSource* pes, ULONG ErrCode, UINT ResourceId = -1)
{
ICreateErrorInfo *pCrErrInfo;
IErrorInfo *pErrInfo;
if (SUCCEEDED(CreateErrorInfo(&pCrErrInfo)))
{
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))))
{
SetErrorInfo(0, pErrInfo);
pErrInfo->Release();
}
pCrErrInfo->Release();
}
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 Err.Source
). It can also load error description from resources.
In order to implement ISupportErrorInfo
interface, we'll use support_error_info
attribute. Actually this is all we need to do.
[
...
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 ServicesMgr
, erroneous situation is when smServices
couldn't be instantiated. Add the following to the code:
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 smServices
, erroneous situation is when services couldn't be enumerated, user specified invalid service handle or index, or service couldn't be stopped or started:
class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
...
private:
enum
{
errCannotEnumServices = 0x200,
errCannotStart,
errCannotStop,
errInvalidIndex,
errInvalidHandle,
errCannotOpenServiceManager,
errCannotEnumerateServices,
errOutOfMemory,
errCannotOpenService,
errCannotQueryStatus,
errOperationFailed
};
...
};
Then CsmServices::get_Item()
will look like this:
STDMETHODIMP CsmServices::get_Item(VARIANT Index, IsmService** ppVal)
{
_Service svc;
*ppVal = 0;
if (VT_BSTR == Index.vt)
{
CString SvcHandle(Index);
if (!GetService(SvcHandle, &svc))
return ReportError(this, errInvalidHandle);
}
else
if (Index.vt & (VT_BYREF | VT_VARIANT))
{
LONG i = Index.pvarVal->lVal;
if (!GetService(i, &svc))
return ReportError(this, errInvalidIndex);
}
else
{
LONG i = V_I4(&Index);
if (!GetService(i, &svc))
return ReportError(this, errInvalidIndex);
}
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
MsgBox Err.Source
MsgBox Err.Number
MsgBox Err.Description
Firing Events
The last thing we'll add to our services manager is an ability to notify the client with events. We'll add ServiceOperationProgress()
event to notify the client about lengthy starting or stopping of service.
First, we create a brand new event interface:
[
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
};
[
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 IServicesMgrEvents
interface as event interface in ServicesMrg
coclass using __event __interface
keyword. ServicesMrg
coclass also must be marked with event_source("com")
attribute. To fire ServiceOperationProgress()
event, we should use __raise
keyword.
[
...
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 Fire_ServiceOperationProgress()
method.
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 stdout
.
Points of Interest
You 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.
History
Version 1.0 so far.