C# から C++ へ文字列送信する最適解|WM_COPYDATA とタイマーで実現するフルデュプレックス通信とスレッド安全性

既存の C++ アプリに、C#(WinForms)からテキストボックスを介さずに定期的に文字列を送りたい──そんな場面は業務システムでよくあります。しかし実装を誤ると、「クロススレッド例外」やハンドル不正、タイマーの再入など、地味にハマる落とし穴がたくさんあります。この記事では、WM_COPYDATA を使った C# → C++ の文字列送信を起点に、フルデュプレックス通信・タイマーの選び方・スレッド安全性まで一気に整理します。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

C# から C++ へ文字列を送る方針整理

まず前提として、「テキストボックスに表示してから送る」必要は一切ありません。ユーザーに見せるかどうかと、プロセス間でどうデータを受け渡すかは完全に別問題です。

Windows 上で C# アプリと C++ アプリ間で軽量に文字列を渡したい場合、代表的な方法は次のようなものです。

方式特徴向いているケース
WM_COPYDATAWindows メッセージベース・同期呼び出し・数 KB 程度までログ、状態文字列、コマンドなど小さなメッセージ
Named Pipe双方向・ストリーム・非同期 I/O も可高頻度・中~大容量の通信
メモリマップトファイル共有メモリで大容量・ポーリングやイベント連携が必要大量データを頻繁に共有するとき
ソケットプロセス/マシンを問わず汎用的将来ネットワーク越し通信に拡張する場合

今回の要件は「60 秒ごとにファイルの中身を C++ へ送る」「メッセージサイズはせいぜい数 KB 程度」「ローカル同一マシン」。この条件であれば、もっともシンプルで実装コストが低いのは WM_COPYDATA です。

WM_COPYDATA の概要と注意点

WM_COPYDATA は、送信側プロセスが SendMessage を使って、受信側ウィンドウに任意のデータを渡せる Win32 メッセージです。C# からは P/Invoke で呼び出し、C++ 側では通常のウィンドウプロシージャ(WndProc)で受信します。

項目内容
方向任意(C# → C++、C++ → C# のどちらも可)
呼び出し方式同期SendMessage 呼び出し中は送信側スレッドがブロック)
データの寿命メッセージ処理中のみ有効。受信側は必ずコピーする必要あり
推奨サイズ数 KB ~ 数十 KB 程度まで(あまり巨大にはしない)
用途コマンド、ログ、設定値などの短いテキストや軽量構造体

データ構造としては COPYDATASTRUCT を使用します。C# から送る場合は、文字列をバイト配列に変換してアンマネージメモリにコピーし、それを lpData に入れて送ります。

HWND は自作しない ─ 正しいウィンドウハンドルの渡し方

質問の中で特に危険なのが、new IntPtr(0xFAA1) のような「適当な数値をハンドルとして使う」やり方です。HWND は OS が発行するハンドルであり、アプリ側で勝手に「それっぽい数値」を作ってはいけません。

やってはいけない例理由
var h = new IntPtr(0xFAA1); など任意値を使う実在しないウィンドウハンドルの可能性が高く、
最悪 OS が別のオブジェクトとして誤解する
一度取得した HWND を、プロセス再起動後も使い回すウィンドウを作り直すと HWND は変わるため、古いハンドルは無効

C# 側が C++ のウィンドウにメッセージを送りたい場合は、次のような方法で 実在する HWND を取得します。

  • ウィンドウタイトル、クラス名から FindWindow で検索する
  • C++ 側が起動時に HWND をファイルや共有メモリに書き出しておき、C# 側が読む
  • 逆に C++ 側が C# ウィンドウに向けて WM_COPYDATA を送り、C# が返信先 HWND として受け取る

