SendMessageとPostMessage:デッドロック・タイムアウト・WM_COPYDATA

SendMessageが止まる理由とPostMessageとの違い、SendMessageTimeoutで固まりを防ぐ方法、WM_COPYDATAでプロセス間に文字列を送る方法、UIPIによる遮断まで。

2026年11月改訂: この記事は2021年に韓国語で書いた SendMessage の講座を、日本語向けに書き直したものです。元の記事には「wParam と lParam の既定値は0」とありましたが、Win32 の SendMessage に既定の引数はありません(既定値があるのは MFC の CWnd::SendMessage です)。デッドロックの説明も原因が混ざっていたので書き直しました。Visual Basic 6 のテスト用プログラムとコンソールのスクリーンショットは、今では再現できない環境なので外しています。C++ のコードはすべて MinGW-w64(GCC 16.2.0)で Windows 向けにコンパイルしましたが、執筆に使った Mac では実行していません。C# 版も .NET 10 SDK でビルドしただけです。実行結果は作らず、動作は Microsoft Learn のドキュメントに基づいて説明します。

ほかのアプリのボタンを押したり文字列を渡したりしようと SendMessage を使い、「自分のアプリまで固まる」「管理者権限のアプリだけ反応しない」「PostMessage に変えたらデータが化けた」と困っている人向けの記事です。先に結論を書くと、他プロセスには SendMessage ではなく SendMessageTimeout、結果の要らない通知は PostMessage、データは WM_COPYDATA を Send 系で送り、相手が管理者権限ならまず UIPI を疑う、の4点です。

SendMessage の形と、2021年版の誤り

