C++では既存のコードを使うことや高速なソフトを開発することに優れている。一方、C#では簡単にフォームを作成し、グラフィカルユーザインターフェースを簡単に作成することができる。これらを両方の特長を活用することができれば、開発の幅を増やすことができる。このページでは1つのC++言語で記述されたウィンドウアプリケーションと、1つのC#で記述されたフォームアプリケーション間でプロセス間通信を行う方法について解説する。
C++アプリケーションをサーバーとし、C#アプリケーションをクライアントとする。クライアントは文字列をサーバーに送り、それに対してサーバーはクライアントに文字列を返すものとする。
サーバーではCreateNamedPipe関数を使用してパイプオブジェクトを作成する。ConnectNamedPipe関数を使用してクライアントからの接続を待つ。
クライアントはNamedPipeClientStreamクラスをインスタンス化し、Connectメソッドのによってサーバーと接続する。クライアントはstringクラスで表現される文字列をEncodingインスタンスのGetBytesメソッドに渡し、バイトシーケンスを得る。NamedPipeClientStreamインスタンスのWriteメソッドにバイトシーケンスを渡すことでサーバーへ文字列を送る。
サーバーはReadFile関数を呼び出し、クライアントのバイトシーケンスを読み取る。その後、WriteFile関数を呼び出しクライアントに文字列をバイトシーケンスで送る。
クライアントはサーバーから送られたバイトシーケンスをNamedPipeClientStreamインスタンスのReadメソッドで読み取り、それをEncodingインスタンスのGetStringメソッドに送ることでstringにエンコードする。
#include <Windows.h>#include <stdlib.h> // system("pause");#include <stdio.h>const char* const kPipeNameA = "\\\\.\\pipe\\MySimplePipe";int main(){ // 1024バイトで余裕あるサイズとする char szReadBuf[1024]; char szWriteBuf[1024]; HANDLE hPipe; DWORD dwRead; DWORD dwWritten; BOOL bRead; BOOL bWritten; int count = 0; // ▼パイプの作成 hPipe = CreateNamedPipeA( kPipeNameA, PIPE_ACCESS_DUPLEX, // 双方向パイプ 0, // パイプモード 1, // パイプに対する最大インスタンス 1024, // 出力バッファ 1024, // 入力バッファ 0, // ConnectNamedPipe関数を待つ、WaitNamedPipe関数のデフォルトタイムアウトミリ秒 NULL); if (hPipe == INVALID_HANDLE_VALUE) { printf_s("パイプ作成に失敗しました\n"); system("pause"); return 0; } printf_s("パイプを作成しました。\n\n"); while (1) // 外側のwhile文 { // ▼接続待機 printf_s("クライアントからの接続を待機します...\n"); if (ConnectNamedPipe(hPipe, NULL) == 0) { printf_s("待機関数が失敗しました。\n"); return 0; } printf_s("クライアントとの接続に成功しました。\n"); while (1) // 内側のwhile文 { count++; // ▼受信 memset(szReadBuf, '\0', sizeof(szReadBuf)); bRead = ReadFile(hPipe, szReadBuf, sizeof(szReadBuf), &dwRead, NULL); if (bRead == FALSE) { printf_s("読み取りに失敗しました。\n\n"); break; } printf_s("受信文字列:%s\n", szReadBuf); // ▼送信 // 受信文字列にカウントを付加する sprintf_s(szWriteBuf, "%d %s", count, szReadBuf); bWritten = WriteFile(hPipe, szWriteBuf, strlen(szWriteBuf), &dwWritten, NULL); if (bRead == FALSE) { printf_s("書き込みに失敗しました。\n\n"); break; } printf_s("送信文字列:%s\n", szWriteBuf); } // while (1) // 内側のwhile文 DisconnectNamedPipe(hPipe); // パイプの再利用 } // while (1) // 外側のwhile文 printf_s("ループを抜けました\n"); CloseHandle(hPipe); system("pause"); return 0;}using System;using System.Text;using System.IO.Pipes;using System.IO;namespace SimpleReplyPipeClient{ class Program { static void Main(string[] args) { // ▼起動 自動的にサーバーを立ち上げる。 /*string serverDir = "C:\\Users"; string serverNameWithoutExe = "SimpleReplyPipeServer"; Process[] ps = Process.GetProcessesByName(serverNameWithoutExe); if (ps.Length > 0) { Console.WriteLine("サーバーは起動済みです。"); } else { Process.Start(serverDir + "\\" + serverNameWithoutExe); }*/ // ▼接続 using (NamedPipeClientStream client = new NamedPipeClientStream(".", "MySimplePipe", PipeDirection.InOut)) { Console.WriteLine("サーバーへ接続中..."); int timeOutMs = 1000; try { client.Connect(timeOutMs); } catch (TimeoutException) { Console.WriteLine("タイムアウトしました(" + timeOutMs + "ミリ秒)。"); return; } catch (UnauthorizedAccessException) { // NamedPipeClientStreamデストラクタのPipeDirectionがサーバーと合わない場合発生する。 // PIPE_ACCESS_INBOUND(1) もしくは PIPE_ACCESS_OUTBOUND(2) いずれかのみの場合。 // サーバー側は、PIPE_ACCESS_DUPLEX(3)を指定すること Console.WriteLine("UnauthorizedAccessException(アクセス権がない)が発生しました。"); return; } catch (IOException) { Console.WriteLine("サーバーが別のクライアントに接続されています。"); return; } Console.WriteLine("サーバーに接続しました。\n"); // エンコーダを作成 Encoding sjisEncoding = Encoding.GetEncoding("Shift_JIS"); for (int i = 0; i < 3; i++) { // ▼送信 string sendString = "hoge"; byte[] outBuffer = sjisEncoding.GetBytes(sendString); int len = outBuffer.Length; Console.WriteLine("送信文字列:{0}", sendString); try { client.Write(outBuffer, 0, len); } catch (IOException) { Console.WriteLine("サーバー停止中を含め、パイプ先が閉じられています。"); return; } client.Flush(); // ▼受信 // バッファサイズは最大でも 1024 バイトにする const int kMaxBuffer = 1024; byte[] inBuffer = new byte[kMaxBuffer]; int readedLength; try { readedLength = client.Read(inBuffer, 0, kMaxBuffer); } catch (IOException) { Console.WriteLine("サーバー停止中を含め、パイプ先が閉じられています。"); return; } Array.Resize(ref inBuffer, readedLength); string receiveString = sjisEncoding.GetString(inBuffer); Console.WriteLine("受信文字列:{0}", receiveString); } // for } Console.WriteLine("\n何らかのキーを押すと終了します..."); Console.ReadKey(true); } }}サーバーを起動し、クライアントを起動すると以下のようになる。左側がクライアント、右側がサーバーである。
サーバーとクライアントは接続後、クライアントは文字列 hoge をサーバーに送り、サーバーはその文字列の先頭に count 変数と追加し、クライアントに文字列を送っている。クライアントは for 文でループし、3回送受信を行った後、キー入力を1回受け付けてプログラムを終了する。サーバーはクライアントが終了すると接続が切れるため ReadFile 関数はFALSEを返す。
ここでサーバーはそのままで、クライアントを再度起動すると以下のようになる。
サーバーのパイプは再利用されており、 count 変数が引き継がれていることが分かる。4が飛んでいますが、読み込み前にインクリメントし、その読み込みが失敗した後インクリメントして再度読み込むためである。
名前付きパイプを作成するには、Win32APIではCreateNamedPipe関数、.NETライブラリではNamedPipeClientStream クラスを使用する。パイプは読み込みと、書き込みのみ、双方向の3種類から指定する。CreateNamedPipe関数では第2引数、NamedPipeClientStream コンストラクタでは第3引数で指定する。
パイプ方向の引数/列挙体
読み込みのみ
書き込みのみ
双方向
DWORD dwPipeMode
PIPE_ACCESS_INBOUND
PIPE_ACCESS_OUTBOUND
PIPE_ACCESS_DUPLEX
PipeDirection
In
Out
InOut
組み合わせとしては
つまり、サーバーの指定に、クライアントは矛盾のない指定をする必要がある。今回はPIPE_ACCESS_DUPLEXとInOutを指定し、双方向の通信が可能になっている。
サーバーはConnectNamedPipe関数でクライアントからの接続を待つ。クライアントはNamedPipeClientStreamクラスのConnectメソッドでサーバーと接続する。サーバーから接続を切るにはDisconnectNamedPipe関数を使用する。このプログラムではクライアントが終了することでサーバーの接続が切れるが、ConnectNamedPipe関数を再度使うことで、パイプを再利用することができ、今回はこの実装になっている。サーバー側でパイプを作り直すにはCloseHandle関数でパイプを閉じ、再度CreateNamedPipe関数でパイプを作成する。
また、C#ではNamedPipeClientStreamクラスはusingステートメントで自動的なリソース解放が可能である。
using (NamedPipeClientStream client = new NamedPipeClientStream(".", "MySimplePipe", PipeDirection.InOut)){}接続ができれば、パイプを使って通信が可能になる。サーバーは読み込みにReadFile関数、書き込みにはWriteFile関数を使用する。同様にクライアントはReadメソッド、Writeメソッドを使用する。ただし、パイプにはバイトストリーム形式、つまり byte[]型にエンコードして送信する必要がある。Encoding.GetEncoding("Shift_JIS")でShift_JIS形式へ変換するエンコーダを作成できる。ユニコードにしたいならEncoding.Unicode、UTF8にしたいならEncoding.UTF8にすればよい。そして、GetBytesメソッドで文字列をbyte[]型にエンコードし、パイプから読み込んだバイトシーケンスはGetStringメソッドでデコードできる。
ReadメソッドとWriteメソッドはストリームクラスから呼び出せるメソッドである。NamedPipeClientStreamの継承を確認しよう。NamedPipeClientStream : PipeStream : Stream : MarshalByRefObject, IDisposable となっており、NamedPipeClientStreamクラスは Streamクラスが継承されていることが分かる。