メッセージによるプロセス間通信

前回は、メモリマップトファイルを用いた共有メモリによってプロセス間のデータ共有を行う方法を説明しましたが、今回はウィンドウメッセージを用いた方法を説明していきましょう。

原則:別プロセスにポインタは渡せない

前回にも説明したように、各プロセスは独立した仮想アドレス空間を持っているため、あるプロセスAでデータが格納されているメモリブロックのアドレスを別のプロセスBに渡したとしても、そのアドレスはプロセスBでは無効なものとなっています。メモリの共有には、メモリマップトファイルのように特殊な方法を使用しなければなりません。

これはウィンドウメッセージを送信またはポストする際にも言えることで、パラメータにメモリアドレスを含む種類のウィンドウメッセージは、原則として、プロセスを超えて送ることができないようになっています。つまり、プロセスを超えてメッセージを送る場合には、wParamパラメータやlParamパラメータにメモリアドレスが含まれていてはいけないということです。

もしメモリアドレスを含むメッセージを送ってしまうと、送信先では、自分のプロセスの、実際にはデータが存在しないアドレス空間にアクセスしようとして、強制終了してしまう可能性があります。すなわち、メッセージを送信した方ではなく、受け取った方のプロセスが終了してしまうという事態に陥りかねないのです。

ウィンドウメッセージによるプロセス間通信

データ量が8バイトまでなら:WM_USER / WM_APP

Windowsでは、システムが定義しているさまざまなメッセージコードのほかに、アプリケーションが自由に意味を定義して使用することができるメッセージコードというものを準備しています。具体的には、以下の範囲で目的別に使用できるようにしているのです。

範囲 意味
0x00000x03FF システムが使用するために予約されているメッセージ
0x04000x7FFF プライベートウィンドウクラスが使用するためのメッセージ
0x80000xBFFF アプリケーションが使用することのできるメッセージ
0xC0000xFFFF アプリケーションが使用するための文字列メッセージ
0x10000 予約されています

これらのうち、0x04000xBFFFの範囲で自由にメッセージコードの意味を定義して使用することができます。コード0x0400WM_USERと、コード0x8000WM_APPとそれぞれ定義されており、新しい定数名を定義する際に、WM_USER+XあるいはWM_APP+X (Xは0以上の整数)いう形で定義できるようになっています。

#define  WM_USER        0x0400
#define  WM_APP         0x8000

ここで、WM_USERの範囲とWM_APPの範囲に分けられているのは、WM_USERの範囲のメッセージコードはWindowsのコントロール(ボタンやエディットコントロールなど)が使用しているメッセージコードと重なる部分があり、WM_APPの範囲のコードではそのような競合がないということです。ただし、今回の場合はコントロールに送ったりするわけではないので、これらの違いをあまり意識する必要はないと思います。

ただし、Windows 95/98/Meでは、ダイアログ制御メッセージとの衝突を避けるためにWM_USER+0x0100(すなわち0x0500)より大きい値を定義しなければならないことになっています。おそらく、ダイアログ制御メッセージはダイアログ用ウィンドウプロシージャで使用されるだけで、通常のウィンドウでは使用されないと考えられますが、すべての環境でうまく動作させるため、とりあえずそのような値を使用した方がよいでしょう。

これらのメッセージコードを使用してメッセージのやり取りをする際に、wParamメッセージやlParamメッセージに任意の値を指定することができます。したがって、一回のメッセージ送信で、同時に8バイトまでのデータを送信することができるということです。8バイトより大きいデータを送る場合に、複数回に分けてメッセージを送るという手段をとっているソフトウェアもあるようです。

それぞれ送信側・受信側の処理は以下のようになります。(ただし、送信先ウィンドウハンドルの取得を行う処理をしていないので、このスクリプトのままでは動作しません。送信先ウィンドウの取得については後述。)

; ==== 送信側 (注意:このままでは動作しません) ====
; データ送信用のメッセージを新しく定義
#define WM_USER             0x0400
#define MYWM_SENDDATA       (WM_USER + 0x0200)

; データ送信
hwndTarget = (送信先のウィンドウハンドル)
data1 = 1111                ; 送信データ1
data2 = 2222                ; 送信データ2
sendmsg hwndTarget, MYWM_SENDDATA, data1, data2
; ==== 受信側 (注意:このままでは動作しません) ====
; データ送信用のメッセージを新しく定義
#define WM_USER             0x0400
#define MYWM_SENDDATA       (WM_USER + 0x0200)

; MYWM_SENDDATAメッセージ受信設定
oncmd gosub *OnReceiveData, MYWM_SENDDATA
; (中略)