さらに 返信をしやすくするために、SendMessagewParam に送信元ウィンドウ(C# 側)の Handle を入れておくと、C++ 側はそれをそのまま返信先 HWND として利用できます。これにより簡易フルデュプレックス通信が実現できます。

C# 送信側(WinForms)実装のポイント

以下は、WinForms アプリケーションから WM_COPYDATA で C++ アプリへ文字列を送信する最小構成です。60 秒ごとに XML ファイルを読み取り、その内容を送る例をベースにしています。

COPYDATASTRUCT と P/Invoke の定義


using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private const int WM_COPYDATA = 0x004A;

    [StructLayout(LayoutKind.Sequential)]
    public struct COPYDATASTRUCT
    {
        public IntPtr dwData;   // 任意のID
        public int cbData;      // バイト数
        public IntPtr lpData;   // データ先頭
    }

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr SendMessage(
        IntPtr hWnd, int msg, IntPtr wParam, ref COPYDATASTRUCT cds);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    private readonly Timer _timer = new Timer();
    private IntPtr _receiverHwnd = IntPtr.Zero;
    private readonly string _filePath = @"C:\path\isrunning.xml";

    public Form1()
    {
        InitializeComponent();

        // C++ 側のウィンドウタイトルを既知として検索する例
        _receiverHwnd = FindWindow(null, "CppReceiverWindowTitle");

        _timer.Interval = 60_000; // 60 秒
        _timer.Tick += TimerOnTick;
    }

    protected override void OnShown(EventArgs e)
    {
        base.OnShown(e);
        _timer.Start();
    }

Timer.Tick 内での非同期ファイル読み込みと送信

60 秒ごとの定期送信を実現するには、WinForms の System.Windows.Forms.Timer を使うのが最も自然です。Tick イベントは UI スレッドで処理されるため、ハンドラが重複して実行される心配はほぼありません(処理が終わるまで次の Tick は処理されない)。

ただしファイル I/O を同期的に実行すると UI が固まる可能性があるため、async/await で非同期 I/Oにしておくと安全です。


private async void TimerOnTick(object sender, EventArgs e)
{
    // 念のため再入防止として一時停止
    _timer.Enabled = false;

    try
    {
        if (_receiverHwnd == IntPtr.Zero)
        {
            // 起動タイミングずれ対策で再取得することも可能
            _receiverHwnd = FindWindow(null, "CppReceiverWindowTitle");
            if (_receiverHwnd == IntPtr.Zero)
            {
                // ログ出力など
                return;
            }
        }

        string text = await File.ReadAllTextAsync(_filePath, Encoding.UTF8);
        SendCopyData(_receiverHwnd, text);
    }
    catch (Exception ex)
    {
        // ログに書く、エラー表示するなど
        Console.WriteLine(ex);
    }
    finally
    {
        _timer.Enabled = true;
    }
}

WM_COPYDATA 送信処理

実際に文字列を送る処理では、UTF-16LE(.NET の内部表現)+ NUL 終端でバイト列を作り、アンマネージメモリにコピーしてから SendMessage を呼び出します。


private void SendCopyData(IntPtr receiver, string text)
{
    if (receiver == IntPtr.Zero)
        throw new InvalidOperationException("受信側ウィンドウが見つかりません。");

    // UTF-16LE(Encoding.Unicode)+ NUL 終端
    byte[] bytes = Encoding.Unicode.GetBytes(text + "\0");

    var cds = new COPYDATASTRUCT
    {
        dwData = IntPtr.Zero,      // メッセージ種別に使ってもよい
        cbData = bytes.Length,
        lpData = Marshal.AllocHGlobal(bytes.Length)
    };

    try
    {
        Marshal.Copy(bytes, 0, cds.lpData, bytes.Length);
        // wParam に送信元 HWND を渡すことで、C++ 側が返信時に使える
        SendMessage(receiver, WM_COPYDATA, this.Handle, ref cds);
    }
    finally
    {
        Marshal.FreeHGlobal(cds.lpData);
    }
}

このコードのポイントは次のとおりです。

  • Encoding.Unicode を使う(UTF-16LE)。C++ 側では wchar_t* として扱いやすい。
  • NUL 終端を付けておくことで、C++ 側で単純な std::wstring コンストラクタが使える。
  • SendMessage は同期的に戻ってくるので、処理は短くし、重い処理は受信側で別スレッドに逃す

C++ 受信側の実装(WndProc)

C++ 側では、通常のウィンドウプロシージャ(WndProc)で WM_COPYDATA メッセージを処理します。ここで注意すべき点は、受け取ったポインタ(lpData)の寿命は SendMessage 呼び出し中のみ有効ということです。関数内ですぐに std::wstring などにコピーしてしまいましょう。


#include <windows.h>
#include <string>

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_COPYDATA:
    {
        auto pCopyData = reinterpret_cast<PCOPYDATASTRUCT>(lParam);
        const wchar_t* data = static_cast<const wchar_t*>(pCopyData->lpData);

        // SendMessage 呼び出し中のみ有効なので、すぐにコピーする
        std::wstring payload = data;

        // ここで payload を解析したり処理キューに投げたりする
        // 例: ログ出力やコマンド解析など

        // 必要に応じて wParam (送信元 HWND) に返信を送ることも可能
        // SendReply(static_cast<HWND>(wParam), L"OK");

        return TRUE; // 受信成功
    }
    }

    return DefWindowProc(hWnd, msg, wParam, lParam);
}

もし C++ 側で重い処理を行う場合は、WM_COPYDATA ハンドラ内では キューに積むだけにしておき、別スレッドのワーカーが処理する構成にすると、送信元 C# 側が長時間ブロックされるのを防げます。

フルデュプレックス通信にするには

C# → C++ の片方向だけでなく、C++ から C# にも結果を返したい場合、WM_COPYDATA だけで簡易的なフルデュプレックス通信が構成できます。

  1. C# 側が SendMessage で WM_COPYDATA を送り、wParam に自分自身の Handle を入れる。
  2. C++ 側は wParamHWND として受け取り、必要であればその HWND に向けて WM_COPYDATA や独自ウィンドウメッセージを送る。

例えば C++ から C# に単純な応答メッセージを返す関数は次のように書けます。


void SendReply(HWND hReplyTo, const std::wstring& message)
{
    if (!IsWindow(hReplyTo)) return;

    COPYDATASTRUCT cds{};
    cds.dwData = 1; // 応答メッセージ種別など
    cds.cbData = static_cast<DWORD>((message.size() + 1) * sizeof(wchar_t));
    cds.lpData = (PVOID)message.c_str(); // ここではスタック上のバッファでも可(同期呼び出し)

    SendMessage(hReplyTo, WM_COPYDATA,
                /* wParam = このアプリ自身の HWND */ nullptr,
                (LPARAM)&cds);
}

ここでも、ロックの取り方に注意する必要があります。C# 側から C++ 側へ SendMessage し、その中で C++ 側が C# 側へさらに SendMessage すると相互待ち(デッドロック)の危険があるため、

  • 返信は PostMessage(非同期)にする
  • もしくは C# 側で返信用に別ウィンドウ(メッセージ専用スレッド)を持つ

など、同期呼び出しが互いにネストしない設計が望まれます。

タイマー選び:60 秒周期送信とスレッド安全性

「60 秒ごとにファイルを読んで C++ に送りたい」という要件に対して、.NET には複数のタイマーがあります。どのタイマーを選ぶかでスレッドの扱い方が変わるため、特徴を整理しておきます。

タイマー発火スレッド特徴UI 更新再入の可能性
System.Windows.Forms.TimerUI スレッドWinForms 専用。UI メッセージループに統合されるそのまま触れる原則なし(1 回の Tick 処理中は次が来ない)
System.Timers.Timerスレッドプール汎用。AutoReset=true だとハンドラ重複に注意Invoke / BeginInvokeあり(ハンドラが長いと重なる)
System.Threading.Timerスレッドプール最低限の機能。コールバックを直接指定Invokeあり(自前で再入制御が必須)

WinForms アプリであれば、まずは System.Windows.Forms.Timer を優先すれば OK です。その上で、

  • Tick の最初で _timer.Enabled = false; として再入防止
  • ファイル読み込みは await File.ReadAllTextAsync で非同期
  • 送信処理は短時間で終わるようにする(ヘビーな処理は C++ 側に任せる)

という基本を守れば、UI の固まりやスレッド競合をかなり避けられます。

テキストボックスを別スレッドから更新するとクロススレッド例外になる理由

.NET の WinForms では、すべての UI コントロールは「作成されたスレッド(通常は UI スレッド)」からのみアクセスするというルールがあります。これに違反して、バックグラウンドスレッドから直接テキストボックスを更新すると、「クロススレッド操作は無効です」という例外が発生します。

解決策はシンプルで、Control.Invoke または Control.BeginInvoke を使って UI スレッドに処理をマーシャリングします。


private void SetTextSafe(TextBox textBox, string text)
{
    if (textBox.InvokeRequired)
    {
        textBox.Invoke(new Action(() => textBox.Text = text));
    }
    else
    {
        textBox.Text = text;
    }
}

もしバックグラウンドで WM_COPYDATA を受信してログを表示したい、といったケースも同様です。受信スレッドは単にデータを受け取り、UI を触る部分だけ上記のように Invoke 経由で実行します。

今回のテーマは「テキストボックスを使わない送信」なので UI 更新自体は不要ですが、「ログをちょっと見せたい」といった時にクロススレッド例外で悩まないよう、このパターンは覚えておくと便利です。

スレッド安全性と再入制御の落とし穴

WM_COPYDATA 自体はシンプルですが、タイマー・バックグラウンド処理・ログ表示などが絡むと、スレッド安全性の問題が表面化してきます。よくある問題と対策を表にまとめておきます。

問題典型的な原因対策
クロススレッド例外スレッドプールから直接 TextBox などを操作UI は必ず Invoke / BeginInvoke 経由で更新
タイマー処理の二重実行System.Timers.Timer で長い処理を行い AutoReset=true のままAutoReset=false、処理開始時に停止し完了後に再スタート、もしくはロックでガード
デッドロック相互に SendMessage を送り合う片方は PostMessage にする、返信専用スレッドを用意する
ハンドル無効化プロセス再起動後も古い HWND を使い回し必要に応じて FindWindow などで毎回取り直すか、起動時に再通知

スレッド安全性を高める上で、Timer ハンドラなど再入の可能性があるところでは、簡単なフラグを用意してガードする手もあります。


private bool _sending = false;

private async void TimerOnTick(object sender, EventArgs e)
{
    if (_sending) return; // 再入防止
    _sending = true;

    try
    {
        string text = await File.ReadAllTextAsync(_filePath, Encoding.UTF8);
        SendCopyData(_receiverHwnd, text);
    }
    finally
    {
        _sending = false;
    }
}

より厳密に行うなら Interlocked.CompareExchange を使ってフラグを立てると、マルチコア環境でも安全にガードできます。

文字コードとメッセージサイズの注意点

WM_COPYDATA で文字列をやり取りする場合、送受信側の文字コードを一致させることが非常に重要です。もっとも扱いやすいのは UTF-16(Windows のワイド文字)です。

  • C# 側:Encoding.Unicode でエンコード(UTF-16LE)。
  • C++ 側:wchar_t* として受け取り、std::wstring にコピー。

また、WM_COPYDATA はメッセージのペイロードとして扱われるため、あまり巨大なバイト列を乗せるべきではありません。数 KB ~ 数十 KB 程度までを目安とし、それを超えるようであれば以下のような戦略が現実的です。

  • WM_COPYDATA ではファイル名やコマンドだけを送る
  • 実データは Named Pipe やメモリマップトファイル、ファイル I/O などで別途受け渡し

今回の「XML ファイルの内容を送る」ケースで、ファイルサイズが大きくなりそうな場合は、ファイルパスだけを WM_COPYDATA で送り、C++ 側が直接ファイルを読むという構成も検討できます。

ここまでを踏まえた全体構成イメージ

今回の要件を安全に満たすための全体像を、箇条書きで整理しておきます。

  • 通信方式: C# → C++ は WM_COPYDATA を使用。必要に応じて C++ → C# も WM_COPYDATA で返信。
  • ハンドル: FindWindow などで取得した実在 HWND を使い、wParam に C# 側 HWND を渡しておく。
  • タイマー: WinForms の System.Windows.Forms.Timer を使い、60 秒ごとに Tick。Tick 内では一時停止して再入防止。
  • ファイル I/O: File.ReadAllTextAsync で非同期読み込みし、UI スレッドを塞がない。
  • UI 更新: ログ表示など UI を触るのは Invoke 経由に限定する。
  • C++ 側: WndProc の WM_COPYDATA ハンドラで文字列をコピーし、その後の重い処理は別スレッドへ。

まとめ:テキストボックス不要、WM_COPYDATA でシンプルに解決

最後に、この記事で押さえておきたいポイントをまとめます。

  • テキストボックスに表示する必要はない。C# から C++ へは WM_COPYDATA で文字列を直接送ればよい。
  • HWND は自作しないFindWindow や起動時通知などで実在するウィンドウハンドルを取得して使う。
  • wParam に送信元 HWND を渡しておくと、C++ 側からの返信先としてそのまま利用でき、簡易フルデュプレックス通信になる。
  • 60 秒周期の処理は、WinForms なら System.Windows.Forms.Timer が扱いやすい。Tick 内で一時停止 → 処理 → 再開とし、非同期 I/O で UI ブロックを防ぐ。
  • バックグラウンドスレッドから UI を触るとクロススレッド例外になるため、UI 更新は必ず Invoke 経由にする。
  • WM_COPYDATA は小さいデータ向け。大容量・高頻度通信では Named Pipe やメモリマップトファイルの利用も検討する。

この設計を採用すれば、「テキストボックスなしで C# から C++ に文字列を送る」「クロススレッド例外を避ける」「60 秒ごとの定期送信をスレッド安全に行う」という三つの課題を、シンプルかつ堅牢にクリアできます。既存の C++ アプリとの連携に悩んでいる場合は、まずは WM_COPYDATA ベースの構成から試してみるのがおすすめです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次