Software developers usually provide a rich graphical user interface to easily run an application in a desktop environment. However, power users sometimes prefer a console application that typically provides access to the fullest range of application options via a command line interface and may be composed into a pipeline of UI-less processing steps. What if an application needs to be available both as a console application for power users and as a graphically rich application for typical users? This article presents techniques for accomplishing this using synchronous and asynchronous named pipes to control a UI-less console application from a separate process.
I am a research engineer at a consulting firm that performs forensic analysis of software source code. Law firms provide us with source code from companies involved in intellectual property disputes, and we use proprietary techniques and software tools to analyze the codebases and determine whether there is evidence of code plagiarism.
It is critical that our tools perform a thorough analysis so we can state our results with confidence. Our algorithms can take hours to perform a complete analysis even when running on high-end workstations.
A few months ago, my boss asked me to enhance one of our code analysis tools. He wanted a console application that would perform the analysis and a separate Windows application that would provide a graphical user interface to the console application.
The tool I was asked to enhance had already been implemented as two programs:
- The Code Analyzer, a console application written in C# and .NET, and
- The GUI Application, a Microsoft Windows application written in C++ and MFC.
A user could run the Code Analyzer from a command window, passing a source code file's path on the command line, to analyze the file with our proprietary algorithms. Alternatively, a user could analyze all the files in a directory tree by running the GUI application. The GUI app would recursively walk the tree and call the Code Analyzer, passing each file's path in the command line. This design had a problem: If there were thousands of files to analyze, the GUI Application would launch the Code Analyzer thousands of times, once per file. Clearly, this was not very efficient!
My first task was to move the directory traversal logic into the Code Analyzer and have the GUI application launch the Code Analyzer just once. Easy enough. My more interesting challenges were to have the Code Analyzer periodically inform the GUI Application of its progress so the latter could update a progress bar, and provide a "Stop" button so a user could stop a long-running job Figure 1 shows the final result. Clearly, the GUI app and the Code Analyzer would have to communicate with each other via an interprocess communication (IPC) mechanism. But which IPC technology should they use?
Choosing an IPC Technology
Over the years, Microsoft has introduced a variety of interprocess communication technologies, each having distinct advantages and disadvantages. Table 1 lists the main IPC technology choices available on the Windows platform today.
When selecting an IPC technology it is important to carefully consider your application's IPC requirements. My requirements were straightforward: I needed a technology that would allow a Windows application written in C++ to communicate with a .NET-based console application written in C#. This ruled out antiquated technologies like DDE, which .NET does not support. This also ruled out sophisticated technologies, such as Windows Sockets or WCF, which would have been overkill. I narrowed the choice down to anonymous pipes, named pipes, and memory-mapped files. Any of these would have worked, but I decided on named pipes for their richer synchronization support and to allow for the possibility of remotely executing the Code Analyzer over our LAN.
Pipes are a mature technology. They can be traced back to the UNIX operating system, which had unnamed pipes since 1972 and rudimentary named pipes by 1977 (Ritchie). Microsoft introduced named pipes in LAN Manager in the 1980s (Fisher), continued to support them in Windows NT and Windows 2000, and added security features to them in subsequent versions of Windows.
Named pipes are a core IPC technology built into the Windows client and server operating systems. They are exposed to application developers through both Windows APIs and .NET classes. When two processes want to communicate through a named pipe, one process, the pipe server, creates the pipe, and another process, the pipe client, connects to the pipe using its name. The two processes can then communicate by writing and reading messages from/to the pipe using APIs similar to those for file I/O. Conceptually, it's all very straightforward. In practice, getting all the synchronization logic in place is a bit more challenging, as we will explore in the next sections.
Using a Synchronous Named Pipe for Progress Reporting
Named pipes come in two varieties: synchronous and asynchronous. The difference has to do with how applications synchronize reading and writing. An unmanaged Windows application can read from a synchronous pipe by calling the
ReadFile API, whereas a .NET application calls the .NET
Read method. Both calls block until another thread writes to or closes the pipe. By contrast, when code calls the
ReadFile API on an asynchronous pipe the call returns immediately regardless of whether a message is in the pipe.
I decided to send progress report messages from the Code Analyzer using a synchronous named pipe because it would be easier to ensure that at most one message was in the pipe. This synchronous design was cleaner than handling a queue of messages in the pipe, which would have had edge cases such as the queue growing to the pipe's buffer size. The Code Analyzer sends a progress message each time it finishes processing some fraction, say 1%, of its files, after the GUI Application has read the previous message. Figure 2 shows a UML-style sequence diagram of the solution I implemented. There are two processes, the GUI Application and the Code Analyzer, and among them three threads.