SendMessageのプロセス間通信でエラー

解決


SAL  2012-10-11 07:27:49  No: 147875  IP: [192.*.*.*]

SendMessageでプロセス間通信をしたい


プログラムA(送信側)からプログラムB(受信側)にWM_COPYDATAを使い、
3秒(Timer1)に1回文字列を送りたい。
環境:Windows7 Pro , VB 2010 Express

下記プログラムAにおいて  ret = SendMessage・・・  の所で
  「PInvokeStackImbalance が検出されました。
  Message: PInvoke 関数 'Test_WatchDog!Test_WatchDog.Form1::SendMessage' がスタックを不安定にしています。
  PInvoke シグネチャがアンマネージ ターゲット シグネチャに一致していないことが原因として考えられます。
  呼び出し規約、および PInvoke シグネチャのパラメーターがターゲットのアンマネージ シグネチャに
  一致していることを確認してください。」
のエラーが発生する。

Public Class Form1
    Private Const WM_COPYDATA As Integer = &H4A
    Private WH_WatchDog As IntPtr

    Public Structure COPYDATASTRUCT
        Public dwData As Integer
        Public cbData As Integer
        Public lpData As String
    End Structure

    Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
        (ByVal lpClassName As String, _
         ByVal lpWindowName As String) As IntPtr

    Private Declare Ansi Function SendMessage Lib "user32" Alias "SendMessageA" _
        (ByVal hwnd As IntPtr, _
         ByVal msg As Integer, _
         ByVal wParam As Integer, _
         ByVal lParam As COPYDATASTRUCT) As Integer


    Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
        Dim txt As String = "Dog"
        Dim ret As Integer

        WH_WatchDog = FindWindow(vbNullString, "WatchDog")  '受信側のハンドル取得

        If WH_WatchDog <> 0 Then
             Dim bytearry() As Byte = _
                          System.Text.Encoding.Default.GetBytes(txt)
            Dim len As Integer = bytearry.Length
            Dim cds As COPYDATASTRUCT
            cds.dwData = 0
            cds.cbData = len + 1
            cds.lpData = txt
 
            ret = SendMessage(WH_WatchDog, WM_COPYDATA, 0, cds)    'ここでエラー発生
        End If
    End Sub
End Class

解決のアドバイスをよろしくお願いいたします。

編集 削除
オショウ  2012-10-11 09:01:36  No: 147876  IP: [192.*.*.*]

ByVal lParam As COPYDATASTRUCT

ByRef じゃ〜ないの?
http://www.geocities.jp/hatanero/sendmessage1.html

因みに、コード自体は酷似していますが、そこのコードを
コピーしたわけではない?

以上。

編集 削除
魔界の仮面弁士  2012-10-11 10:39:03  No: 147877  IP: [192.*.*.*]

> Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
SendMessageA や FindWindowA ではなく、Declare Unicode Function で
SendMessageW や FindWindowW を用いることをお奨めします。
(あるいは、Auto 指定を使う手もあります)

Ansi 指定でも間違いでは無いのですが、Win98 等で動かすのでもなければ、
今更 A 系 API を呼び出す利点は無い気がします。


> ByVal lParam As COPYDATASTRUCT) As Integer
WM_COPYDATA が求める lParam の型は、COPYDATASTRUCT のポインタです。すなわち、
  (a案) COPYDATASTRUCT を Class として宣言し、ByVal で渡す。
  (b案) COPYDATASTRUCT を Structure として宣言し、ByRef で渡す。
  (c案) COPYDATASTRUCT 相当のバイナリの先頭アドレスを渡す。
などが求められるわけですが、現在のコードはいずれにも合致していません。


> Public Structure COPYDATASTRUCT
API に渡す Class や Structure には、StructureLayout 属性を付与した方が安全です。
特に String をメンバーに含む場合には、Charset も一緒に指定しましょう。


> Public Structure COPYDATASTRUCT
>     Public dwData As Integer
>     Public cbData As Integer
>     Public lpData As String
> End Structure

COPYDATASTRUCT の本来の宣言は、下記のようになっています。

typedef struct tagCOPYDATASTRUCT {
  ULONG_PTR dwData;
  DWORD     cbData;
  PVOID     lpData;
} COPYDATASTRUCT, *PCOPYDATASTRUCT;

dwData は Integer ではなく、IntPtr とするのが正しいです。
dwData のサイズは、Win32 と Win64 で異なることに注意してください。

また、lpData に渡せるデータは、文字列とは限りません。
汎用的にするなら、lpData As IntPtr として宣言しておき、
そこに文字列を渡す場合には、Marshal.StringToHGlobal〜 と
Marshal.FreeHGlobal を使うようにします。

文字列しか渡さない場合は、lpData As String にすることも可能ですが、
その場合は、lpData に MarshalAs 属性を付与しておいた方が良いでしょう。

下記のスレッドも参照してみてください。
http://madia.world.coocan.jp/cgi-bin/VBBBS2/wwwlng.cgi?print+201205/12050001.txt

編集 削除
SAL  2012-10-11 12:11:20  No: 147878  IP: [192.*.*.*]