*OnReceiveData
; データ受信
data1 = wParam              ; 送信データ1
data2 = lParam              ; 送信データ2
return

互換性を利用して:WM_SETTEXT / WM_GETTEXT

Windowsには、メモリアドレスを渡すにもかかわらず、プロセスを超えて渡すことができるメッセージがいくつか存在します。そのうちの代表的なものはWM_SETTEXTおよびWM_GETTEXTです。これらのメッセージは、ウィンドウに関連付けられている文字列の設定や取得を行うメッセージです。

タイトルバーがついている通常のウィンドウに送信すると、ウィンドウのタイトル文字列が対象となります。また、エディットボックスやボタンなどのなどのコントロールに送信すると、それらのコントロールに表示されている文字列が対象となります。

これらのメッセージは、16ビット時代からの互換性のために、プロセスを超えて送られた場合でも上手く動作するように実装されています。したがって、プロセス間でやり取りする情報が文字列だけであるならば、ユーザーからは見えないエディットコントロールを準備して、それを介してプロセス間通信を行うということも可能なのです。

任意サイズのバイナリデータを渡す:WM_COPYDATA

これとは別に、ウィンドウメッセージを介してプロセス間通信を行うことを目的としているメッセージがあります。それが、WM_COPYDATAメッセージです。

WM_COPYDATAメッセージは任意のサイズのバイナリデータを別のウィンドウへ送信する機能を持ちます。送信先ウィンドウが送信元とは異なるプロセスに属していても動作するようになっています。

実際にはこのメッセージは、前回のメモリマップトファイルを介した共有メモリによる通信を内部で行っています。私たちプログラマからはそれが見えないようになっているだけです。しかし、そのおかげで私たちは、共有メモリなどの概念を考えることなく、このメッセージを使って簡単にプロセス間通信を行うことができるのです。

このメッセージの使用方法としては、送信側でSendMessage関数を使ってWM_COPYDATAメッセージを送信し、受信側のウィンドウプロシージャで同じくWM_COPYDATAメッセージに対する処理を行うことになります。

《送信側の処理》

WM_COPYDATAメッセージのメッセージコードは0x004Aです。wParamパラメータには、データ送信側(自分自身のウィンドウハンドル)を指定する必要があります。また、lParamパラメータには、渡されるデータに関する情報を格納したCOPYDATASTRUCT構造体のアドレスを指定します。

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

dwDataメンバでは任意の整数値を1つ渡すことができます。そして、lpDataには、渡されるバイナリデータが格納されているメモリブロックのアドレスを指定します。また、そのデータサイズをバイト単位でcbDataメンバに指定しておく必要があります。dwDataメンバには、lpDataが指すメモリブロックに格納されているデータがどんな種類のデータであるかを識別するIDを格納しておくと便利です。

送信側はこのメッセージをSendMessage関数を使って送信しなければいけません。PostMessage関数によりメッセージをポストすることはできません。

hwndTarget = (送信先のウィンドウハンドル)

data = 1, 2, 3, 4, 5        ; 送信データを格納
data_size = 20              ; 送信データサイズ(バイト単位)
data_id   = 0               ; データ識別値

; COPYDATASTRUCT 構造体
dim cds, 3
cds(0) = data_id
cds(1) = data_size
cds(2) = varptr(data)
; WM_COPYDATA メッセージ送信
sendmsg hwndTarget, WM_COPYDATA, hwnd, varptr(cds)

今回のようにウィンドウメッセージを介してデータ通信を行う場合には、何らかの方法で送信先のウィンドウハンドルを取得しておかなければなりません。これについては後で述べます。

《受信側の処理》

次に受け取る側の処理についてです。送信側がWM_COPYDATAメッセージを送信すると、受信側では同じWM_COPYDATAメッセージを受け取ることになります。したがって、ウィンドウプロシージャでデータ受信処理を行うことになります。HSPの場合にはWM_COPYDATAメッセージを受け取ったときに割り込み処理を行うように設定しておき、割り込みが発生したところで受信処理を行うということです。

; メッセージ割り込み処理の設定
oncmd gosub *OnCopyData, WM_COPYDATA

メッセージを受け取ったときのwParamパラメータやlParamパラメータについては送信側と同様、wParamには送信側のウィンドウハンドルが、lParamパラメータにはCOPYDATASTRUCT構造体のアドレスが指定されています。もちろん、内部での共有メモリ作成やアドレス変換により、送信側とまったく同じアドレスになっているということはありませんが。

受信側では、COPYDATASTRUCT構造体のdwDataメンバから、どんなデータが送られてきたかを判断して(1種類のデータしかやり取りしない場合は必要ないでしょうが)、lpDataメンバのメモリアドレスが指すメモリブロックに対しての処理を行うことになります。

