VBAのSendMessageで外部プロセスに構造体ポインタを安全に引き渡すメモリ管理技術

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

VBAのSendMessageで外部プロセスに構造体ポインタを安全に引き渡すメモリ管理技術

【背景と目的】

VBAからWin32 APIを介して外部アプリケーション(特に標準的なGUIコントロール)を操作する際、構造体(UDT)を用いてデータをやり取りする必要がありますが、プロセス間のメモリ空間の違いにより、単にローカル変数のポインタを渡すとメモリ破壊やアクセス違反が発生します。

本稿では、VBA環境からWin32 APIのSendMessageを使用し、外部プロセスが安全にアクセスできる共有メモリ領域を確保・利用することで、構造体データを正確に受け渡し、実行速度と安定性を両立させるためのロバストなロジックを設計します。

【処理フロー図】

graph TD
    A["VBAで構造体(UDT)を準備"] --> B{"GlobalAllocで共有メモリ領域確保"};
    B --> C["CopyMemoryでUDTの内容をメモリへ転送"];
    C --> D["SendMessageでメモリポインタ(LongPtr)を送信"];
    D --> E{"外部プロセスでの処理・応答データの書き込み"};
    E --> F["CopyMemoryで共有メモリの内容をUDTへ読み戻し"];
    F --> G["GlobalFreeで共有メモリを解放"];
    G --> H["終了"];

    subgraph VBAプロセス
        A; B; C; F; G; H
    end
    subgraph Win32 API
        D
    end
    subgraph 外部プロセス
        E
    end

※【厳守】ブロック内には「graph TD」から始まるコードのみ記述。説明文は外に書くこと。

【実装:VBAコード】

外部プロセスとの安全な構造体受け渡しを実現するためのモジュールコードです。PtrSafe宣言とメモリ管理API(GlobalAlloc, CopyMemory, GlobalFree)を使用します。

Option Explicit
' ----------------------------------------------------------------------------------
' 1. Win32 API宣言 (64bit対応: PtrSafe必須)
' ----------------------------------------------------------------------------------
' SendMessage: 外部プロセスにメッセージを送信する
Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" ( _
    ByVal hwnd As LongPtr, _
    ByVal wMsg As Long, _
    ByVal wParam As LongPtr, _
    lParam As Any) As LongPtr

' GlobalAlloc: グローバルヒープからメモリを割り当てる(外部プロセスとの共有に適した方法の一つ)
Private Declare PtrSafe Function GlobalAlloc Lib "kernel32" ( _
    ByVal uFlags As Long, _
    ByVal dwBytes As LongPtr) As LongPtr

' GlobalFree: 割り当てたメモリを解放する
Private Declare PtrSafe Function GlobalFree Lib "kernel32" ( _
    ByVal hMem As LongPtr) As LongPtr

' CopyMemory: メモリ領域間でデータを高速にコピーする
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
    Destination As Any, _
    Source As Any, _
    ByVal Length As LongPtr)

' ----------------------------------------------------------------------------------
' 2. 構造体定義(例: 外部アプリケーションとやり取りするデータ構造)
' ----------------------------------------------------------------------------------
Private Type SAMPLE_DATA_STRUCT
    ID As Long          ' データ識別子
    Value As Long       ' 読み書きしたい値
    TextBuffer(255) As Byte ' テキストバッファ(固定長配列)
End Type

' ----------------------------------------------------------------------------------
' 3. 定数定義
' ----------------------------------------------------------------------------------
Const GHND As Long = &H42           ' Global Alloc フラグ (ゼロ初期化 & 移動可能)
Const WM_USER_GET_DATA As Long = &H400 + 100 ' 外部プロセスが定義するメッセージID(例)

' ----------------------------------------------------------------------------------
' 4. メイン処理関数
' ----------------------------------------------------------------------------------
Public Function ExchangeDataWithExternalApp(ByVal TargetHwnd As LongPtr, ByVal TargetID As Long) As Boolean

    Dim udtData As SAMPLE_DATA_STRUCT ' VBA側の構造体インスタンス
    Dim hGlobalMem As LongPtr       ' GlobalAllocで確保するメモリハンドル
    Dim lSize As LongPtr            ' 構造体のバイトサイズ
    Dim lResult As LongPtr          ' SendMessageの戻り値
    Dim fSuccess As Boolean

    ' エラーハンドラ設定
    On Error GoTo ErrorHandler

    ' (1) 構造体の初期化とサイズ取得
    lSize = Len(udtData)
    udtData.ID = TargetID ' 外部プロセスに要求するデータを指定

    ' (2) 共有可能なメモリ領域を確保
    hGlobalMem = GlobalAlloc(GHND, lSize)
    If hGlobalMem = 0 Then
        MsgBox "メモリ確保に失敗しました。", vbCritical
        Exit Function
    End If

    ' (3) VBA構造体の中身を、確保したメモリ領域にコピー
    ' Destination: 確保したメモリのポインタ (ByVal hGlobalMem)
    ' Source: VBA構造体のデータ (udtData)
    CopyMemory ByVal hGlobalMem, udtData, lSize

    ' (4) SendMessageでポインタを外部プロセスへ送信
    ' LPARAMとして、確保したメモリのポインタ (hGlobalMem) を渡す
    lResult = SendMessage(TargetHwnd, WM_USER_GET_DATA, 0, ByVal hGlobalMem)

    ' 外部プロセスがデータを処理し、hGlobalMemの領域に結果を書き込む

    ' (5) 共有メモリの内容をVBA構造体へ読み戻し
    ' 外部プロセスが書き込んだ結果を udtData に反映させる
    CopyMemory udtData, ByVal hGlobalMem, lSize

    ' (6) 処理結果の確認(外部プロセスが成功時に 1 を返すなど)
    If lResult <> 0 Then
        ' 成功した場合の処理。取得したデータは udtData に入っている
        Debug.Print "データ取得成功。ID: " & udtData.ID & ", Value: " & udtData.Value
        fSuccess = True
    Else
        Debug.Print "外部プロセスからの応答エラー、またはメッセージ処理失敗。"
        fSuccess = False
    End If