オショウ様、魔界の仮面弁士様
早速のアドバイスありがとうございました。
ご指摘の箇所を修正することにより無事動作するようになりました。
詳細は再度報告いたしますが、とりあえず途中結果報告まで。

編集 削除
SAL  2012-10-12 10:18:25  No: 147879  IP: [192.*.*.*]

その後の結果報告です

オショウ様
>ByRef じゃ〜ないの?

間違えました。ByRefにすることによりエラーはでなくなり希望の動作になりました。


>因みに、コード自体は酷似していますが、そこのコードを
>コピーしたわけではない?

その通りです。コピーし元にしましたが、他の場所でエラーが出たため色々変更し
間違えたようです。

魔界の仮面弁士様
>SendMessageW や FindWindowW を用いることをお奨めします。

今までSendMessageA や FindWindowAしか使用したことがありませんでしたが、
SendMessageW,FindWindowWを使用して希望の動作になることを確認しました。

>汎用的にするなら、lpData As IntPtr として宣言しておき、
>そこに文字列を渡す場合には、Marshal.StringToHGlobal〜 と
>Marshal.FreeHGlobal を使うようにします。

Marshalの使い方がよくわからないため、もう少し勉強しようと思います。

最終的に以下のようにして正常動作しています。
お二方、本当にありがとうございました。

Public Class Form1
    Private Const WM_COPYDATA As Integer = &H4A
    Private WH_WatchDog As IntPtr

    Public Structure COPYDATASTRUCT
        Public dwData As IntPtr
        Public cbData As Integer
        Public lpData As String
    End Structure

    Private Declare Unicode Function FindWindow Lib "user32" Alias "FindWindowW" _
        (ByVal lpClassName As String, _
         ByVal lpWindowName As String) As IntPtr

    Private Declare Unicode Function SendMessage Lib "user32" Alias "SendMessageW" _
         (ByVal hwnd As IntPtr, _
          ByVal msg As Integer, _
          ByVal wParam As Integer, _
          ByRef lParam As COPYDATASTRUCT) As Integer

    Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
        Dim txt As String = "Dog"
        Dim ret As Integer

        WH_WatchDog = FindWindow(vbNullString, "WatchDog")  '受信側のハンドル取得

        If WH_WatchDog <> 0 Then
            Dim bytearry() As Byte = _
                         System.Text.Encoding.Default.GetBytes(txt)
            Dim len As Integer = bytearry.Length
            Dim cds As COPYDATASTRUCT
            cds.dwData = 0
            cds.cbData = len + 1
            cds.lpData = txt

            ret = SendMessage(WH_WatchDog, WM_COPYDATA, 0, cds)
        End If
    End Sub
End Class

編集 削除
魔界の仮面弁士  2012-10-12 13:40:24  No: 147880  IP: [192.*.*.*]

> 今までSendMessageA や FindWindowAしか使用したことがありませんでしたが、
> SendMessageW,FindWindowWを使用して希望の動作になることを確認しました。

FindWindowA の場合、Shift_JIS で表現できない文字列、たとえば
  Dim nihao As String = ChrW(&H4F60) & "好"
  Dim cubicMeter As String = ChrW(&H33A5)
などが使われているウィンドウを処理することができません。

今回は A でも W でも動作させれらるとは思いますが、Win98 系向けの
VB.NET アプリを作るわけでは無いのなら、A 系を採用するメリットは
あまり無いと思います。


> ByVal wParam As Integer, _
Int32 型を採用されているようですが、WPARAM のサイズは
4バイト固定ではなく、Win16/Win32/Win64 環境で異なる事に注意してください。

WPARAM/LPARAM のサイズはポインターの幅であるため、本来は
wParam/lParam 共に IntPtr で宣言されるべきものです。

さらに言えば、SendMessage の戻り値である LRESULT 型についても、
本来は IntPtr とすることが求められます。


ただし 32bit 環境でしか実行されない場合に限っては、
ByVal wParam As Integer を用いることもできます。

4 バイト幅の環境で動作することを保証したいのであれば、今回のコードを
x86 ビルドにてコンパイルするようにしてください。AnyCPU でビルドするなら
IntPtr 型で宣言する必要があります。


> If WH_WatchDog <> 0 Then
これだと、Option Strict On モードでコンパイルが通らなくなります。

WH_WatchDog は Integer ではなく IntPtr なので、
比較するべきは 0 ではなく、IntPtr.Zero となります。


>    cds.dwData = 0
これも同様の理由から、「cds.dwData = IntPtr.Zero」とすべきです。


>    cds.cbData = len + 1
+1 している理由は何ですか?


> Dim bytearry() As Byte = System.Text.Encoding.Default.GetBytes(txt)
> Dim len As Integer = bytearry.Length
String 型として lpData を用意するのであれば、COPYDATASTRUCT に
<StructLayout(…)> で Charset 指定を明示した方が良いでしょう。
Encoding.Default なら Charset.Ansi を指定しておくようにします。


なお、COPYDATASTRUCT.lpData に格納するデータを Unicode 文字列とするか
Shift_JIS 文字列とするかは、送信側と受信側で一致させておいてください。

編集 削除