CsWin32でWindows用ネイティブEXEからリソースデータを取り出す場合の注意点

# はじめに

Windows用の実行ファイル(Portable Executable)から、リソースデータを取り出す方法を調べたので詰まった点を残しておく。
リソースAPI自体の使い方は、下記のMS公式ドキュメントを参考にすること

* [リソースAPIの概要](https://learn.microsoft.com/en-us/windows/win32/menurc/resources-overviews)
* [実装例](https://learn.microsoft.com/en-us/windows/win32/menurc/using-resources)

# 使用するライブラリの準備

補助ライブラリとして、[CsWin32](https://github.com/microsoft/CsWin32)を使用する。

CsWin32とは、ネイティブDLLのAPI参照をソース生成で行うプロジェクトの事で、下記手順で使用することができる。
なお、TargetFrameworkに"-windows"サフィックスをつけていることからも察せられると思うが、同ライブラリはWindows上で実行するときにしか使えないことに注意が必要(メソッドを呼び出さなければビルド自体は可能)。

1. プロジェクトのTargetFrameworkを`net[バージョン]-windows`にする
    * これをしないとコンパイル時に[CA1416警告](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1416)が出る(エラーにはならない)
2. [`Microsoft.Windows.CsWin32`](https://www.nuget.org/packages/Microsoft.Windows.CsWin32/#readme-body-tab)をnugetパッケージとして追加
3. csprojと同じディレクトリに"NativeMethods.txt"を作成
4. 一行に一つずつインポートしたい関数、構造体、マクロ、定数を記述する
    * UnicodeとASCIIをマクロで使い分けているものは、サフィックス無しでインポートしても使用可能で、その場合はUnicode系APIがインポートされる

NativeMethods.txtの記述例は以下。

```
LoadLibrary
HRSRC
GetLocaleInfoEx
LockResource
FindResourceEx
LoadResource
EnumResourceNames
LOCALE_NAME_INVARIANT
RT_STRING
```

このように記述すると、`class Windows.Win32.PInvoke`以下にメソッドが、`namespace Windows.Win32.Foundation`に構造体の定義等が入る。
なお、全ての構造体を記述しなくても、メソッドに関連する構造体ならばある程度は自動的に引っ張ってきてくれる。
また、HANDLE型等の後処理が必要な型に関しては、SafeHandleでラップしてくれるものもある(全てではない)。
WindowsAPIの中には、出力先バッファへのポインタ+バッファの長さという形式の引数も存在するが、後述するGetLocaleInfoEx等、一部のAPIではSpanとして扱うことができるオーバーライドも生成される。

リソースAPIそのものの使い方は冒頭にリンクを置いた公式ドキュメントを参考にしてもらうとして、注意すべき点を書いていく。

# リソースIDの作り方

[FindResourceEx](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-findresourceexa)等、多くの場面で指定する機会の多いlpType及びlpNameの値だが、リソースAPIの場合単純な名前ではなく、ポインタの中に直接IDの値を入れるという指定の仕方をする場合がある。
ポインタの中に直接IDの値が入っていた場合、PWSTRをToString等すると例外を起こす場合がある。

C++の場合は[MAKEINTRESOURCEマクロ](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-makeintresourcea)を使用して値を作成し、[IS_INTRESOURCEマクロ](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-is_intresource)で通常の文字列とリソースIDが入っているかどうか判別していた。
しかし、CsWin32ではこれらのマクロはインポートできないので、自分で実装する必要がある。
とはいえそれほど複雑な話ではなく、以下のようなコードで同じ機能を実現できる
```csharp
// 判別
public static bool IsIntResource(UIntPtr ptr)
{
    // 16bitより上が全て0の場合はリソースIDが直接入っているものとみなす
    // 参考: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-is_intresource
    return (((UIntPtr.MaxValue) << 16) & ptr) == 0;
}
// 値の作成
public static PWSTR MakeIntPwstrResource(IntPtr ptr)
{
    return new PWSTR(ptr);
}
// PCWSTRの場合はなぜかUIntPtrが直接渡せないのでunsafeの中でポインタにキャストして渡す
public static PCWSTR MakeIntPcwstrResource(IntPtr ptr)
{
    unsafe
    {
        return new PCWSTR((char*)ptr);
    }
}
```

lpTypeに指定する値としては、C++ではRT_STRING等のマクロを使用していたが、CsWin32でもそれらのマクロは使用可能なので、NativeMethodsで指定してインポートする。

# 言語IDの取得

[FindResourceEx](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-findresourceexa)等で指定するwLanguageだが、[MAKELANGID](https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-makelangid)はインポートできない。
また、ドキュメントには載ってないがMAKELANGIDはdeprecatedになっており、将来的に廃止される可能性がある。
なので、値の取得は[`GetLocaleInfoEx(PCWSTR lpLocaleName, LCTYPE LCType, Span<char> lpLCData)`](https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getlocaleinfoex)を使用する。
lpLocaleNameには"ja-JP"等具体的な名前を入れても問題ないが、"LOCALE_NAME_INVARIANT"等のマクロもNativeMethodsでインポートが可能。
LCTypeは [LOCALE_ILANGUAGE](https://learn.microsoft.com/en-us/windows/win32/intl/locale-ilanguage) をNativeMethodsでインポートして使用する。
成功すれば、lpLCDataに16進数の文字列("411"(10進数では1041)等)が格納されるので、`ushort.Parse`でパースしてushortの数値として取得する。

# 終わりに

CsWin32を利用すればかなり楽になるのは事実だが、WindowsのリソースAPIの「lpTypeあるいはlpNameにIDの値を直接入れる」という特殊性に留意していないと、思わぬ所で例外を起こす可能性があるので注意すること。
なお、今回検証に使用したソースのリポジトリリンクを下記に置いておく。
* [resourcecswin32](https://github.com/itn3000/penetlabs/tree/master/resourcecswin32)

今後は、LinuxでWin32APIを使わずにリソースデータを取り出すやり方をそのうち書きたい。