ここで注意しておくべきことは、受け取ったデータのメモリブロックは、メッセージ処理を行っている間のみ有効であって、同じアドレスに後でアクセスすることはできないということです。HSPの場合には、割り込み処理が終了した時点でこれらのアドレスは無効になるため、あとでこれらのアドレスからデータを読み取ることはできないということになります。したがって、後でデータが必要になる場合には、別の変数にデータを格納しておく必要があります。

*OnCopyData
; COPYDATASTRUCT 構造体.
dupptr cds, lParam, 12
data_id   = cds(0)
data_size = cds(1)
dupptr data_ref, cds(2), data_size

; データ識別値(dwDataメンバの値)ごとの処理.
if data_id == 0 {
    ; ここで処理するのでなければ別の変数にコピーしておく.
    dim data, data_size/4
    memcpy data, data_ref, data_size
}
dim cds, 1
dim data_ref, 1
return 0

通信相手のウィンドウを探すには

実際にウィンドウメッセージを送信するためには、送信先のウィンドウのハンドルを取得しなければなりません。ハンドルを取得する方法はいくつか考えられます。

親プロセスからコマンドパラメータとして渡す

多くの場合、データ共有が必要になるのは、親プロセスがサブプロセスを起動してデータを引き継がせる場合でしょう。ウィンドウハンドルはどのプロセスからも同じ値で参照されるため、このようにプロセス間通信を行う相手プロセスをサブプロセスとして新しく起動する場合には、コマンドラインパラメータとして親プロセスのウィンドウハンドルを渡すことができます。この場合には、サブプロセスが(少なくとも最初の)メッセージ送信側となります。

ウィンドウタイトルから検索する

すでに起動しているプロセスのウィンドウハンドルを得る方法として、ウィンドウタイトルから取得する方法があります。これには、user32.dllが提供するFindWindow関数を使います。

HWND FindWindowA(
    LPCTSTR pClassName,   // ウィンドウクラス名.
    LPCTSTR pWindowName   // ウィンドウタイトル.
);

1つ目のpClassNameパラメータにはウィンドウクラス名を表す文字列のアドレスを指定します。クラス名を使わずウィンドウタイトルだけで対象ウィンドウの検索を行う場合には0 (NULL) を指定することができます。HSP3の場合、ウィンドウクラス名は「hspwnd0」なので、これを指定しておいた方がよいでしょう。

2つ目のpWindowNameパラメータにはウィンドウタイトルを表す文字列のアドレスを指定します。ウィンドウタイトルを使わずウィンドウクラス名だけで対象ウィンドウの検索を行う場合には0 (NULL) を指定することができます。ただし、クラス名「hspwnd0」だけを指定してウィンドウを検索すると、HSP3で作成されたすべてのアプリケーションのすべてのウィンドウ(ID1以降も含めて)がヒット対象となるので、同時にタイトル文字列も指定しておく必要があります。

; == サンプル:受信側 ==
#define WM_USER       0x0400
#define MYWM_TEST     (WM_USER+0x0101)
; タイトル文字列を固有のものに変える
title "HSP TestApp for SendMessage"
oncmd gosub *OnReceive, MYWM_TEST
stop
*OnReceive
mes "メッセージを受け取りました"
return
; == サンプル:送信側 ==
#include "user32.as"
#define WM_USER       0x0400
#define MYWM_TEST     (WM_USER+0x0101)
; 固有のタイトル文字列を検索
FindWindow "hspwnd0", "HSP TestApp for SendMessage"
hTarget = stat
if hTarget == 0 {
    dialog "ウィンドウが見つかりません",1,"エラー"
    end
}
sendmsg hTarget, MYWM_TEST, 0, 0
mes "メッセージを送信しました"

もし指定されたウィンドウが存在しないとFindWindow関数は0 (NULL) を返します。そこで上記のサンプルでは、送信先ウィンドウが見つからなかったときにエラーが起こったことを知らせるメッセージボックスを表示しています。

この方法でのウィンドウハンドル取得では問題が発生することがあります。FindWindow関数はウィンドウタイトルが指定されたものと完全に一致するものに対してヒットさせるので、何らかの理由でウィンドウタイトルが変えられていた場合にはヒットしなくなるということです。ソフトウェアの中には、他のソフトウェアのウィンドウタイトルを変えてしまうものが存在するため、注意が必要です。

もうひとつの問題として、同じタイトルをもったウィンドウが他にも存在する可能性があるということです。アプリケーション固有のウィンドウタイトルにしておけばほとんど問題ないと思いますが、もし偶然にも同じタイトル文字列を持つウィンドウが存在した場合には、そちらが取得されてしまう可能性があります。

