他プロセスとのメモリのやり取り

別のプログラムを"ライブラリ"として参照できると、プログラミングは楽です。しかし何らかの理由でそうできず、別の実行ファイルとして呼び出す場合があります。

そうするとすぐ直面するのが情報のやりとりをどうするかです。今回は、VirtualAllocExで作ったメモリ領域を通して実現する方法の備忘録です。

やりとりの方法

ぱっと思いつくのはこんなところですが、一長一短ですね。

  • 標準入出力
    • - データが少ないなら最適です。大きかったり複雑なデータだと一気に面倒に。
  • 一時ファイルで受け渡し
    • - 特にバイナリデータなら王道でしょうか。ですが、長時間稼働だと消しそこないファイルができたり、ディスクI/Oのオーバーヘッドが足を引っ張ったり。
  • プロセス間通信


今回はこれ以外で攻めてみます。受け渡すデータは、適当なデカいサイズのバイナリデータということで、画像とします。

前提知識

すごくざっくりですが。

あるプロセスの中でのポインタは、仮想アドレス空間での位置を示しています。仮想アドレス空間はプロセスごとに用意されます。ですから、同じ番地に見えても、別のプロセスでは別なものが入っています。

あるプロセスではメモリの20番地に1が入っていても、別のプロセスのメモリ20番地では2が入っている、という状態です。よって、単純に自分のところの番地を別プロセスに教えても意味がありません。

別なプロセスと同じメモリを操作するため、今回はその別のプロセスにお邪魔して、メモリを確保させてもらい、そこを読み書きします。お邪魔する側が読み書きするための専用関数が用意されています。お邪魔される側は、自分自身の仮想アドレス空間なので全く普通に触れます。

これを実現するため、以下のWin32API関数を使います。

この辺の使い方は偉大な先人が書き尽くしていると思いますので、省略。

コード例

画像データ(Bitmap)を受け渡すサンプルです。

Slave側プログラム

命令を受けて、お仕事をするプログラムです。

命令は、文字列1行で標準入力で受けます。内容は「画像サイズ」「画素データの先頭アドレス」だけです。この画素データの実体の受け渡し方がこの記事の肝です。

ここでは簡単にC#で書きましたが、私の今回のニーズからいうとここがC++でした。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace Slave
{
    class Program
    {
        static void Main(string[] args)
        {
            // 入力待ち
            string line = Console.ReadLine();
            string[] tokens = line.Split(',');

            // 与えられた情報をパース
            // 形式(CSV): 幅,高さ,1行のバイト幅,画素ポインタ
            int width = Int32.Parse(tokens[0]);
            int height = Int32.Parse(tokens[1]);
            int stride = Int32.Parse(tokens[2]);
            IntPtr pointer = new IntPtr(Int64.Parse(tokens[3]));

            // 与えられた情報・ポインタをBitmapに復元
            using (var bitmap = new Bitmap(width, height, stride, PixelFormat.Format32bppArgb, pointer))
            {
                // ファイルに出力
                bitmap.Save(@"C:\temp\slave.png", ImageFormat.Png);
            }
        }
    }
}

上記コードを見てわかる通り、Bitmapのメモリ領域はこのプログラムでは全く用意していません。Master様のおしごとです。

Master側プログラム

上記Slaveプロセスを作り、命令を出す方のプログラムです。説明は全部コメントで書きました。

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;

namespace Master
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            // Bitmapを開き、画素データのポインタを得る
            using (var bitmap = new Bitmap("lena.png"))
            {
                BitmapData bd = bitmap.LockBits(
                    new Rectangle(0, 0, bitmap.Width, bitmap.Height), 
                    ImageLockMode.ReadOnly, 
                    PixelFormat.Format32bppArgb);
                int length = bd.Stride * bd.Height;

                // slaveプロセス開始
                var info = new ProcessStartInfo
                {
                    FileName = "Slave.exe",
                    RedirectStandardInput = true,
                    UseShellExecute = false,
                };
                var process = Process.Start(info);
                IntPtr hProcess = IntPtr.Zero;

                try
                {
                    // プロセスのハンドルを取得
                    hProcess = Win32Api.OpenProcess(
                        Win32Api.PROCESS_VM_READ | Win32Api.PROCESS_VM_WRITE, false, process.Id);
                    IntPtr processMemory = IntPtr.Zero;
                    try
                    {
                        // slaveプロセスの仮想アドレス空間のメモリを確保
                        processMemory = Win32Api.VirtualAllocEx(
                            hProcess, IntPtr.Zero, length,
                            Win32Api.MEM_COMMIT | Win32Api.MEM_RESERVE,
                            Win32Api.PAGE_READWRITE);
                        // 確保したメモリに、画像データをコピー
                        int writtenSize;
                        Win32Api.WriteProcessMemory(
                            hProcess, processMemory, bd.Scan0, length, out writtenSize);

                        // 画像サイズ・ポインタの情報
                        var arguments = String.Format(
                            "{0},{1},{2},{3}",
                            bitmap.Width, bitmap.Height, bd.Stride, processMemory);

                        // Slaveに命令
                        process.StandardInput.WriteLine(arguments);
                    }
                    finally
                    {
                        if (processMemory != IntPtr.Zero)
                        {
                            Win32Api.VirtualFreeEx(hProcess, processMemory, length, Win32Api.MEM_RELEASE);
                        }
                    }
                }
                finally
                {
                    if (hProcess != IntPtr.Zero)
                    {
                        Win32Api.CloseHandle(hProcess);
                    }
                    process.Dispose();
                }

                bitmap.UnlockBits(bd);
            }
        }
    }
}

