Windows applications that do not export any program interface may be, however, converted to an automation server. The term "Automation" in this context means that clients running in separate processes are able to
control converted to server application,
use its some services in synchronous and/or asynchronous (by receiving event notifications) modes.
This is useful particularly in the case of old legacy and the third party applications. Their functionality and user interface may be upgraded, and their services become available for another applications. This may be achieved with COM object(s) injected to the process. The DLL injection technique in Win32 NT was described by Jeffrey Richter  and has already become almost conventional. Now we try to move a step further: to embed a COM object into a target application (now "promoted" to server) and, using this component, communicate with the application from outside of its process.
Brief Technique Description
Let's assume for simplicity that our target application is running in just one thread. This application has one main frame window having one client (view) window. To automate such an application, the following steps should be taken.
- A special Loader application injects a Plugin DLL into the target process using a remote thread. This is a worker thread, and its function is
DllMain() of the Plugin DLL. After
DllMain() returns, the thread will be terminated.
- The Plugin DLL contains also callback window procedures to subclass the frame and view windows of a target application. These window procedures include message handlers with new functionality for the target application.
DllMain() of the Plugin DLL actually performs the subclassings.
- New window procedure for, say, a frame window has a handler for an additional Plugin-specific Windows message,
DllMain() performs the target application window's subclassing, and after that posts the
WM_CREATE_OBJECT message to a frame window. Then
- Upon receiving the
WM_CREATE_OBJECT message, a new frame window procedure creates a COM object and calls its method for registration with Running Object Table (ROT). If asynchronous notification to client(s) is required then the embedded COM object should support the Connection Point mechanism and define outgoing sink interface in its IDL file. It is preferable to have the outgoing interface being an
IDispatch-based (or dual) one to allow a script client to implement it.
- Client applications obtain a proxy of the injected COM object via ROT, thus gaining control over the target application. To subscribe for the server's events notification, the clients should implement sink interface and marshal ("advise") its pointer to an embedded object.
To accomplish automation task information about the target application, the window class name is required. It may be revealed by using Spy utility, which is part of the Visual Studio installation. Another useful tool is ROT Viewer. It is also a Visual Studio utility. ROT viewer allows developer to inspect the ROT and therefore check the proper registration of a COM object embedded into the target process.
A well-known Notepad text editor is taken as a target application for automation.
The Loader.exe application is responsible for the injection of the NotepadPlugin.dll in Notepad.exe. Loader finds a running instance of Notepad (or starts a new instance in case no running instance is available), obtains its process handle, and actually performs the injection of NotepadPlugin.dll using a remote thread (please refer to [1, 2] and code sample for details).
A NotepadPlugin.dll is to be injected into Notepad.exe. Its
DllMain() method first finds the frame and view windows of Notepad.exe and then subclasses both of them with appropriate custom windows procedures. As it was stated above,
DllMain() is running not in the Notepad.exe main thread, but in an additional thread created remotely by the Loader application. This additional thread vanishes when NotepadPlugin.dll is completely loaded. A custom
WM_CREATE_OBJECT Windows message is posted to the frame window to initiate creation of the COM object for automation and its registration with ROT. The message
WM_CREATE_OBJECT handler of a new frame window procedure initializes COM, creates NotepadHandler COM object, and calls its appropriate method for its ROT registration.
A COM in-process server component, NotepadHandler.dll, implements the dual-interface
IHandler, specially tailored for our custom Notepad automation.
IHandler has methods to register/unregister the object with the ROT. The outgoing source dual interface
IHandlerEvents is added to the NotepadHandler project (in file NotepadHandler.idl). The interface contains method
HRESULT SentenceCompleted([in] BSTR bsText). The NotepadHandler component implements the connection point mechanism (appropriate code may be added by activating of the Implement Connection Point... item in the right-click menu on
CHandler in ClassView tab of Visual Studio workspace and the dialog followed).
According to my scenario (just for illustration), the server (embedded COM object) picks all Notepad editor keyboard input symbols (including non-alphabetic ones). As soon as one of the end-of-the-sentence symbols (".", "?", and "!" characters) appears, the server
Fire_SentenceCompleted() event, thus providing all the event's subscribers with the sentence completed (content of appropriate buffer).
Below two variants of clients, for Win32 and .NET platforms are discussed. Each of them supports two ROT registration approaches: ActiveObject and moniker registration. Registration details may be seen with the ROT Viewer tool.
The AutomClient.exe application is a sample of an Win32 automation client. It creates a proxy for the NotepadHandler COM object by using the component's ROT registration and actually controls the automated instance of Notepad through the
IHandler interface implemented by NotepadHandler.
The client application process should implement the
IHandlerEvents interface. It may be done in various ways. I choose to create a special COM component (in-process server), Notification. To construct it, the additional project NotepadHandlerNotification was added to the workspace with the ATL COM AppWizard. Then an ATL object was inserted into the component, using the right-click menu. This object implements additional an interface that I called
INotification. This interface is not a compulsory one, but may be useful to provide the component with data from the client application via appropriate methods (I supply to the component handle of the client application's main window that way). The
CNotification class implements
IHandlerEvents interface (by activating the Implement Interaface... right-click menu item followed by the corresponding dialog). A client application class
CAdvise contains advising mechanism. The method
CAutomationClientDlg::OnButtonAutomate() contains code responsible for advising.
Implemented in the Notification component, the
SentenceCompleted() method of the
IHandlerEvents interface is called by the server. The application main window handle (in this case) is supplied to the Notification component with the
SetMainWnd() method of the
INotification interface. Having this handle in its possession, the
SentenceCompleted() method posts a notification message to the main window of the client application, passing a pointer to a buffer with data received from the server.
By default the ActiveObject registration is used. To switch to moniker registration, definition of _MONIKER should be uncommented in file .\Include\DefineMONIKER.h before compilation.
The AutomClientNET.exe application is a sample of a .NET automation client. From user perspective it acts similarly to AutomClient.exe application. AutomClientNET.exe application uses RotHelper assembly classes to create .NET Interop proxy for the NotepadHandler COM object. The RotHelper and NotepadHandler Interop (as NOTEPADHANDLERLib) have to be added to Referances of AutomClientNET project. This is done using standard VS.NET Add Reference... dialog: RotHelper through Projects tab, and through NOTEPADHANDLERLib selecting the NotepadHandler Type Library in COM tab.
By default the ActiveObject registration is used. To switch to moniker registration, definition of _MONIKER should be uncommented (in the beginning of file .\AutomationClientNET\NotepadClientForm.cs) before compilation.
Running the Test
An already compiled demo is available for the test sample. Please note that before you run it, the NotepadHandler.dll and NotepadHandlerNotification.dll COM components has to be registered with the regsvr32.exe utility. For the registration, you have to run the file Register Components.bat, located in the demo directory. Then one or more copies of AutomationClient.exe and AutomationClientNET.exe may be started.
By pressing the NOTEPAD AUTOMATE button, client internally starts the Loader.exe application. The latter starts Notepad and automates it. A word [Automated] appears in caption of Notepad main window. Alternatively, you may run Notepad manually before the AutomationClient.exe. In this case, Loader automates the already running instance of Notepad. As soon as Notepad has been automated client subscribes to its Sentence Completed event.
Now you may type some text in the automated Notepad instance and press the Copy Text button of the client application. The last text fragment you typed will appear in the edit box of the client application. To simplify things, only actually typed characters and symbols are copied, not copied-and-pasted text. Pressing the Find and Append Menu buttons of the client causes corresponding Notepad response.
If user types some characters followed by a sentence conclusion sign (".", "?", or "!"), the character sequence will be reproduced in an edit box of all clients' applications subscribed to the event. The automated Notepad sends ("fires") the Sentence Completed event to its subscribers (clients) as soon as any character from the set .?! is input. Upon this event, the appropriate buffer content is sent to subscribers and displayed by them.
On exit from Notepad, a "Bye-Bye..." message box is generated by NotepadPlugin.dll on behalf of Notepad to amuse user :) .
Compiling the Test
The test sample consists of NotepadAuto workspace for Visual C++ 6.0 and AutomationClientNET solution for Visual Studio .NET (files NotepadAuto.dsw and AutomationClientNET.sln are located in main directory of the source). VC++ 6.0 stuff is completely independent from .NET solution and may be tested separately.
First, file NotepadAuto.dsw should be loaded to VC++ 6.0 Studio and _Build_All_Projects project should be built. Then, if .NET client is of your interest, file AutomationClientNET.sln has to be loaded to VS.NET and its projects should be built (be sure that references to RotHelper and NOTEPADHANDLERLib interop are added to references of AutomationClientNET project).
The psapi library contains
EnumProcesses() functions is required for a system-wide search for particular windows and processes. So, if psapi.dll file is not installed in your system directory then it should be copied from .\Psapi_library directory either to system directory, or it should be made loadable in some other way (e.g., by coping to directories available through environmental path variable, working directories or with Visual Studio's Tools->Option dialog->Directories). It is also assumed that Notepad.exe is located in your [Windows]\System32 directory. After the above arrangements have been made, you may build the _Build_All_Projects project (the rest are its dependants) and run the sample.
The AutomationClient.exe application should be run to test the Win32 sample. Please note that sample should be tested outside Visual Studio. The AutomationClientNET.exe application should be run to test the .NET sample. This test may be performed from VS.NET too.
Some Ways for Further Development
|User interface (UI) upgrade||The frame and view windows of a target application may be subclassed by an MFC-aware plugin DLL. This allows considerable UI upgrade, like, for example, adding toolbars to an old-style application.|
|Actions Sequence||The client may implement a sequence of commands given to the target application. This sequence could be implemented with some scripting.|
|Configurator||An object embedded into the target process may serve as an "objects factory." With its help, another COM object(s) may be created within the target process to accomplish various specific tasks. The object factory component obtains data for new objects' construction through its methods and/or Registry.|
|Part of distributed system||Automated applications may be included to distributed systems (see e.g., ).|
A technique for automation of Windows applications that do not export program interface is presented. Usage of COM objects in a DLL injected into a target process allows developer to automate such applications. This approach is useful to upgrade functionality and GUI of target applications (particularly, legacy applications) and expose their services to out-process clients. Source sample demonstrates such an automation for both Win32 (MFC) and .NET (C#) clients.
 Jeffrey Richter. Advanced Windows. Third edition. Microsoft Press, 1997.
 John Peloquin. Remote Library Loading.
 Dino Esposito. Add Object Models to Non-COM Apps.
The .NET client is added. ROT registration of injected COM object may be carried out with ActiveObject or moniker. Text of the article is changed to reflect changes in code.
My thanks to all people who read this article and expressed their opinion and suggestions. My special thanks to Alex Furman for his refinement of RotHelper.Moniker class.