はじめに
消耗していたのは、私のことです。。
Win32API の GetLastError 関数に関連するエラーメッセージを全部見てみたくて、0 から 0xFFFFFFFF まで for 文でループさせてみたり、winerror.h から #define ラベル部分を抜いてC言語ソースにして抜いてみたりしましたが、前者はとても時間がかかりますし、後者はヘッダに定義のないメッセージが取得対象から外れてしまいます。
幸運にもリソースのメッセージテーブルからまるごと文字列データを抜き出す方法を知ることが出来ましたので、あえてC#にてここにまとめてみます。VisualBasicのエラーメッセージもこれで一覧にすることが出来ますよ。
2018/5/22:ストリングテーブル (RT_STRING) に関する姉妹編も投稿してみました。
参考にしたサイト
- Enumerating Message Table Contents - Stefan Kuhr さん (CodeProject)。あなたのおかげですよ!解説と C++ のサンプルコードあり
- System Error Codes - Windows デベロッパーセンター (マイクロソフト)エラーコード情報なら、わざわざコードを書いて抜き出さなくてもここに一覧があります
コードと解説
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
);
}
}
}
データ構造
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 の構造があり、先頭 4バイト (NumberOfBlocks) には後続する MESSAGE_RESOURCE_BLOCK 構造の個数が入っていて、直後から MESSAGE_RESOURCE_BLOCK 構造が連続して格納されています。
MESSAGE_RESOURCE_BLOCK
それぞれの MESSAGE_RESOURCE_BLOCK には、連続するメッセージIDをもつ MESSAGE_RESOURCE_ENTRY の情報が格納されており、先頭のメッセージ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 関数を呼び出してモジュールをアンロードすることでリリースされます。
コードのきも
- メッセージテーブルは、リソース名 1 つまり MAKEINTRESOURCE(1) にすべて集約されています。
- FindResource関数では取得できず、FindResourceEx関数で取得できます。外国語もリソースに含んでいれば、EnumResourceLanguages 関数で言語IDを列挙して、それを指定して取得できます。
おわりに
先達に感謝します!
内容や誤字のご指摘、お待ちしております。