C# PEファイルの文字列リソースをすべて取得する

# 言語と動作確認環境

- C# 8、.NET Core 3.1 (Preview)
- Visual Studio Community 2019 Preview（Version 16.7.0 Preview 1.0）、Windows 10。

# 目的
C# 8と.NET Core 3.1でPEファイルの文字列リソースをすべて取得するサンプルコードです。同様のコードはQiitaや海外サイトでも公開されていますが、自身の学習のために作成しています。

# サンプルコード
```cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            using var handle = NativeMethods.LoadLibraryExW(
                "user32.dll",
                IntPtr.Zero,
                NativeMethods.DONT_RESOLVE_DLL_REFERENCES
                | NativeMethods.LOAD_LIBRARY_AS_DATAFILE
                | NativeMethods.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
            if (handle.IsInvalid)
            {
                throw new Win32Exception();
            }

            var strings = GetStringResources(handle.DangerousGetHandle());
        }

        /// <summary>
        /// ある型のリソースのIDをすべて取得します。
        /// </summary>
        private static ushort[] GetResourceIDs(IntPtr moduleHandle, IntPtr resourceType)
        {
            var resnames = new List<ushort>();
            NativeMethods.EnumResourceNamesW(
                moduleHandle,
                resourceType,
                (IntPtr hModule, IntPtr lpszType, IntPtr lpszName, nint lParam) =>
                {
                    var id = lpszName.ToInt64();
                    if (id >> 16 != 0)
                        throw new Exception();
                    resnames.Add((ushort)id);
                    return true;
                },
                0);
            return resnames.ToArray();
        }

        /// <summary>
        /// モジュールの文字列リソースをすべて取得します。
        /// </summary>
        private static string[] GetStringResources(IntPtr moduleHandle)
        {
            var stringResIds = GetResourceIDs(moduleHandle, NativeMethods.RT_STRING);
            Array.Sort(stringResIds);
            var strings = new List<string>();
            foreach (var strResId in stringResIds)
            {
                var resHandle = NativeMethods.FindResourceW(moduleHandle,
                    new IntPtr(strResId), NativeMethods.RT_STRING);
                var memoryHandle = NativeMethods.LoadResource(moduleHandle, resHandle);
                var size = NativeMethods.SizeofResource(moduleHandle, resHandle);
                // pointerの中身は2バイト（文字数N）＋N*2バイト（UTF-16文字列）の配列
                var pointer = NativeMethods.LockResource(memoryHandle);
                for (int offset = 0; offset < size;)
                {
                    uint len = (ushort)Marshal.ReadInt16(pointer + offset);
                    strings.Add(Marshal.PtrToStringUni(pointer + offset + sizeof(ushort), (int)len));
                    offset += sizeof(ushort) + (int)len * 2;
                }
            }
            return strings.ToArray();
        }

        private static class NativeMethods
        {
            [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
            public static extern SafeModuleHandle LoadLibraryExW(
                [In] string lpLibFileName,
                IntPtr hFile,
                uint dwFlags);

            [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool EnumResourceNamesW(
                IntPtr hModule,
                IntPtr lpType,
                EnumResNameProcW lpEnumFunc,
                nint lParam);

            public const uint DONT_RESOLVE_DLL_REFERENCES = 0x00000001;
            public const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002;
            public const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;

            [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public delegate bool EnumResNameProcW(IntPtr hModule, IntPtr lpszType, IntPtr lpszName, nint lParam);

            public static readonly IntPtr RT_STRING = new IntPtr(6);

            [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)]
            public static extern IntPtr FindResourceW(
                IntPtr hModule,
                IntPtr lpName,
                IntPtr lpType);

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

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

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern uint SizeofResource(
                IntPtr hModule,
                IntPtr hResInfo);
        }
    }

    public sealed class SafeModuleHandle : SafeHandle
    {
        private static class NativeMethods
        {
            [DllImport("kernel32.dll")]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool FreeLibrary(IntPtr hLibModule);
        }

        public SafeModuleHandle()
            : base(IntPtr.Zero, true)
        {
        }

        public SafeModuleHandle(IntPtr handle, bool ownsHandle)
            : base(handle, ownsHandle)
        {
        }

        public override bool IsInvalid => handle == IntPtr.Zero;

        protected override bool ReleaseHandle()
        {
            return NativeMethods.FreeLibrary(handle);
        }
    }
}
```

# `HMODULE`型の`SafeHandle`によるラップ

`LoadLibraryEx`関数の戻り値は`HMODULE`型のハンドルであり、使用後は`FreeLibrary`関数で解放する必要があります。C#では`using`構文と`SafeHandle`の派生クラスにより確実な解放を保証できるため、ここでは`SafeHandle`を継承したクラスを作成しています。

このハンドルを解放しなかった場合、プロセスの終了までライブラリがメモリ存在したままとなります。なお、同じ`HMODULE`型を返す関数でも`GetModuleHandle`関数の戻り値は基本的に解放してはいけません。

# `LoadLibraryEx`関数とフラグ

`LoadLobraryEx`関数の呼び出し時にはいくつかのフラグを指定することができます。今回は既定の場所からPEファイルを読み込み、そのリソースのみが必要なので以下のフラグを指定しています。

|フラグ|目的|
|:--|:--|
|DONT_RESOLVE_DLL_REFERENCES|DLL参照の解決を無効化する。|
|LOAD_LIBRARY_AS_DATAFILE|データファイルとして読み込む。|
|LOAD_LIBRARY_SEARCH_DEFAULT_DIRS|既定の場所から検索する。|

# `LoadResource`関数と`LockResource`関数

`LoadResource`関数と`LockResource`関数は後方互換性のために別々に存在します。Windows 10ではどちらも同じ値を返します。戻り値はプロセスの終了時に自動的に解放されます（[Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/win32/api/libloaderapi/nf-libloaderapi-lockresource)）。

# ポインタから整数や文字列の読み込み

`System.Runtime.InteropServices`名前空間の`Marshal`クラスを使用してポインタから整数や文字列を読み込むことができます。16ビット整数は`Marshal.ReadInt16`、UTF-16文字列の読み込みは`Marshal.PtrToStringUni`です。

# `RT_STRING`型リソースの中身

`RT_STRING`型リソースは各IDに長さ（UInt16、2バイト）とその長さのUTF-16文字列（Char型）のペアが1～32個含まれます。各IDに含まれる文字列の個数は記録されていませんが、`SizeofResource`関数で取得したバイト数まで上記ペアを読み込むことですべての文字列を取得できます。

````plain:RT_STRING型リソースの模式図
#ID 1 <- バイト数はSizeofResource関数で取得
Length(2バイト), String(Length文字 = Length*2バイト)
...
Length(2バイト), String(Length文字 = Length*2バイト) <- 最大32個

#ID 2
Length(2バイト), String(Length文字 = Length*2バイト)
...
Length(2バイト), String(Length文字 = Length*2バイト)

...

#ID N <- EnumResourceNamesで見つかった個数だけ存在
...
```