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>