モチベーション
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を使いたいなぁ〜