モチベーション
C#でプロセス間通信をしたので、備忘録代わりに残しておきます。
普段の生活で(研究とかで)Windows Presentation Foundation (WPF)を使って簡単なソフトウェアを作ることはよくあります。大抵はUIスレッドのみで完結し、たまにマルチスレッドにするくらいで、マルチプロセスにすることなんて滅多にありません。 しかし、最近とあるDLL内の関数を同一のプロセスから叩くと、なぜか正常に機能しないということがありました。なので、仕方なくマルチプロセスにした、という次第です。
普段はmacを使っていて、WPFも完全には理解できていないので、変なところがあるかもしれませんが、コメントなりPRなり頂けると幸いです。
Windows Communication Foundation (WCF)
今回はWCFというものを使ってプロセス間通信を行います。WCFとはなにか、については↓
今回はこのWCFを使って、クライアントアプリからホストアプリ2つを呼び出す、という感じのものを作ります。
wcf_sample
今回作ったwcf_sampleについて軽く説明します。

Solution 'wcf_sample'はwcf_sample, host, wcfの3つから成ります。wcf_sampleとhostはWPFアプリで、wcfは.NET Frameworkのクラスライブラリです。
クライアントアプリwcf_sampleからホストアプリhostを呼び出すという感じの構成です。通信関連の処理はwcfにカプセル化し、wcf_sample, hostはwcfを参照しています。
WCFで通信を行うためには、System.ServiceModelのアセンブリを参照する必要があるので、wcfはSystem.ServiceModelを参照します。

実装するのは、以下の2つの内容です。
- ホストアプリに入力された内容を、クライアントアプリから取得する
- クライアントアプリの内容を、ホストアプリに反映させる
こんな感じで動きます。
https://www.youtube.com/watch?v=IELotyjpYF4
wcf
wcfではWCFに関連する処理をカプセル化しています。
含まれるのは、以下のwcf.csのみです。
using System; using System.ServiceModel; namespace wcf { public struct Data { public string text; public int number; } public class WCF { static string Address = "net.pipe://localhost/wcf_sample"; [ServiceContract(CallbackContract = typeof(ICallback))] public interface IHost { [OperationContract(IsOneWay = true)] void LoadData(); [OperationContract(IsOneWay = true)] void UpdateSlider(double val); } [ServiceContract] public interface ICallback { [OperationContract(IsOneWay = true)] void SendData(Data data); [OperationContract(IsOneWay = true)] void Send(); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)] public class Host : IHost { public delegate Data LoadDataListener(); public delegate void UpdateSliderListener(double val); private LoadDataListener loadDataListener; private UpdateSliderListener updateSliderListener; public void LoadData() { var data = loadDataListener(); ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>(); callback.SendData(data); } public void UpdateSlider(double val) { updateSliderListener(val); ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>(); callback.Send(); } public static void StartHost(string id, LoadDataListener loadDataListener, UpdateSliderListener updateSliderListener) { var host = new Host() { loadDataListener = loadDataListener, updateSliderListener = updateSliderListener }; var serviceHost = new ServiceHost(host); var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None); try { serviceHost.AddServiceEndpoint(typeof(IHost), binding, Address + id); serviceHost.Open(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } } public class Callback : ICallback { private string id; public delegate void DataHandler(string id, Data data); public delegate void Handler(string id); private DataHandler dataHandler; private Handler handler; public void SendData(Data data) { dataHandler(id, data); } public void Send() { handler(id); } public static IHost ConnectHost(string id, DataHandler dataHandler, Handler handler) { var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None); Callback callback = new Callback() { id = id, dataHandler = dataHandler, handler = handler }; var host = new DuplexChannelFactory<IHost>(callback, new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), new EndpointAddress(Address + id)).CreateChannel(); return host; } } } }
ホストからクライアントに送る内容はstruct Dataを定義しています。
IHostとICallbackでどのような形式で通信を行うかを定義します。この2つは別のソースファイルでも、名前空間が異なっていても構わないので、同一の内容である必要があります。
今回はwcfにまとめてしまっていますが、ホストアプリではIHostを実装したクラス(ここではHost)に実際に呼ばれたときの挙動を実装し、クライアントアプリではICallbackを実装したクラス(ここではCallback)にホストアプリからの応答を実装します。
今回の場合、クライアントアプリからIHost#LoadDataを叩くと、ホストアプリでHost#LoadDataが呼ばれ、その中でICallback#SendDataを叩くと、クライアントアプリのCallback#SendDataが呼ばれる、という流れです。
ホストアプリはホスティングをするためにServiceHost#openをする必要があります。ここでは、Host.StartHostに記述しています。
イメージとしてはサーバーを起動するような感じです。
クライアントアプリはホストに接続しに行きます。Callback.ConnectHostに記述しています。
今回のように双方向で通信を行う場合はDuplexChannelFactoryでIHostを取得します。この取得したIHostを使って、ホストアプリにリクエストを送るような感じになります。
WCFで接続をする際には、ホストとクライアントで共通でユニークなアドレスを指定する必要があります。
今回の場合2つのホストを起動するので、Address = "net.pipe://localhost/wcf_sample"にidを付与したものをアドレスとして指定しています。
このlocalhostにLAN内のIPを指定したら他のマシンのプロセスと通信できるんでしょうか?そのうち試してみたいと思います。
wcf_sample, host
wcf_sampleとhostはそれぞれ以下のような画面です。

