Integrating ACE and ATL
Introduction
Download the source code for this project
It’s my experience that combining ACE with ATL provides me with an excellent
foundation for writing reliable COM based services in C++.
ACE provides a wealth of functionality when it comes to multithreading and
synchronization. It has one of the most feature-complete APIs for developing
fast and reliable networked solutions in C++.
The framework provides state of the art functionality for writing serious real-time software solutions.
In this article we will build a simple COM Service application demonstrating
one possible approach to integrating ACE and ATL. ACE will be used to
implement asynchronous callbacks from the service to the COM clients.
ACE is a C++ library
with an excellent track record when it comes to developing reliable solutions.
For a list of companies and projects using ACE take a look at the
“Who is Using ACE” page.
While ACE is mostly used to develop portable and highly efficient networked solutions,
integrating it with ATL is an interesting concept. The same approach can be used to
enable integration with TAO, allowing us to easily develop combined Corba and Com services.
ACE is according to Douglas C. Schmidt,
the inventor of ACE, written to be portable, flexible, extensible, predictable, reliable,
and affordable.
Since it’s an open source project, it’s certainly affordable, and it has an
active and responsive developer community.
As a developer I can appreciate the “flexible, extensible, predictable, reliable” parts too.
If you don’t know anything about ACE, take a look at this
tutorial by Umar Syyid.
Prerequisites
Download ACE at http://download.dre.vanderbilt.edu/
and build the project according to the included instructions – or take a look
at ACE-INSTALL.html
for detailed instructions.
This project assumes that “config.h” for the ACE libraries includes
#define ACE_USES_WCHAR 1
and
#define ACE_NTRACE 0
before
#include "ace/config-win32.h"
ACE_USES_WCHAR builds ACE using wchar_t and Unicode. Defining “ACE_NTRACE 0” turns on the
ACE_TRACE macro. This is will provide us with an awful lot of information during execution
of our program, but it’s a nice feature, especially during development and testing.
Remember to set the ACE_ROOT environment variable and add %ACE_ROOT%\lib to the system path.
Under Windows 7: run Visual Studio as administrator to enable automatic registration of the COM application during builds.
Create the project
We will start by creating a standard ATL service application.
Remember to select the “Service (EXE)” radio button on the “Application Settings” page of the ATL project Wizard.
Click finish and Visual Studio creates a standard ATL service application.
Now we need to tell Visual Studio where it will find the ACE libraries.
Add “$(ACE_ROOT)\lib” to the “Library Directories”
At this point we are ready to add our ATL COM object implementation,
so switch to the “Class View” and select Add->Class from the projects popup menu.
This will bring up the “Add Class” dialog where we will select “ATL Simple Object”.
Click “Add” to bring up the ATL Simple Object Wizard
Enter “Demo” in the “Short name” field and go to the “Options” page
By selecting the “Free” threading model we tell COM that we will be able deal
with synchronization issues on our own. In this example we don’t want
to support Aggregation, but we do want to provide error handling support
using “ISupportErrorInfo” and “Connection Points” to provide notifications using COM events.
At this point we have a basic “do-nothing” COM service, and it’s time to start
adding functionality based on ACE, but first we need to tell Visual Studio
where it will find the include files that allows us to use ACE. Bring up the
properties dialog of the project and add $(ACE_ROOT) to the “Include directories”.
Go to the Linker->System page and change the “SubSystem” setting to “Console (/SUBSYSTEM:CONSOLE)”.
Something that’s useful during debugging, and harmless in production scenarios since a service is
not visible anyway. This step also allows us to use ACE_TMAIN(int argc, ACE_TCHAR* argv[])
as our entry point.
Open “stdafx.h” and add the following includes to the end of the file:
#include "ace/Log_Msg.h"
#include "ace/Svc_Handler.h"
#include "ace/Method_Request.h"
#include "ace/Activation_Queue.h"
#include "ace/Future.h"
#include <vector>
Open ACEATLDemo.cpp and add the following after the include section to tell the linker about the ACE library:
#ifndef _DEBUG
#pragma comment(lib,"ace")
#else
#pragma comment(lib,"aced")
#endif
At this point our project looks something like this:
Rebuild the project and we are ready to start implementing the COM service
based on functionality from ACE.
To make things interesting we will implement the core of our service as an
active object, where the functionality is executed asynchronously on a
separate thread. The class looks like this:
class CDemo;
class CDemoImpl : public ACE_Task_Base
{
ACE_Activation_Queue activation_queue_;
std::vector<CDemo*> clients_;
public:
CDemoImpl(void);
~CDemoImpl(void);
virtual int svc (void);
int enqueue (ACE_Method_Request *request);
int exitImpl();
int postMessageImpl(CComBSTR text);
int registerImpl(CDemo *pDemo);
int unregisterImpl(CDemo *pDemo);
IntFuture callExit();
void callPostMessage(BSTR bstr);
IntFuture callRegister(CDemo *pDemo);
IntFuture callUnregister(CDemo *pDemo);
};
IntFuture is a simple typedef:
typedef ACE_Future<int> IntFuture;
A future is a construct that that allows us to wait on a possible future value.
ACE lets us use the following declaration to implement a singleton, which is
a construct that guaranties that there will be only one instance of
“CDemoImpl” accessible through “DemoImpl”.
typedef ACE_Singleton<CDemoImpl, ACE_Null_Mutex> DemoImpl;
The heart of the “CDemoImpl” class is the “svc” function:
int CDemoImpl::svc (void)
{
ACE_TRACE ("CDemoImpl::svc");
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (FAILED(hr))
{
ACE_ERROR ((LM_ERROR,
ACE_TEXT ("CoInitializeEx failed - returned:%d\n"),hr));
return -1;
}
while (1)
{
auto_ptr
request (this->activation_queue_.dequeue ());
if (request->call () == -1)
{
break;
}
}
return 0;
}
Requests are dequeued from the activation queue and executed using their “call” function.
An “ACE_Method_Request” typically looks like this:
class CExitMethodRequest : public ACE_Method_Request
{
IntFuture result_;
public:
CExitMethodRequest(IntFuture& result)
: result_(result)
{
ACE_TRACE ("CExitMethodRequest::CExitMethodRequest");
}
~CExitMethodRequest( )
{
ACE_TRACE ("CExitMethodRequest::~CExitMethodRequest");
}
virtual int call (void)
{
ACE_TRACE ("CExitMethodRequest::call");
int result = DemoImpl::instance()->exitImpl();
result_.set(result);
return result;
}
};
The “call” function uses our “DemoImpl” singleton definition and
sets the value of the “IntFuture” “result_” making the result
available to the calling thread through the “IntFuture”.
The counterpart of the “svc” function is the “enqueue” function
int CDemoImpl::enqueue (ACE_Method_Request *request)
{
ACE_TRACE ("CDemoImpl::enqueue");
return this->activation_queue_.enqueue (request);
}
The “enqueue” function is used like this:
IntFuture CDemoImpl::callExit()
{
ACE_TRACE ("CDemoImpl::callExit");
IntFuture result;
CExitMethodRequest *request = new CExitMethodRequest(result);
enqueue (request);
return result;
}
Open ACEATLDemo.cpp and enter:
typedef ATL::CAtlServiceModuleT< CACEATLDemoModule, IDS_SERVICENAME > Inherited;
at the top of the “CACEATLDemoModule” class definition. Then add the following declarations to the class:
void RunMessageLoop() throw();
void OnStop() throw();
bool ParseCommandLine(LPCTSTR lpCmdLine,HRESULT* pnRetCode) throw();
and implement them like this:
void CACEATLDemoModule::RunMessageLoop() throw()
{
ACE_TRACE( "RunMessageLoop" );
ACE_Reactor::instance()->run_reactor_event_loop();
}
void CACEATLDemoModule::OnStop() throw()
{
ACE_TRACE( "OnStop" );
ACE_Reactor::instance()->end_reactor_event_loop();
IntFuture futureResult = DemoImpl::instance()->callExit();
int result = 0;
futureResult.get(result);
if(result != -1)
{
ACE_ERROR ((LM_ERROR,
ACE_TEXT ("callExit failed - returned:%d\n"),result));
}
DemoImpl::instance()->wait();
}
bool CACEATLDemoModule::ParseCommandLine(LPCTSTR lpCmdLine,
HRESULT* pnRetCode) throw()
{
ACE_TRACE( "ParseCommandLine" );
bool result = Inherited::ParseCommandLine(lpCmdLine,pnRetCode);
return result;
}
By implementing RunMessageLoop we effectively replace ATLs default implementation
and use the ACE reactor as a replacement for the standard message loop.
To provide for correct handling of the service control managers stop
event we need to implement the OnStop method too. Since “DemoImpl”
runs the “svc” function on a separate thread we use “callExit”
to tell the “svc” that it’s time to exit the request processing loop,
and call wait to ensure that the thread has completed it’s execution.
ParseCommandLine calls the default implementation using the “Inherited”
we added to the top of the “CACEATLDemoModule” class definition.
It’s here to show how it’s possible to “hook” up to ATLs processing of the command line.
To support simulation if the service control managers stop event we
implement an application handler routine for console control events
like Ctrl+C and Ctrl+Break.
BOOL WINAPI ConsoleCtrlHandler(DWORD dwCtrlType)
{
ACE_TRACE( "HandlerRoutine" );
_AtlModule.OnStop();
return TRUE;
}
Now we change the “_tWinMain” function to:
int ACE_TMAIN (int argc, ACE_TCHAR * argv[] )
{
ACE_TRACE("main");
int result = 0;
try
{
STARTUPINFO startupInfo = {sizeof(STARTUPINFO),0,};
GetStartupInfo(&startupInfo);
if(IsDebuggerPresent())
{
SetConsoleCtrlHandler(ConsoleCtrlHandler,TRUE);
HRESULT hr = _AtlModule.InitializeCom();
result = _AtlModule.Run(startupInfo.wShowWindow);
_AtlModule.UninitializeCom();
_AtlModule.Term();
SetConsoleCtrlHandler(ConsoleCtrlHandler,FALSE);
}
else
{
result = _AtlModule.WinMain(startupInfo.wShowWindow);
}
}
catch(...)
{
ACE_ERROR ((LM_ERROR, ACE_TEXT ("%p\n"),
ACE_TEXT ("Unknown exception in main")));
}
return result;
}
At this point we’ve created a working application that provides us with some
useful information during execution. When the application is executed under a
debugger it will always run as a console application, disregarding the
“LocalService” setting in the registry.
Open ACEATLDemo.idl and add
[id(1)] HRESULT PostMessage(BSTR messageText);
to the definition of the “IDemo” interface, and then open Demo.h and add
STDMETHOD(PostMessage)(BSTR messageText);
as a public method. Open Demo.cpp and implement the method:
STDMETHODIMP CDemo::PostMessage(BSTR messageText)
{
ACE_TRACE("CDemo::PostMessage");
DemoImpl::instance()->callPostMessage(messageText);
return S_OK;
}
The callPostMessage
function enques a CPostMessageMethodRequest
request on the activation queue.
void CDemoImpl::callPostMessage(BSTR bstr)
{
ACE_TRACE ("CDemoImpl::callPostMessage");
CPostMessageMethodRequest *request = new CPostMessageMethodRequest(bstr);
enqueue (request);
}
When the request is dequed, its call()
function
will invoke:
int CDemoImpl::postMessageImpl(CComBSTR text)
{
ACE_TRACE ("CDemoImpl::postMessageImpl");
for(vector::iterator it = clients_.begin();
it < clients_.end();
it++)
{
CDemo *pDemo = (*it);
pDemo->Fire_OnPostMessage(text.m_str);
}
return 0;
}
Implementing the Test Client
To test our server we need to develop a small test application. This is easily done using .Net and c#.
We implement the client like this:
public partial class MainForm : Form
{
ACEATLDemoLib.Demo demo;
public MainForm()
{
InitializeComponent();
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
demo = new ACEATLDemoLib.Demo();
demo.OnPostMessage += new ACEATLDemoLib._IDemoEvents_OnPostMessageEventHandler(demo_OnPostMessage);
}
delegate void demo_OnPostMessageDelegate(string messageText);
void demo_OnPostMessage(string messageText)
{
if (InvokeRequired)
{
BeginInvoke(new demo_OnPostMessageDelegate(demo_OnPostMessage), messageText);
}
else
{
messagesTextBox.AppendText(messageText + Environment.NewLine);
}
}
private void sendMessageButtonButton_Click(object sender, EventArgs e)
{
demo.PostMessage(messageTextBox.Text);
}
}
The form looks like this in Visual Studio 2010
Now we can start the server and a couple of instances of the client application.
Since we’ve enabled the ACE_TRACE macro the server will provide an interesting
view into its behavior.
Think of this project as a starting point, combining ACE or TAO with ATL allows us to create
software based on the functionality provided. Browse the documentation and you will find
quality implementations of some seriously challenging aspects of real-time software development.
Further reading
The book “The ACE Programmer's Guide: Practical Design Patterns for Network and
Systems Programming” by Stephen D. Huston, James CE Johnson, and Umar Syyid provides
an introduction to ACE development.