FormatMessageのループでまだ消耗しているの！？dllなどからメッセージテーブルを根こそぎ取得する方法

#はじめに
消耗していたのは、私のことです。。
Win32API の [GetLastError](https://msdn.microsoft.com/ja-jp/library/cc428944.aspx) 関数に関連するエラーメッセージを全部見てみたくて、0 から 0xFFFFFFFF まで for 文でループさせてみたり、winerror.h から #define ラベル部分を抜いてC言語ソースにして抜いてみたりしましたが、前者はとても時間がかかりますし、後者はヘッダに定義のないメッセージが取得対象から外れてしまいます。
幸運にもリソースのメッセージテーブルからまるごと文字列データを抜き出す方法を知ることが出来ましたので、あえてC#にてここにまとめてみます。VisualBasicのエラーメッセージもこれで一覧にすることが出来ますよ。

2018/5/22：[ストリングテーブル](https://qiita.com/otagaisama-1/items/de96cfaf298fd3286e05) (RT_STRING) に関する姉妹編も投稿してみました。

#参考にしたサイト
- [Enumerating Message Table Contents](https://www.codeproject.com/Articles/14444/Enumerating-Message-Table-Contents) - Stefan Kuhr さん ([CodeProject](https://www.codeproject.com/))。あなたのおかげですよ！解説と C++ のサンプルコードあり
- [System Error Codes](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms681381.aspx) - [Windows デベロッパーセンター](https://developer.microsoft.com/ja-jp/windows) （マイクロソフト）エラーコード情報なら、わざわざコードを書いて抜き出さなくてもここに一覧があります

#コードと解説

```c#:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

namespace EnumSystemMessagesTool
{
    class Program
    {
        static void Main(string[] args)
        {
            //  サンプルDLL
            var targetFilePath = @"C:\Windows\System32\kernel32.dll";

            var messages = EnumMessageTableMessages(targetFilePath);
            if (!messages.Any())
            {
                Console.WriteLine("読み込めませんでした");
                return;
            }
            foreach (var msg in messages)
            {
                Console.WriteLine("{0} : {1}", msg.Item1, msg.Item2);
            }
            Console.WriteLine("おわりました");
            Console.Read();
        }

        static IEnumerable<System.Tuple<UInt32, string>> EnumMessageTableMessages(string filePath)
        {
            //  メッセージテーブルリソースをもつファイルを読み込みます。
            var hModule = NativeMethods.LoadLibraryEx(filePath, IntPtr.Zero, NativeMethods.LoadLibraryExFlags.DONT_RESOLVE_DLL_REFERENCES | NativeMethods.LoadLibraryExFlags.LOAD_LIBRARY_AS_DATAFILE);
            if (IntPtr.Zero.Equals(hModule))
                yield break;
            try
            {
                var hRsrc = NativeMethods.FindResourceEx(hModule, new IntPtr(NativeMethods.RT_MESSAGETABLE), new IntPtr(1), NativeMethods.LANGID_NEUTRAL);
                if (!IntPtr.Zero.Equals(hRsrc))
                {
                    var hGlobal = NativeMethods.LoadResource(hModule, hRsrc);
                    if (!IntPtr.Zero.Equals(hGlobal))
                    {
                        var topOfResourceData = NativeMethods.LockResource(hGlobal);
                        if (!IntPtr.Zero.Equals(topOfResourceData))
                        {
                            // ブロック数はDWORD
                            var numOfBlocks = Marshal.ReadInt32(topOfResourceData);
                            var ptrInBlocks = topOfResourceData + 4;
                            for(int i = 0; i < numOfBlocks; i++)
                            {
                                // 先頭IDはDWORD
                                var lowID = (UInt32)Marshal.ReadInt32(ptrInBlocks);
                                ptrInBlocks += 4;
                                // 終端IDはDWORD
                                var highID = (UInt32)Marshal.ReadInt32(ptrInBlocks);
                                ptrInBlocks += 4;
                                // オフセットはDWORD
                                var offsetFromTop = Marshal.ReadInt32(ptrInBlocks);
                                ptrInBlocks += 4;

                                var ptrOfMessage = topOfResourceData + offsetFromTop;
                                for(var j = lowID; j <= highID; j++)
                                {
                                    // この構造のバイトサイズはWORD
                                    var lengthInBytes = Marshal.ReadInt16(ptrOfMessage);
                                    var messageLengthInBytes = lengthInBytes - 4;
                                    ptrOfMessage += 2;
                                    // フラグ（文字種）はWORD
                                    var flag = Marshal.ReadInt16(ptrOfMessage);
                                    ptrOfMessage += 2;

                                    string str = string.Empty;
                                    switch (flag)
                                    {
                                        case 0:
                                            str = Marshal.PtrToStringAnsi(ptrOfMessage, messageLengthInBytes);
                                            break;
                                        case 1:
                                            str = Marshal.PtrToStringUni(ptrOfMessage, messageLengthInBytes / 2);
                                            break;
                                    }
                                    ptrOfMessage += messageLengthInBytes;

                                    yield return new Tuple<uint, string>(j, str.Trim());
                                }
                            }
                        }
                    }
                }
            }
            finally
            {
                // 読み込んだファイルを閉じます。
                NativeMethods.FreeLibrary(hModule);
            }

        }

        private class NativeMethods
        {
            public const int RT_MESSAGETABLE = 0x0000000B;
            public const UInt16 LANGID_NEUTRAL = 0x0000;

            [Flags]
            public enum LoadLibraryExFlags : UInt32
            {
                DONT_RESOLVE_DLL_REFERENCES = 0x0000001,
                LOAD_LIBRARY_AS_DATAFILE = 0x00000002
            }

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern IntPtr LoadLibraryEx(
                string lpFileName,
                IntPtr hFile,
                LoadLibraryExFlags dwFlags
                );

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool FreeLibrary(
                IntPtr hModule
                );

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern IntPtr FindResourceEx(
                IntPtr hModule,
                IntPtr lpszType,
                IntPtr lpszName,
                UInt16 wLangID
            );

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern IntPtr LoadResource(
                IntPtr hModule,
                IntPtr hRsrc
            );

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern IntPtr LockResource(
                IntPtr hResData
            );
        }
    }
}
```

##データ構造

```c++:winnt.hの一部
typedef struct _MESSAGE_RESOURCE_DATA {
    DWORD NumberOfBlocks;
    MESSAGE_RESOURCE_BLOCK Blocks[ 1 ];
} MESSAGE_RESOURCE_DATA, *PMESSAGE_RESOURCE_DATA;

typedef struct _MESSAGE_RESOURCE_BLOCK {
    DWORD LowId;
    DWORD HighId;
    DWORD OffsetToEntries;
} MESSAGE_RESOURCE_BLOCK, *PMESSAGE_RESOURCE_BLOCK;

typedef struct _MESSAGE_RESOURCE_ENTRY {
    WORD   Length;
    WORD   Flags;
    BYTE  Text[ 1 ];
} MESSAGE_RESOURCE_ENTRY, *PMESSAGE_RESOURCE_ENTRY;
```

###MESSAGE_RESOURCE_DATA
リソースデータをロックして得られるメモリ領域に、上記のような構造でメッセージテーブルのデータが格納されています。
先頭に [MESSAGE_RESOURCE_DATA](https://msdn.microsoft.com/en-us/library/windows/desktop/ms648021.aspx) の構造があり、先頭 4バイト (NumberOfBlocks) には後続する [MESSAGE_RESOURCE_BLOCK](https://msdn.microsoft.com/en-us/library/windows/desktop/ms648020.aspx) 構造の個数が入っていて、直後から MESSAGE_RESOURCE_BLOCK 構造が連続して格納されています。

###MESSAGE_RESOURCE_BLOCK
それぞれの MESSAGE_RESOURCE_BLOCK には、連続するメッセージIDをもつ [MESSAGE_RESOURCE_ENTRY](https://msdn.microsoft.com/en-us/library/windows/desktop/ms648022.aspx) の情報が格納されており、先頭のメッセージID (LowId) と最後のメッセージID (HighId)、メッセージの実体である MESSAGE_RESOURCE_ENTRY 構造の、ロックして得られたメモリ領域の先頭からのバイトオフセット (OffsetToEntries) が、おのおの 4バイトで入っています。

###MESSAGE_RESOURCE_ENTRY
MESSAGE_RESOURCE_ENTRY には、Length や Flags を含む、自身のMESSAGE_RESOURCE_ENTRY 構造全体のバイトサイズが Length に、Flagsにはメッセージテキストデータのエンコード (0:ANSI 1:Unicode) が格納されており、さらにメッセージテキストデータが Length - 4 バイト分入っています。そして、その次 (=+1) のメッセージIDをもつMESSAGE_RESOURCE_ENTRY構造が直後につづきます。

ロックしたリソースは [FreeLibrary](https://msdn.microsoft.com/ja-jp/library/cc429103.aspx) 関数を呼び出してモジュールをアンロードすることでリリースされます。

##コードのきも
- メッセージテーブルは、リソース名 1 つまり [MAKEINTRESOURCE](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms648029.aspx)(1) にすべて集約されています。
- [FindResource](https://msdn.microsoft.com/ja-jp/library/cc410845.aspx)関数では取得できず、[FindResourceEx](https://msdn.microsoft.com/ja-jp/library/cc364614.aspx)関数で取得できます。外国語もリソースに含んでいれば、[EnumResourceLanguages](https://msdn.microsoft.com/ja-jp/library/cc410819.aspx) 関数で言語IDを列挙して、それを指定して取得できます。

#おわりに
先達に感謝します！
内容や誤字のご指摘、お待ちしております。