wcf_sampleでReloadボタンを押すと、WCF経由でhostのTextとNumberのTexBoxに入れた内容が取得されます。
また、wcf_sampleのSliderを動かすと、その値がWCF経由でhostに送られ、hostのSliderに反映されます。
まず、クライアント側(wcf_sample)の主要な実装です。
private void StartHost() { var p1 = Process.Start(@"..\..\..\host\bin\Debug\host.exe", "1"); var p2 = Process.Start(@"..\..\..\host\bin\Debug\host.exe", "2"); p1.WaitForInputIdle(); p2.WaitForInputIdle(); hosts = new WCF.IHost[] { WCF.Callback.ConnectHost("1", HandleData, Handle), WCF.Callback.ConnectHost("2", HandleData, Handle) }; } private void HandleData(string id, Data data) { switch (id) { case "1": H1TextTextBox.Text = data.text; H1NumberTextBox.Text = data.number.ToString(); break; case "2": H2TextTextBox.Text = data.text; H2NumberTextBox.Text = data.number.ToString(); break; } Console.WriteLine("HandleData"); } private void Handle(string id) { Console.WriteLine("Handle"); } private void H1Load_Click(object sender, RoutedEventArgs e) { hosts[0].LoadData(); } private void H1Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { hosts[0].UpdateSlider(H1Slider.Value); }
wcf_sampleが起動されたときに、hostを2つ起動します。(多分パスの指定はもっといい方法があると思います)このとき、コマンドライン引数でidを渡します。("1"と"2")
hostが起動したら、接続して、返ってきたIHostを保持しておきます。
Reloadボタンが押されると、IHost#LoadDataが、Sliderの値が変更されるとIHost#UpdateSliderが叩かれます。
次にホスト側(host)の実装です。
public MainWindow() { InitializeComponent(); string[] args = Environment.GetCommandLineArgs(); if (args.Length >= 2) { id = args[1]; Title = "Host: " + id; } WCF.Host.StartHost(id, LoadData, UpdateSlider); } private Data LoadData() { var data = Dispatcher.Invoke(new Func<Data>(() => { int number = -1; int.TryParse(NumberTextBox.Text, out number); return new Data() { text = TextTextBox.Text, number = number }; })); return data; } private void UpdateSlider(double val) { Dispatcher.Invoke(new Action(() => Slider.Value = val)); }
ホストアプリが起動されると、Host.StartHostでホスティングを開始します。
クライアントアプリからIHost#LoadDataが呼ばれると、delegeteを経由して上記のLoadDataが呼ばれます。
ここで、TextBoxの内容を取得して、callbackによってクライアントに返ります。
1点注意すべきことは、WCF経由で呼ばれる処理はUIスレッドでは無いので、TextBoxなどのUIコンポーネントを普通に呼ぶとエラーが起きます。
なので、Dispatcher.Invokeを使いましょう。
How to run
hostをビルドしていないと、Process.Startができないので、まずhostをビルドしましょう。
その後、wcf_sampleを実行すれば動くはずです。
デバッグをする場合
wcf_sampleを実行すると、hostのデバッグがやりにくいですが、「デバッグ>プロセスにアタッチ」からプロセスにアタッチすると、ホストアプリでもブレークポイントを張ったりできます。

ホストアプリのほうでエラーが起きて、正常にCallbackに処理が返らないと、クライアント側でCommunicationObjectFaultedExceptionが出ます。
WCFの処理の途中でエラーが起きてもUIが固まったりしない(非UIスレッドなので)ので、わかりにくいですが、そういうときは、プロセスにアタッチすると良いでしょう。
あとがき
WPFは良いフレームワークだけど、C#を書くのがつらい…… SwiftでWPFを使いたいなぁ〜