LRESULT SendMessageW(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
BOOL    PostMessageW(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

hWnd は受信ウィンドウ、Msg はメッセージ番号、wParam・lParam はメッセージごとに意味の変わる付加情報で、戻り値の意味もメッセージ次第です。ここまでは元の記事のとおりです。

直す点は2つあります。1つ目、wParam と lParam には既定値がありません。Win32 の SendMessage は C の関数なので4つの引数を必ず書きます。既定値0があるのは MFC の CWnd::SendMessage(UINT message, WPARAM wParam = 0, LPARAM lParam = 0) で、元の記事に「MFC」のタグが付いていたのも同じ混同です。

2つ目は動き方です。Microsoft Learn の SendMessage のページは次のように分けて説明しています。

  • 呼び出したスレッド自身が作ったウィンドウなら、ウィンドウプロシージャが普通の関数のようにその場で呼ばれます。キューは通りません。
  • 別のスレッド(別プロセスを含む)のウィンドウなら、そのスレッドが次にメッセージを取り出すとき(GetMessage/PeekMessage など)にウィンドウプロシージャが実行され、送った側はそれまでブロックされます。ただし待っている間も、ほかのスレッドから自分宛てに送られたメッセージ(nonqueued message)は処理します。

ボタンメッセージの表は winuser.h で値を確かめ、すべて正しいことを確認しました。Windows Vista で BM_SETDONTCLICK が1つ増えています。

メッセージ 値 内容
BM_GETCHECK / BM_SETCHECK 0x00F0 / 0x00F1 チェック状態の取得・設定
BM_GETSTATE / BM_SETSTATE 0x00F2 / 0x00F3 押下・フォーカス状態の取得/強調表示の設定
BM_SETSTYLE 0x00F4 ボタンのスタイル変更
BM_CLICK 0x00F5 ユーザーがクリックしたように動かす
BM_GETIMAGE / BM_SETIMAGE 0x00F6 / 0x00F7 ビットマップ・アイコンのハンドル
BM_SETDONTCLICK 0x00F8 ラジオボタンがフォーカスを得たときに BN_CLICKED を送るか(Vista 以降)

2021年の BM_CLICK の例と「固まる」本当の理由

元の例は「폼(フォーム)」というウィンドウの中から「버튼 클릭(ボタンクリック)」というボタンを探して BM_CLICK を送るものでした。MinGW では警告なしでコンパイルできますが、インポートを見ると SendMessageA、FindWindowA、FindWindowExA が呼ばれています。UNICODE のない ANSI ビルドなので韓国語のタイトルが UTF-8 のバイト列のまま渡り、システムの ANSI コードページで解釈されて見つかりません。日本語のタイトルでも Shift_JIS(CP932)との食い違いで同じことが起きます。詳しくは FindWindow の記事 でバイト単位で確かめています。

元の記事の観察、「メッセージボックスを閉じるまで SendMessage が戻らない」はドキュメントどおりの動きです。BM_CLICK を受けたボタンは親に WM_COMMAND(BN_CLICKED)を送り、親の処理が MessageBox を出すと、その処理はボックスが閉じるまで戻りません。送った側はその戻りを待っています。これ自体はデッドロックではなく、ただ長く待っているだけです。

デッドロックはお互いを待つときに起きます。典型例は、ワーカースレッドが UI スレッドのウィンドウに SendMessage し、同じ瞬間に UI スレッドはそのワーカーの終了を WaitForSingleObject(C# なら Task.Wait() や Thread.Join())で待っている場合です。UI スレッドはメッセージを取り出さないので SendMessage は戻らず、ワーカーは終わらないので UI スレッドも起きません。WinForms の Control.Invoke も UI スレッドが処理するまで待つ点で同じ構造なので、同じように固まります。

直した例は W 版の関数を使い、待ち時間に上限を付けます。

// click.cpp — 2021年の BM_CLICK の例を直したもの
#include <windows.h>
#include <cstdio>

int main()
{
    HWND form = FindWindowW(nullptr, L"폼");
    HWND button = form ? FindWindowExW(form, nullptr, nullptr, L"버튼 클릭") : nullptr;
    if (!button) { std::printf("button not found\n"); return 1; }

    // BM_CLICK は相手のクリック処理を同期で実行する。そこで MessageBox を出されると
    // 普通の SendMessage は誰かがそれを閉じるまで待ち続ける。
    DWORD_PTR r = 0;
    if (!SendMessageTimeoutW(button, BM_CLICK, 0, 0, SMTO_ABORTIFHUNG, 3000, &r))
        std::printf("no reply within 3 s (error %lu)\n", GetLastError());
    return 0;
}

-Wall -Wextra で警告0件、インポートは FindWindowW、FindWindowExW、SendMessageTimeoutW でした。タイトルは元のテスト用プログラムの韓国語のままです。ボタンはクラス名ではなくキャプションで探しています。2021年の対象は Visual Basic 6 のプログラムで、VB6 のボタンは標準の Button クラスではないからです。また Microsoft Learn の BM_CLICK のページには「ボタンがダイアログボックスの中にあり、そのダイアログがアクティブでないと BM_CLICK は失敗することがある」という注意があります。

2段の図。上段:呼び出し側が SendMessageW を呼ぶと止まり、相手スレッドが次にメッセージを取り出すときにウィンドウプロシージャを実行し、LRESULT が返ってから呼び出し側が再開する。下段:PostMessageW はメッセージを相手スレッドのキューに入れ、呼び出し側はすぐに先へ進む。相手はあとで自分のループで処理する。
別スレッドのウィンドウへの SendMessage は、相手のウィンドウプロシージャが戻るまで待つ。PostMessage はキューに入れるだけですぐ戻る。

SendMessageTimeout と PostMessage の使い分け

SendMessageTimeout は SendMessage に制限時間(ミリ秒)とフラグを足したものです。

  • SMTO_ABORTIFHUNG:相手が応答なしに見えるなら、制限時間を待たずにすぐ戻る。
  • SMTO_ERRORONEXIT:待っている間に相手のスレッドが終了したら0を返す(Vista 以降)。
  • 失敗や時間切れでは0を返し、GetLastError が1460(ERROR_TIMEOUT)なら時間切れ。
  • 処理結果(LRESULT)は戻り値ではなく最後の引数(PDWORD_PTR lpdwResult)で受け取る。

PostMessage はキューに入れてすぐ TRUE を返します。結果は受け取れず、相手がいつ処理するかもわかりません。だからポインターを渡すメッセージには使えません。自分のスタック上の文字列のアドレスは相手が読むころには消えているかもしれず、別プロセスならそもそもそのアドレスに意味がありません。Microsoft Learn によると、WM_USER 未満のシステムメッセージでパラメーターにポインターを含むもの(WM_SETTEXT など)を PostMessage すると失敗します。

状況 使う関数
同じスレッドの自分のウィンドウ、結果が必要 SendMessage
別プロセス、結果が必要 SendMessageTimeout + SMTO_ABORTIFHUNG
通知だけ(閉じて、再描画して) PostMessage
別プロセスへ文字列・バイト列を渡す WM_COPYDATA を SendMessageTimeout で
ワーカーから UI スレッドへの進捗通知 PostMessage(UI スレッドを待たない)

WM_COPYDATA で別プロセスに文字列を送る

プロセスごとにアドレス空間が違うので、lParam にポインターを入れて送っても相手は読めません。WM_COPYDATA は、システムがデータを相手のプロセスへコピーしてくれるメッセージです。まず受信側です。

// receiver.cpp — WM_COPYDATA を受け取る小さなトップレベルウィンドウ
#include <windows.h>
#include <cstdio>

constexpr ULONG_PTR kCmdText = 1;   // WM_COPYDATA の中で使う独自のコマンド番号

static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg) {
    case WM_COPYDATA: {
        auto* cds = reinterpret_cast<const COPYDATASTRUCT*>(lp);
        if (cds->dwData != kCmdText) return FALSE;
        // データはこの呼び出しの間だけ有効。必要なら戻る前にコピーする
        wprintf(L"got %lu bytes: %.*ls\n", cds->cbData,
                static_cast<int>(cds->cbData / sizeof(wchar_t)),
                static_cast<const wchar_t*>(cds->lpData));
        return TRUE;                 // 送信側が受け取る結果
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

int wmain()
{
    WNDCLASSEXW wc{};
    wc.cbSize        = sizeof(wc);
    wc.lpfnWndProc   = WndProc;
    wc.hInstance     = GetModuleHandleW(nullptr);
    wc.lpszClassName = L"TLabReceiver";
    RegisterClassExW(&wc);
    HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"Receiver", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, nullptr, nullptr, wc.hInstance, nullptr);

    // このプロセスが管理者権限だと、UIPI が通常プロセスからの WM_COPYDATA を捨てる。
    // このウィンドウについて、このメッセージだけを許可する。
    ChangeWindowMessageFilterEx(hwnd, WM_COPYDATA, MSGFLT_ALLOW, nullptr);

    ShowWindow(hwnd, SW_SHOWNORMAL);
    MSG m;
    while (GetMessageW(&m, nullptr, 0, 0) > 0) { TranslateMessage(&m); DispatchMessageW(&m); }
    return 0;
}

次に送信側です。

// sender.cpp — SendMessageTimeoutW + WM_COPYDATA、そのあと PostMessageW
#include <windows.h>
#include <cstdio>
#include <cwchar>

int wmain()
{
    HWND target = FindWindowW(L"TLabReceiver", nullptr);
    if (!target) { wprintf(L"receiver not running\n"); return 1; }

    const wchar_t text[] = L"hello from sender";
    COPYDATASTRUCT cds{};
    cds.dwData = 1;
    cds.cbData = static_cast<DWORD>(wcslen(text) * sizeof(wchar_t));
    cds.lpData = const_cast<wchar_t*>(text);

    DWORD_PTR result = 0;
    LRESULT ok = SendMessageTimeoutW(target, WM_COPYDATA,
        0 /* 送信元ウィンドウ:コンソールアプリにはない */, reinterpret_cast<LPARAM>(&cds),
        SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 2000 /* ms */, &result);
    if (!ok) {
        DWORD e = GetLastError();      // 1460 = ERROR_TIMEOUT、5 = UIPI による遮断
        wprintf(L"SendMessageTimeout failed, GetLastError=%lu\n", e);
        return 1;
    }
    wprintf(L"receiver returned %llu\n", static_cast<unsigned long long>(result));

    // PostMessage はすぐ戻る。lParam が自分のメモリを指すメッセージ(WM_COPYDATA、
    // WM_SETTEXT など)は決して Post しない:処理されるころには消えているかもしれない。
    PostMessageW(target, WM_CLOSE, 0, 0);
    return 0;
}
$ x86_64-w64-mingw32-g++ -std=c++17 -Wall -Wextra -O2 -municode receiver.cpp -o receiver.exe -luser32
$ x86_64-w64-mingw32-g++ -std=c++17 -Wall -Wextra -O2 -municode sender.cpp -o sender.exe -luser32
$ file receiver.exe sender.exe
receiver.exe: PE32+ executable (console) x86-64, for MS Windows
sender.exe: PE32+ executable (console) x86-64, for MS Windows

最初は WNDCLASSEXW wc{ sizeof(wc) }; と先頭メンバーだけ初期化していたところ、-Wextra が残りのメンバーごとに warning: missing initializer for member 'tagWNDCLASSEXW::style' [-Wmissing-field-initializers] を11行出しました。残りは0で埋まるので動作に問題はありませんが、wc{} のあとで cbSize を別に代入する形に変えて警告を消しています。

WM_COPYDATA のルールは Microsoft Learn にまとまっています。必ず Send 系で送ること(Post は不可)、受信側はデータを読み取り専用として扱い処理中だけ有効とみなすこと、処理したら TRUE を返すこと。実務からもう1つ足すなら、受信側は送信側を信用しないことです。同じデスクトップのどのプロセスでも WM_COPYDATA を送れるので、dwData と cbData を確かめ、受け取った文字列をそのままファイルパスやコマンドとして実行しないのが基本です。

UIPI:管理者権限のウィンドウへのメッセージは捨てられる

図。中整合性レベルの通常アプリから、高整合性レベル(管理者として実行)のアプリへメッセージを送る。UIPI のチェックで、受信側ウィンドウが ChangeWindowMessageFilterEx でそのメッセージを許可しているかを見る。許可していれば届く。していなければ捨てられ、SendMessage は0を返し、GetLastError は5(アクセス拒否)になる。高から中への送信は制限されない。
UIPI(ユーザーインターフェイス特権の分離)。通常のプロセスは管理者権限のウィンドウを見つけることはできるが、相手が許可したメッセージ以外はほとんど送れない。

受信側のプログラムを「管理者として実行」すると整合性レベルが「高」になり、通常(中)のプロセスから来たメッセージはほとんど UIPI に捨てられます。このとき SendMessage は0を返し、GetLastError は5(ERROR_ACCESS_DENIED)です。逆向き、高から中へのメッセージは制限されません。

受信側を自分で作っているなら、上の receiver のように ChangeWindowMessageFilterEx(hwnd, WM_COPYDATA, MSGFLT_ALLOW, nullptr) でそのウィンドウにそのメッセージだけを許可できます(Windows 7 以降)。プロセス全体にかかる古い ChangeWindowMessageFilter より範囲が狭く安全です。ただし許可した瞬間から、権限の低いプロセスが管理者プロセスへデータを入れられるようになるので、前述の入力チェックがいっそう重要になります。他人のプログラムを操作する場合の対処は1つだけ、自分のツールも同じ権限で実行することです。

C# の LibraryImport で WM_COPYDATA を送る

.NET 10 の LibraryImport で同じ送信側を書くと次のとおりです(net10.0-windows、AllowUnsafeBlocks を有効化)。

using System.Runtime.InteropServices;

const uint WM_COPYDATA = 0x004A, WM_CLOSE = 0x0010;
const uint SMTO_ABORTIFHUNG = 0x0002, SMTO_ERRORONEXIT = 0x0020;

nint target = Native.FindWindowW("TLabReceiver", null);
if (target == 0) { Console.WriteLine("receiver not running"); return 1; }

string text = "hello from C#";
unsafe
{
    fixed (char* p = text)
    {
        var cds = new COPYDATASTRUCT
        {
            dwData = 1,
            cbData = (uint)(text.Length * sizeof(char)),
            lpData = (nint)p,
        };
        nint ok = Native.SendMessageTimeoutW(target, WM_COPYDATA, 0, (nint)(&cds),
            SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 2000, out nuint result);
        if (ok == 0)
        {
            // 1460 = ERROR_TIMEOUT、5 = ERROR_ACCESS_DENIED(UIPI)
            Console.WriteLine($"failed, error {Marshal.GetLastPInvokeError()}");
            return 1;
        }
        Console.WriteLine($"receiver returned {result}");
    }
}
Native.PostMessageW(target, WM_CLOSE, 0, 0);   // すぐ戻る
return 0;

[StructLayout(LayoutKind.Sequential)]
struct COPYDATASTRUCT
{
    public nuint dwData;   // ULONG_PTR
    public uint cbData;    // DWORD
    public nint lpData;    // PVOID
}

static partial class Native
{
    [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
    public static partial nint FindWindowW(string? lpClassName, string? lpWindowName);

    [LibraryImport("user32.dll", SetLastError = true)]
    public static partial nint SendMessageTimeoutW(nint hwnd, uint msg, nint wParam,
        nint lParam, uint flags, uint timeoutMs, out nuint result);

    [LibraryImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static partial bool PostMessageW(nint hwnd, uint msg, nint wParam, nint lParam);
}

dotnet build -c Release -r win-x64 -p:EnableWindowsTargeting=true で警告なしにビルドできました(実行はしていません)。確かめた点は次のとおりです。

  • PostMessageW の [return: MarshalAs(UnmanagedType.Bool)] を外すと error SYSLIB1051: Marshalling bool without explicit marshalling information is not supported になります。Win32 の BOOL は4バイトの整数なので、対応のさせ方を明示する必要があります。
  • SetLastError = true の関数のエラーコードは Marshal.GetLastPInvokeError() で読みます。生成コードはネイティブ呼び出しの直後に Marshal.GetLastSystemError() で値を取り、SetLastPInvokeError で保存しているので、途中で別の API に上書きされません。
  • COPYDATASTRUCT の dwData は ULONG_PTR で、64 ビットでは8バイトです。uint で宣言すると後ろのフィールドの位置がずれ、相手が間違ったサイズとアドレスを読むので nuint にしています。
  • 文字列のアドレスは fixed で固定している間だけ有効です。SendMessageTimeoutW は同期呼び出しで fixed ブロック内で終わるので安全で、同じ理由で PostMessage にはこの書き方は使えません。

WinForms や WPF のアプリなら、受信側は WndProc のオーバーライド(WinForms)か HwndSource.AddHook(WPF)で WM_COPYDATA を受け取れます。ウィンドウを作る基本の流れは CreateWindow・ShowWindow、ハンドルの探し方は FindWindow にあります。

検証環境:macOS(Apple Silicon)上の MinGW-w64 x86_64-w64-mingw32-g++ (GCC) 16.2.0、.NET SDK 10.0.403(EnableWindowsTargeting=true)。Windows 向けにコンパイルのみ行い、実行はしていません。

よくある質問

SendMessage と PostMessage は何が違う?

SendMessage は受信側のウィンドウプロシージャがメッセージを処理して戻るまで待ち、その結果(LRESULT)を返します。PostMessage は受信スレッドのキューに入れるだけですぐ戻ります。結果が要る、またはポインターを渡すなら SendMessage(できれば SendMessageTimeout)、知らせるだけなら PostMessage です。

ほかのアプリに SendMessage したら自分のアプリまで固まりました。

SendMessage は相手が処理し終えるまで待つので、相手が応答なしになっていたり、メッセージボックスを出したままだったりすると、自分のスレッドも一緒に止まります。他プロセスには SendMessageTimeout に SMTO_ABORTIFHUNG と制限時間を付けて送ります。時間切れなら0を返し、GetLastError は1460(ERROR_TIMEOUT)です。

SendMessage が0を返し、GetLastError が5です。

UIPI(ユーザーインターフェイス特権の分離)に止められています。管理者として実行された(高整合性の)ウィンドウへ、通常のプロセスから送ったメッセージはほとんど破棄されます。自分のプログラムも同じ権限で実行するか、受信側を自分で作っているなら ChangeWindowMessageFilterEx で必要なメッセージだけを許可します。

  • #Win32
  • #SendMessage
  • #PostMessage
  • #WM_COPYDATA
  • #UIPI
  • #LibraryImport

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