全ウィンドウにコンタクトしてみる:ブロードキャスト

別のアプローチとして、「とりあえず存在するウィンドウすべてにメッセージを送信してみて、相手側からの反応を待つ」というものがあります。ウィンドウメッセージを発生させるSendMessage関数やPostMessage関数では、ウィンドウハンドルの代わりに0xFFFF (HWND_BROADCAST) を指定することによって、デスクトップ上のすべてのトップレベルウィンドウにメッセージを送ることができるようになっています(ブロードキャストメッセージ)。これを使って全ウィンドウにメッセージを送ってみるということです。

ただし、ブロードキャストメッセージを使用するには注意が必要で、もしこのメッセージをSendMessage関数で送信した場合、システム上のどれか1つのウィンドウでもハングアップ(フリーズ)していると、こちらのプログラムもまたハングアップしてしまう可能性があるということです。PostMessage関数を使う場合にはその危険性はなくなりますが、同期(タイミング)のとり方などが少し難しくなる可能性があります。

メッセージの送信において、SendMessage関数の代わりにSendMessageTimeout関数を使うことによって、メッセージ処理の最大待ち時間を指定することができるようになっています。この関数では、メッセージ送信先がハングアップ状態であっても、待ち時間が過ぎると制御が戻るようになっています。ブロードキャストメッセージを使用する場合にはSendMessageTimeout関数を使用するほうがよいでしょう。

ブロードキャストメッセージを送信する場合、上記のようなWM_APP+X(あるいはWM_USER+X)のような値を使用してはいけません。他のアプリケーションは、これらのメッセージコードを別の意味で使用している可能性があるためです。

ブロードキャストメッセージを送信する際に使用するメッセージコードは、システム上で一意である(それ以外の別の意味を持っていない)必要があります。そのようなメッセージコードは、user32.dllRegisterWindowMessageを使って取得することができます。

UINT RegisterWindowMessageA(
    LPCTSTR pString      // メッセージ文字列.
);

この関数は、システム上で一意のメッセージを登録するためのもので、固有の名前を渡すことにより、その名前に対応したメッセージコードを取得することができます。どのプロセスからでも、同じ名前を渡すことによって、同じメッセージコードを受け取ることができるようになっています。この関数で取得されるメッセージは文字列メッセージなどと呼ばれます。

以下のスクリプトは、ブロードキャストメッセージを使って相手のウィンドウを探す簡単なサンプルです。

; == サンプル:送信側 ==
#include "user32.as"
#define WM_USER       0x0400
#define MYWM_RECEIVE  (WM_USER+0x0101)

screen 0, 500, 200
title "送信側 : hwnd="+hwnd
oncmd gosub *OnReceive, MYWM_RECEIVE
button "送信", *Send
stop
*Send
; ブロードキャスト送信
mes "ブロードキャストメッセージを送信"
RegisterWindowMessage "HSP_Broadcast_Test"
msgcode = stat
sendmsg 0xFFFF, msgcode, 0, hwnd
mes "ブロードキャストメッセージを送信終了"
stop
*OnReceive
hTarget = lParam
mes "受信側からメッセージを受け取りました : 受信側 hwnd = "+hTarget
return
; == サンプル:受信側 ==
#include "user32.as"
#define WM_USER       0x0400
#define MYWM_RECEIVE  (WM_USER+0x0101)

screen 0, 500, 200
title "受信側 : hwnd="+hwnd
; ブロードキャスト受信設定
RegisterWindowMessage "HSP_Broadcast_Test"
msgcode = stat
oncmd gosub *OnReceiveBroadcast, msgcode
stop
*OnReceiveBroadcast
hTarget = lParam
mes "送信側からメッセージを受け取りました : 送信側 hwnd = "+hTarget
mes "送信側にMYWM_RECEIVEメッセージを返します"
sendmsg hTarget, MYWM_RECEIVE, 0, hwnd
return

まず、送信側が「HSP_Broadcast_Test」文字列メッセージ(RegisterWindowMessage関数で登録したもの)をブロードキャストメッセージとして全ウィンドウに送信しています。受信側がこれを受け取ると、今度は返信メッセージ(ここではMYWM_RECEIVEと定義)を返しています。どちらのメッセージでもlParamパラメータに自分自身のウィンドウハンドルを渡すことによって、ウィンドウハンドルを知らせるようにしています。このハンドルを保存しておけば、これ以降でウィンドウメッセージによる通信を続けることができるようになります。ブロードキャストメッセージは若干の負担がかかるため、これを使用するのは最初の1回のみにとどめておいた方が無難です。