以上です。ごちゃごちゃして見えるかもしれませんが実に簡単です。Win32Apiクラスの実装は最後に示します。

逆にSlaveからデータを受け取りたいときは、WriteProcessMemoryではなくReadProcessMemoryを使うだけです。

Win32Apiクラス

using System;
using System.Runtime.InteropServices;

namespace Master
{
    // ReSharper disable InconsistentNaming

    static class Win32Api
    {
        // プロセスオブジェクトで認められるアクセス方法を指定します
        public const int 
            PROCESS_VM_READ = 0x0010,
            PROCESS_VM_WRITE = 0x0020;

        // メモリ確保のタイプを指定する、一連のビットフラグを指定します
        public const int
            MEM_COMMIT = 0x1000,
            MEM_RESERVE = 0x2000;

        // 解放操作のタイプを表す一連のビットフラグを指定します
        public const int
            MEM_DECOMMIT = 0x4000,
            MEM_RELEASE = 0x8000;

        // 割り当てたいページ領域のアクセス保護のタイプを指定する、一連のビットフラグを指定します
        public const int
            PAGE_NOACCESS = 0x01,
            PAGE_READONLY = 0x02,
            PAGE_READWRITE = 0x04,
            PAGE_WRITECOPY = 0x08;

        /// <summary>
        /// 指定されたプロセスの仮想アドレス空間内のメモリ領域の予約とコミットの一方または両方を行います。
        /// この関数は MEM_RESET フラグがセットされていない限り、確保されるメモリが自動的に 0 で初期化されます。
        /// </summary>
        /// <param name="hProcess">割り当てたいメモリを保持するプロセス</param>
        /// <param name="lpAddress">割り当てたい開始アドレス</param>
        /// <param name="dwSize">割り当てたい領域のバイト単位のサイズ</param>
        /// <param name="flAllocationType">割り当てのタイプ</param>
        /// <param name="flProtect">アクセス保護のタイプ</param>
        /// <returns></returns>
        [DllImport("kernel32")]
        public static extern IntPtr VirtualAllocEx(
            IntPtr hProcess, IntPtr lpAddress, int dwSize, int flAllocationType, int flProtect);

        /// <summary>
        /// 指定されたプロセスの仮想アドレス空間内のメモリ領域を解放またはコミット解除します。
        /// </summary>
        /// <param name="hProcess">解放したいメモリを保持するプロセス</param>
        /// <param name="lpAddress">解放したいメモリ領域の開始アドレス</param>
        /// <param name="dwSize">解放したいメモリ領域のバイト単位のサイズ</param>
        /// <param name="dwFreeType">解放操作のタイプ</param>
        /// <returns></returns>
        [DllImport("kernel32")]
        public static extern int VirtualFreeEx(
            IntPtr hProcess, IntPtr lpAddress, int dwSize, int dwFreeType);

        /// <summary>
        /// 指定されたプロセスのメモリ領域にデータを書き込みます。
        /// </summary>
        /// <param name="hProcess">プロセスのハンドル</param>
        /// <param name="lpBaseAddress">書き込み開始アドレス</param>
        /// <param name="lpBuffer">データバッファ</param>
        /// <param name="nSize">書き込みたいバイト数</param>
        /// <param name="lpNumberOfBytesWritten">実際に書き込まれたバイト数</param>
        /// <returns></returns>
        [DllImport("kernel32")]
        public static extern int WriteProcessMemory(
            IntPtr hProcess, IntPtr lpBaseAddress,
            IntPtr lpBuffer, int nSize, out int lpNumberOfBytesWritten);

        /// <summary>
        /// 指定されたプロセスのメモリ領域からデータを読み取ります。
        /// </summary>
        /// <param name="hProcess">プロセスのハンドル</param>
        /// <param name="lpBaseAddress">読み取り開始アドレス</param>
        /// <param name="lpBuffer">データを格納するバッファ</param>
        /// <param name="nSize">読み取りたいバイト数</param>
        /// <param name="lpNumberOfBytesRead">読み取ったバイト数</param>
        /// <returns></returns>
        [DllImport("kernel32")]
        public static extern int ReadProcessMemory(
            IntPtr hProcess, IntPtr lpBaseAddress,
            IntPtr lpBuffer, int nSize, out int lpNumberOfBytesRead);

        /// <summary>
        /// 既存のプロセスオブジェクトのハンドルを開きます。
        /// </summary>
        /// <param name="dwDesiredAccess">アクセスフラグ</param>
        /// <param name="bInheritHandle"></param>
        /// <param name="dwProcessId"></param>
        /// <returns></returns>
        [DllImport("kernel32")]
        public static extern IntPtr OpenProcess(
            int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

        /// <summary>
        /// 開いているオブジェクトハンドルを閉じます。
        /// </summary>
        /// <param name="hObject">オブジェクトのハンドル</param>
        /// <returns></returns>
        [DllImport("kernel32")]
        public static extern int CloseHandle(IntPtr hObject);
    }
}