CleanUp:
    ' (7) メモリ解放は必須
    If hGlobalMem <> 0 Then
        GlobalFree hGlobalMem
    End If
    ExchangeDataWithExternalApp = fSuccess
    Exit Function

ErrorHandler:
    MsgBox "ランタイムエラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Function

' ----------------------------------------------------------------------------------
' 利用例(デバッグ用)
' ----------------------------------------------------------------------------------
Sub RunExternalDataExchange()
    ' 外部ウィンドウのハンドルを事前に取得しておく必要があります
    ' 例: FindWindowA/FindWindowExA などでウィンドウハンドル (hwnd) を取得
    Dim TargetHwnd As LongPtr

    ' 以下の行はテスト用の仮のハンドルです。実際には有効な外部プロセスのウィンドウハンドルを指定してください
    TargetHwnd = 123456 ' 例: ターゲットウィンドウのハンドル

    If TargetHwnd <> 0 Then
        If ExchangeDataWithExternalApp(TargetHwnd, 101) Then
            Debug.Print "外部操作完了。"
        Else
            Debug.Print "外部操作失敗。"
        End If
    End If
End Sub

【技術解説】

1. プロセス間通信とメモリポインタの課題

VBA(Excel/Access)プロセスと外部プロセスは、異なる仮想メモリ空間に存在します。VBAで宣言した構造体変数のアドレス(ポインタ)をそのままSendMessagelParamとして外部に渡しても、外部プロセスはそのアドレスを参照できません(アクセス違反が発生します)。

これを回避するため、GlobalAllocを使用してOSのグローバルヒープ領域にメモリを確保します。このグローバルヒープ領域は、多くのWin32コントロールやアプリケーションが共有可能なメモリとして設計されているため、ポインタを渡すことが可能になります。

2. PtrSafeとLongPtrの使用

64bit版のOffice環境でWin32 APIを安全に使用するためには、以下の対応が必須です。

  • Declare PtrSafe: API宣言に必ず含めます。

  • LongPtr: ポインタ(メモリのアドレス)やハンドル(hwnd, hMemなど)を格納するために使用します。32bit環境ではLongと同じ4バイト、64bit環境では8バイトを確保し、アドレスを確実に保持します。

  • lParam As AnyByVal hGlobalMem: SendMessageの引数lParamは通常ByVal LongPtrで定義されますが、構造体ポインタを扱う場合は、Anyで宣言しておき、呼び出し時にByValキーワードを使ってポインタの値を渡します。

3. CopyMemoryによる高速なデータ移動

VBA構造体の内容をグローバルメモリに転送し、また戻す作業は、RtlMoveMemory(VBAではCopyMemoryとしてエイリアス設定)を使って行います。これは、バイト単位での非常に高速なメモリ操作を可能にし、構造体全体の内容を効率的にコピーします。

【注意点と運用】

1. メモリリークの回避(最も重要)

外部プロセス操作が成功・失敗に関わらず、GlobalAllocで確保したメモリ(hGlobalMem)は必ずGlobalFreeで解放しなければなりません。解放を怠ると、メモリリークが発生し、システム全体のパフォーマンスが低下します。

提示したコードでは、On Error GoTo ErrorHandlerCleanUpラベルを組み合わせることで、エラー発生時や正常終了時であっても、必ずGlobalFreeが実行されるロジック(ガベージコレクションのような役割)を組み込んでいます。

2. 構造体のパディングとアライメント

外部プロセスAPIが要求する構造体の定義は、VBAでの定義と完全に一致している必要があります。C/C++環境ではメモリ効率のために「パディング」が行われますが、VBAのType定義は通常デフォルトのアライメントに従います。もし外部APIが厳密なパッキング(アライメント指定)を要求する場合は、VBA側で Private Type 定義の前に #If VBA7 Then #Else #End If などを用いて適切なバイト長調整を行う必要があります。

3. SendMessageのタイムアウト

SendMessageは同期的に動作するため、外部プロセスが応答を停止(ハングアップ)すると、VBAも停止してしまいます。実務運用では、ハングアップリスクを避けるため、SendMessageの代わりにSendMessageTimeoutを使用し、タイムアウト時間を設定することを強く推奨します。

【まとめ】

VBAからWin32 APIを用いて外部プロセスと構造体を安全にやり取りするための運用のコツは、以下の3点に集約されます。

  1. PtrSafeとLongPtrの徹底: 64bit環境での安定稼働のために、ポインタやハンドルは必ずLongPtr型で扱い、API宣言にPtrSafeを付与すること。

  2. GlobalAlloc/GlobalFreeによるメモリ分離: プロセス間のメモリ空間の違いを吸収するため、GlobalAllocで共有可能なメモリを確保し、CopyMemoryでデータを橋渡しすること。そして、いかなる場合も必ずGlobalFreeで解放すること。

  3. エラーハンドリングによる堅牢性: メモリ解放処理をCleanUpルーチンに集約し、エラー発生時も含めて必ず実行される構造にすることで、システム全体の安定性を確保すること。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました