知っている人は知っているし、その辺にあるサンプルプログラムを適当に実行してもそれはそれで動くんだけど・・・・
まぁあたしもそれで使っていたんだけど、実装に際し、色々と細かい問題点が有ることも判っている。
これはWindowsがどれだけ多言語に対応しているかという所にも拠る。
ようするに使うVSや、使う.net Frameworkのバージョンによっても動作は変わってくる。
これらを正しく問題なく使えるようにするにはやはり、正規のやり方が重要になってくる。
public string lpData;
とか定義してある物は、現在は使えても、その後使えなくなる可能性がある。
日本語、英語、だけならともかく、CHKやアラビア文字、ヨーロッパ文字やらロシア文字などが含まれてくればなおさら。
Windowsにおけるchar型は色々と不確定で、文字列としてはやはりstringを利用するのが推奨で有り、そう言った多言語をAPIで利用するのであれば、Unicodeの32やUTF-8へ変換するのがいい。
UTF-8は現在も拡張され続けているUnicodeの全ての文字を単一コードで表現出来る唯一の文字コードで有り、Unicode16等の古い文字列がコードページ次第で正常に表現出来ないのとは違う。
さて、その上でどうしようか?となる。
当然ながら文字列はUTF-8で有ることが良いけれど、C#はこの文字列をそのままで利用しているつもりでも内部の動作はUnicode16か32に置き換えられている。
また、プロセス間通信を可能にするWM_COPYDATAでは送信されるデータについては一切関知していない。
WM_COPYDATAはCOPYDATASTRUCTに定義されたデータを、該当プロセスのグローバルメモリにコピーしてから目的のアプリケーションに対して、通知している。
ようするに、直接送信されては居ない。
だから、適当にこの構造体にデータを代入しても、相手側には正しく届かない.
正しく送信するためにはどうすれば良いのかというと、
受信する側のアプリに合わせて文字列のデータを作成し、受信側は、送信側で送信したのと同じようにデータを受信する。
lpDataの正しい型式である、LPVOIDはc#では一般的には使われていない方で有り、コレは大概Intptrに置き換えられるけれど、作成したデータをポインタ型に変換出来ないC#ではどうすれば良いのかというと、Marshalのクラスを利用してポインタを作成しなければならない。
Marshalクラスは、いわゆる、アンマネージドメモリを操作するためにあるが、今回のWM_COPYDATAに添付するデータはアンマネージドメモリで有る必要は無い。
先ほどの通り、データはSendMessageで引き渡されたデータを相手方のメモリ空間に複製してからWM_COPYDATAで渡すから。
でもまぁ、C#ではこれ以外ポインタを利用出来ないので使うしか無い。
COPYDATASTRUCTはこんな感じ、
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public UInt32 cbData;
public IntPtr lpData;
}
LayoutKind.Sequentialを使わないと、最適化の際に順番が変わったりすることがあるので必須。
次に送信するデータの作成方法
これは文字列を送信するので、
Byte[] data = System.Text.Encoding.Unicode.GetBytes(sData);
でbyte配列型へ代入し、データを確定させます。
今回はUnicodeにしていますが、相手が利用する文字列に合わせる必要があります。
cd.lpData = System.Runtime.InteropServices.Marshal.AllocHGlobal(data.Length);
で書き込む領域を作成して、ポインタを取得します。
System.Runtime.InteropServices.Marshal.Copy(data, 0, cd.lpData, data.Length);
これでbyte配列からマーシャル領域へデータをコピーします。
cd.cbData = (uint)data.Length;
まぁ、これはいつ行っても良いのですが、送信するサイズを代入します。
これでデータは完成ですので、
SendMessage(hHwnd, WM_COPYDATA, (IntPtr)0, ref cd);
で送信して終わりです。
なお、
cd.dwDataは何を入れても構わないので、好きなデータを入れてください。
最後に
System.Runtime.InteropServices.Marshal.FreeHGlobal(cd.lpData);
でメモリ解放することをお忘れ無く。
ココで作成したメモリは明示的に開放しない限り開放されません。
特にHGlobalは使用出来る数に限界がありますので、HGlobalは多用しない方が良いです。
またAPIを直接利用する手もあります。
HeapAllocやLocalAllocを利用することで(c#でどれだけまともに動くかは判りません。)
より高速に動くメモリが確保出来ます。(Globalメモリの動作はとても遅い)
Marshalクラスがあるので使う意味はほぼ無いですが、GlobalAllocのAPIを利用することで殆ど同じ事が出来ます。というか、APIを直接使った方が優れています。
まとめ:
COPYDATASTRUCT cd;
cd = new COPYDATASTRUCT();
Byte[] data = System.Text.Encoding.Unicode.GetBytes(sData);
cd.lpData = System.Runtime.InteropServices.Marshal.AllocHGlobal(data.Length);
System.Runtime.InteropServices.Marshal.Copy(data, 0, cd.lpData, data.Length);
cd.cbData = (uint)data.Length;
cd.dwData = (IntPtr)0;
SendMessage(hHwnd, WM_COPYDATA, (IntPtr)0, ref cd);
System.Runtime.InteropServices.Marshal.FreeHGlobal(cd.lpData);
では受信する側
受信する側は、コレと反対のことをします。
COPYDATASTRUCT cd = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT));
でデータを取得します。
まず、アンマネージドメモリからコピーする為にbyte配列を作成します。
Byte[] data = new byte[cd.cbData];
System.Runtime.InteropServices.Marshal.Copy(cd.lpData, data, 0, (int)cd.cbData);
で、データをコピーして、
string sData = System.Text.Encoding.Unicode.GetString(data);
で、byte配列をUnicode文字列として取り出します。
これで終わりですね、
アンマネージドメモリはWindowsが確保してデータを送っていますので、C#側で開放してはいけません。
まとめ:
COPYDATASTRUCT cd = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT));
System.Runtime.InteropServices.Marshal.Copy(cd.lpData, data, 0, (int)cd.cbData);
string sData = System.Text.Encoding.Unicode.GetString(data);
受け取る側は簡単ですね〜
WM_COPYDATAはプロセス間通信の方法としては、とても簡単な機能ですが
お互いの送受信を制御しにくいと言う問題が有ります。
なので、WM_USER以降の番号を使ったメッセージの遣り取りなどを利用して、ハンドシェイク通信をすることをお勧めします。
まぁ、そう言うのであれば、WM_COPYDATAの遣り取りは、全くもって気軽に出来るものでは無いと言うことが判るかと思います。
また、上記の例を見ればc#で行うには、同じデータを何度も変換やコピーをする必要があり、あまり有効的な手段では無いことが判るかと思います。
文字列をunicodeに変換してからbyte配列へコピー
byte配列をアンマネージドメモリへコピー
WM_COPYDATAが送信先プロセスのメモリへコピー
アンマネージドメモリからbyte配列へコピー
byte配列をunicodeとして読み込みつつ文字列へコピー
とまぁ、5回もコピーしています。
アセンブラなどを利用すれば、もっと効率よく行えるんじゃ無いかと思わなくも無いのですが?
C#のプログラムじゃなくなっちゃうのでね?
まぁ、何はともあれIntPtrを素直に安全に使うのであれば、Marshalクラスを利用するしか有りません。
また、それとは別にガベージコレクションの対象外にしてメモリを固定する方法が有ったかと思うのですが?
ようするに、アンマネージコードとしてポインタを取得して利用する方法があったはずなのですが、忘れました(笑
Marshalクラス以外の方法でアンマネージ領域にアクセスするのは、C#の仕組みからして動作が遅くなるのでお勧めはしないのですが?それなりの量のアンマネージ状態で実行するコードが在るので在れば、それはそれで早くなるかも知れません。
まぁ、そんな感じで、WM_COPYDATAの話は終了。
それ以外のプロセス間通信としては、
COMやらDDEやらソケットが推奨。
パイプはソケットと比べて扱いが面倒なのであまりお勧めはしない。
|