Use Named Pipes and Shared Memory for inter process communication with a child process or two
I wanted to inject some very low impact code that would run in any “parent” process, like Notepad or Excel or Visual Studio. I wanted to have some User Interface for the data that my injected code gathered about the parent process, and that would work best in a different “child” process, preferably using WPF.
In the old days, I could call a COM server to handle the job. A DLL server would be in process, but it could be made out of process by making it a COM+ application (see Blogs get 300 hits per hour: Visual FoxPro can count. and Create multiple threads from within your application).
.Net Remoting seemed to be a little heavyweight for Parent->Child process communication.
About 5 years ago, I wrote this: Use Named Pipes to communicate between processes or machines, so I thought I’d use a combination of Named Pipes and Shared Memory. Luckily .Net 3.5 added support for Named Pipes making the child process pretty simple.
Pipes could be used to send messages, and the lion’s share of data movement could be in the Shared memory.
Synchronization and lifetime management are a little tedious. We want the parent process to continue optionally if the child process terminates, but we want the child to terminate automatically when the parent terminates for any reason. Similarly, the child process should terminate if the parent has gone.
This sample shows a parent process in C++ and 2 child processes in C# and VB. The parent spins off a thread to use to service incoming requests from the children. Events are used to synchronize communication. A timer in each child process fires off requests to the parent.
I was using Visual Studio 2010: you can use VS 2008, but you’ll have to adjust for some of the new features I use, especially in the VB code.
Start Visual Studio. File->New->Project->C++ Win32 Project->Windows Application. In the wizard, Click Add common header files for ATL
Now hit F5 to build and see it execute: there’s a window and a couple menus.
Now add a second EXE project to your solution: choose File->Add->New Project->VB WPF Application. (Repeat to add a 3rd project for C# !)
Fiddle with the Project->Properties->Compile->Build Output path so it builds into the same folder as the parent exe (for me, it was” ..\Debug\”)
Paste in the VB code below into MainWindow.Xaml.Vb
Somewhere inside the _tWinMain of your CPP project, add these 2 lines to instantiate a class that calls the WpfApplication as a child process, with a shared memory size of 2048 (make sure to change the name of the EXE to match your VB and C# EXEs):
CreateChildProcess opCreateChildProcessCS(_T("NamedPipesCS.exe"),2048, 1);
CreateChildProcess opCreateChildProcessVB(_T("NamedPipesVB.exe"),2048, 2);
Paste the CPP code below before the _tWinMain.
F5 will show both processes launched. You can alt-tab between the 2: they behave like independent processes. Try terminating one of them.
If you uncomment the MsgBox, then hit F5, you can actually use VS to attach to a child process before it does too much. Try attaching to all 3!
See also:
Remove double spaces from pasted code samples in blog
<C++ Code>
// CreateChildProcess : class in parent process to instantiate and communicate with a child process
// usage: CreateChildProcess opCreateChildProcess(_T("WpfApplication1.exe"),2048);
class CreateChildProcess
{
HANDLE m_hChildProcess;// handle to the child process we create
HANDLE m_hNamedPipe; // handle to the named pipe the paren process creates
HANDLE m_hEvent;
HANDLE m_hThread; // thread in parent process to communicate with child
LPVOID m_pvMappedSection;
DWORD m_cbSharedMem;
public:
CreateChildProcess(TCHAR* szChildExeFileName,DWORD cbSharedMemSize, int ChildNo )
{
m_cbSharedMem = cbSharedMemSize;
TCHAR szPipeName[1000];
TCHAR szEventName[1000];
swprintf_s(szPipeName, L"Pipe%d_%d", ChildNo, GetCurrentProcessId()); //make the names unique per child and per our (parent) process
swprintf_s(szEventName,L"Event%d_%d", ChildNo, GetCurrentProcessId()); //
SECURITY_ATTRIBUTES SecurityAttributes = {
sizeof( SECURITY_ATTRIBUTES ), // nLength
NULL, // lpSecurityDescriptor. NULL = default for calling process
TRUE // bInheritHandle
};
HANDLE hFileMapping = CreateFileMapping(
INVALID_HANDLE_VALUE, // backed by paging file
&SecurityAttributes,
PAGE_READWRITE,
0,
static_cast< DWORD >(m_cbSharedMem), // Truncation in 64-bit
NULL);
m_pvMappedSection = MapViewOfFile( hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0 );
swprintf_s((TCHAR *)m_pvMappedSection, m_cbSharedMem,_T("here i am in shared mem %d"), 1);
CComBSTR bstrFullPipeName(L"\\\\.\\pipe\\");
bstrFullPipeName.Append(szPipeName); // the pipe name is "\\.\pipe\MyName"
m_hNamedPipe = CreateNamedPipe(
bstrFullPipeName,
PIPE_ACCESS_DUPLEX + FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_MESSAGE + PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
100, //nOutBufferSize
100, //nInBufferSize
100, // nDefaultTimeout
0); // lpSecurityAttributes
m_hEvent = CreateEvent(
&SecurityAttributes,
FALSE, //bManualReset
FALSE, //bInitialState
szEventName); // name
STARTUPINFO StartupInfo = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION ProcessInformation = {};
TCHAR achCommandLine[MAX_PATH*2];
// like WpfApplication1.exe 92 Event2648 Pipe2648 2048
swprintf_s( achCommandLine, _T("%s %d %s %s %d"),szChildExeFileName, hFileMapping, szPipeName, szEventName, m_cbSharedMem);
if( !CreateProcess(
NULL,
achCommandLine,
NULL,
NULL,
TRUE, // inherit handles
CREATE_DEFAULT_ERROR_MODE | NORMAL_PRIORITY_CLASS,
NULL,
NULL,
&StartupInfo,
&ProcessInformation))
{
OutputDebugString(L"failed to create process");
return;
}
::CloseHandle( ProcessInformation.hThread );// We don't need the thread handle
DWORD dwThreadId = 0;
m_hChildProcess = ProcessInformation.hProcess;
// Create the actual background thread in the parent process
m_hThread = CreateThread(NULL, // no security
0, // default stack size
CreateChildProcess::ThreadProc, // initial method
(void *)static_cast<CreateChildProcess *>(this), // parameter to the thread proc
0, // run immediately
&dwThreadId);
//DWORD res = ::WaitForSingleObject( hThread, INFINITE );
return ;
}
static DWORD WINAPI ThreadProc(void *pvThreadObject)
{
CreateChildProcess *pCreateChildProcess = static_cast<CreateChildProcess *>(pvThreadObject);
BOOL fDone = false;
while (!fDone)
{
DWORD res = ::WaitForSingleObject( pCreateChildProcess ->m_hEvent, 2000 );
switch(res)
{
case WAIT_OBJECT_0: // the event signalled: we got a request
{
DWORD nTotBytesAvail = 0;
PeekNamedPipe(pCreateChildProcess ->m_hNamedPipe,0,0,0,&nTotBytesAvail,0);
if (nTotBytesAvail != 0)
{
char data[1000];
DWORD nRead = 0;
//read the msg
if (ReadFile(pCreateChildProcess->m_hNamedPipe,data,nTotBytesAvail, &nRead,NULL))
{
data[nTotBytesAvail] = 0; //null terminate
CComBSTR bstrdat( data);
OutputDebugString(bstrdat);
OutputDebugString(L"\n");
if (_wcsnicmp(L"Quit",bstrdat,4)==0)
{
OutputDebugString(L"Child Proc sent Quittin' time msg\n");
fDone=true;
exit(0);
}
// write some stuff to shared mem
swprintf_s(
(wchar_t *)pCreateChildProcess->m_pvMappedSection,
pCreateChildProcess->m_cbSharedMem,
L"Written to shared mem %s", bstrdat);
// send a msg to the child
data[0]='C';
data[1]='+';
data[2]='+';
DWORD nBytesWritten= 0;
WriteFile( pCreateChildProcess->m_hNamedPipe, data, nTotBytesAvail,&nBytesWritten,0);
}
}
}
break;
case WAIT_TIMEOUT:
// test to see if child process still alive
if (WaitForSingleObject(pCreateChildProcess->m_hChildProcess,0) == WAIT_OBJECT_0) // did the proc terminate?
{
fDone = true;
}
break;
default:
fDone=true;
}
}
// interprocess comm is over
CloseHandle(pCreateChildProcess->m_hChildProcess);
pCreateChildProcess->m_hChildProcess = INVALID_HANDLE_VALUE;
CloseHandle(pCreateChildProcess->m_hThread);
CloseHandle(pCreateChildProcess->m_hNamedPipe);
return 0;
}
};
</C++ Code>
<VB Code>
Imports System.IO.Pipes
Imports System.Runtime.InteropServices
Imports System.Windows.Threading
Imports System.Text
Module NativeImports
Public Const FILE_MAP_READ As Int32 = &H4
Public Const FILE_MAP_WRITE As Int32 = &H2
Declare Function MapViewOfFile Lib "kernel32" (ByVal hFileMappingObject As IntPtr, ByVal dwDesiredAccess As UInt32, ByVal dwFileOffsetHigh As UInt32, ByVal dwFileOffsetLow As UInt32, ByVal dwNumberOfBytesToMap As UInt32) As UInt32
End Module
Class MainWindow
Private WithEvents _timer As New DispatcherTimer With {.Interval = TimeSpan.FromSeconds(2)}
Private _pipestream As NamedPipeClientStream
Private _event As System.Threading.EventWaitHandle
Private _sharedMemAddr As UInteger
Private _sharedMemSize As Integer
Private _txtStatus As New TextBox With {
.AcceptsReturn = True,
.AcceptsTab = True,
.VerticalScrollBarVisibility = ScrollBarVisibility.Auto
}
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
' MsgBox("attach a debugger", "VB")
Me.Title = "VB Child Process"
Me.Content = _txtStatus
Dim args = System.Environment.GetCommandLineArgs
If args.Count < 4 Then
MessageBox.Show("No args")
Close()
Return
End If
Dim hFileMapping = CInt(args(1))
_sharedMemAddr = MapViewOfFile(hFileMapping, FILE_MAP_READ Or FILE_MAP_WRITE, 0, 0, 0)
Dim pipename = args(2)
_pipestream = New NamedPipeClientStream(".", pipename,
PipeDirection.InOut,
PipeOptions.Asynchronous)
Dim eventname = args(3)
_event = System.Threading.ManualResetEvent.OpenExisting(eventname)
_sharedMemSize = CInt(args(4))
_pipestream.Connect()
UpdateStatus("start: shared mem " + ReadSharedMem())
_timer.Start()
End Sub
Shared _numticks As Integer
Private _byteEncoding As New UTF8Encoding
Private _IsClosed = False
Private _decoder = _byteEncoding.GetDecoder
Sub OnTimerTick() Handles _timer.Tick
Try
If _pipestream Is Nothing Or Not _pipestream.IsConnected Then
DoForceClose()
Return
End If
SendMsg("WPFClient msg" + _numticks.ToString + " " + DateTime.Now.ToLongTimeString)
_numticks += 1
Dim str = GetMsg()
UpdateStatus("Client got data: " + str)
Dim ss = ReadSharedMem()
UpdateStatus("shared mem: " + ss)
Catch ex As IO.IOException ' a named pipe IO op failed
DoForceClose()
Catch ex As Exception
DoForceClose()
End Try
End Sub
Sub DoForceClose()
If Not _IsClosed Then
_IsClosed = True
_timer = Nothing
_pipestream = Nothing
UpdateStatus("Disconnected")
Me.Close()
End If
End Sub
Sub SendMsg(ByVal szMsg As String)
Dim barray = _byteEncoding.GetBytes(szMsg)
_pipestream.Write(barray, 0, barray.Length)
_event.Set()
End Sub
Function GetMsg() As String
Dim str = ""
Dim barrRead(90) As Byte
Dim charsRead(90) As Char
Dim nBytesRead = _pipestream.Read(barrRead, 0, barrRead.Length)
Dim nChars = _decoder.GetChars(barrRead, 0, nBytesRead, charsRead, 0)
str = New String(charsRead, 0, nChars)
Return str
End Function
Function ReadSharedMem() As String
Dim sResult = ""
For i = 0 To _sharedMemSize Step 2 ' unicode
Dim aaddr = New IntPtr(_sharedMemAddr + i)
Dim aByte = Marshal.ReadByte(aaddr)
If aByte = 0 Then
Exit For
End If
sResult += Chr(aByte)
Marshal.WriteByte(aaddr, aByte + 1) ' demo: change the mem we just read: under a debugger you can see that shared mem was written
Next
Return sResult
End Function
Private Sub UpdateStatus(ByVal newStat As String)
newStat = DateTime.Now.ToString + " " + newStat + vbCrLf
Debug.Write(newStat)
Me._txtStatus.AppendText(newStat)
Me._txtStatus.ScrollToEnd()
End Sub
Sub On_Closed() Handles Me.Closed
_timer = Nothing
If Not _IsClosed Then
SendMsg("Quit")
End If
If _pipestream IsNot Nothing Then
_pipestream.Dispose()
_pipestream = Nothing
End If
End Sub
End Class
</VB Code>
<C# Code>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO.Pipes;
using System.Runtime.InteropServices;
namespace NamedPipesCS
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr MapViewOfFile(
IntPtr hFileMappingObject,
FileMapAccess dwDesiredAccess,
uint dwFileOffsetHigh,
uint dwFileOffsetLow,
uint dwNumberOfBytesToMap);
[Flags]
public enum FileMapAccess : uint
{
FileMapCopy = 0x0001,
FileMapWrite = 0x0002,
FileMapRead = 0x0004,
FileMapAllAccess = 0x001f,
fileMapExecute = 0x0020,
}
private TextBox _txtStatus;
private IntPtr _sharedMemAddr;
private NamedPipeClientStream _pipestream;
private System.Threading.EventWaitHandle _event;
private int _sharedMemSize;
private System.Windows.Threading.DispatcherTimer _timer;
static int _numticks;
private UTF8Encoding _byteEncoding = new UTF8Encoding();
private bool _IsClosed;
private Decoder _decoder;
public MainWindow()
{
//MessageBox.Show("attach a debugger","C#");
InitializeComponent();
_decoder = _byteEncoding.GetDecoder();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_txtStatus = new TextBox();
_txtStatus.AcceptsReturn = true;
_txtStatus.AcceptsTab = true;
_txtStatus.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
this.Title = "C# Child Process";
this.Content = _txtStatus;
var args = Environment.GetCommandLineArgs();
if (args.Count() < 4)
{
MessageBox.Show("No args");
Close();
return;
}
var hFileMapping = int.Parse(args[1]);
_sharedMemAddr = MapViewOfFile(new IntPtr(hFileMapping), FileMapAccess.FileMapRead | FileMapAccess.FileMapWrite, 0, 0, 0);
var pipeName = args[2];
_pipestream = new System.IO.Pipes.NamedPipeClientStream(".", pipeName, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);
var Eventname = args[3];
_event = System.Threading.ManualResetEvent.OpenExisting(Eventname);
_sharedMemSize = int.Parse(args[4]);
_pipestream.Connect();
UpdateStatus(string.Format("start: shared mem {0}" , ReadSharedMem()));
_timer = new System.Windows.Threading.DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(2);
_timer.Tick += new EventHandler(OnTimerTick);
_timer.Start();
this.Closed += new EventHandler(on_Closed);
}
void OnTimerTick(Object o, EventArgs e)
{
try
{
if (_pipestream == null || !_pipestream.IsConnected)
{
DoForceClose();
return;
}
SendMsg(String.Format("WPFClient msg {0} {1}" , _numticks, DateTime.Now.ToLongTimeString()));
_numticks += 1;
var str = GetMsg();
UpdateStatus(String.Format("Client got data: {0}" , str));
var ss = ReadSharedMem();
UpdateStatus(String.Format("shared mem: {0}" , ss));
}
catch (Exception)
{
DoForceClose();
}
}
void DoForceClose()
{
if (!_IsClosed)
{
_IsClosed = true;
_timer = null;
_pipestream = null;
UpdateStatus("Diconnected");
Close();
}
}
void SendMsg(string szMsg)
{
var barray = _byteEncoding.GetBytes(szMsg);
_pipestream.Write(barray, 0, barray.Length);
_event.Set();
}
string GetMsg()
{
var str = "";
var barrRead =new Byte[90];
var charsRead = new char[90];
var nByteRead = _pipestream.Read(barrRead, 0, barrRead.Length);
var nChars = _decoder.GetChars(barrRead, 0, nByteRead, charsRead, 0);
str = new string(charsRead, 0, nChars);
return str;
}
String ReadSharedMem()
{
var sResult = "";
for (int i = 0; i < _sharedMemSize; i += 2)
{
var aaddr = _sharedMemAddr + i;
var aByte = Marshal.ReadByte(aaddr);
if (aByte == 0)
{
break;
}
sResult += Convert.ToChar(aByte);
Marshal.WriteByte(aaddr,(byte)(aByte+(byte)1));// ' demo: change the mem we just read: under a debugger you can see that shared mem was written
}
return sResult;
}
private void UpdateStatus(String newStat)
{
newStat =string.Format("{0} {1}\n", DateTime.Now.ToString(),newStat);
System.Diagnostics.Debug.Write(newStat);
_txtStatus.AppendText(newStat);
_txtStatus.ScrollToEnd();
}
void on_Closed(Object o, EventArgs e)
{
_timer = null;
if (!_IsClosed)
{
SendMsg("Quit");
}
if (_pipestream != null)
{
_pipestream.Dispose();
_pipestream = null;
}
}
}
}
</C# Code>