HarmonyLibが提供する便利なリフレクション補助ツール

.NETアプリケーションの振る舞いを変更したり、内部状態を調査したりする際、リフレクションは非常に強力なツールです。特に、HarmonyLibのようなパッチングフレームワークを使用する場合、リフレクション操作は不可欠となります。本稿では、HarmonyLibが提供する、リフレクションをより簡単かつ強力にするためのユーティリティ群を紹介します。

型情報へのアクセスを簡素化する AccessTools

AccessToolsは、静的なリフレクション操作を大幅に簡素化するために設計された、HarmonyLibの中心的なツールキットの一つです。型のメソッド、プロパティ、フィールド、コンストラクタなどを効率的に取得できます。HarmonyLibの内部でも頻繁に使用されており、例えばパッチ定義の際に特定のメソッドを指定する場面などでその恩恵を受けられます。

以下に、System.Threading.Threadクラスを例にとり、AccessToolsの様々な使用例を示します。

using System;
using System.Reflection;
using System.Threading;
using HarmonyLib; // HarmonyLibをインポート

class ReflectionExamples
{
    static void Main(string[] args)
    {
        // 1. 引数なしの Thread.Start() メソッドを取得
        MethodInfo startMethod = AccessTools.Method(typeof(Thread), "Start", Type.EmptyTypes);
        Console.WriteLine($"1. Start() メソッド: {startMethod?.Name ?? "N/A"}");

        // 2. Thread(ThreadStart start) コンストラクタを取得
        ConstructorInfo threadCtor = AccessTools.Constructor(typeof(Thread), new Type[] { typeof(ThreadStart) });
        Console.WriteLine($"2. Thread コンストラクタ: {threadCtor?.Name ?? "N/A"}");

        // 3. Thread.IsBackground プロパティのセッターを取得
        MethodInfo setIsBackground = AccessTools.PropertySetter(typeof(Thread), "IsBackground");
        Console.WriteLine($"3. IsBackground プロパティセッター: {setIsBackground?.Name ?? "N/A"}");

        // 4. Thread.ManagedThreadId プロパティのゲッターを取得
        MethodInfo getManagedThreadId = AccessTools.PropertyGetter(typeof(Thread), "ManagedThreadId");
        Console.WriteLine($"4. ManagedThreadId プロパティゲッター: {getManagedThreadId?.Name ?? "N/A"}");

        // 5. Thread.Sleep(int millisecondsTimeout) メソッドを取得
        MethodInfo sleepMethod = AccessTools.Method(typeof(Thread), "Sleep", new Type[] { typeof(int) });
        Console.WriteLine($"5. Sleep(int) メソッド: {sleepMethod?.Name ?? "N/A"}");

        // 6. Thread クラスで宣言されているすべてのフィールドを取得
        FieldInfo[] declaredFields = AccessTools.GetDeclaredFields(typeof(Thread));
        Console.WriteLine($"6. Threadクラスの宣言済みフィールド数: {declaredFields.Length}");

        // 7. Thread クラスで宣言されているすべてのメソッドを取得
        MethodInfo[] declaredMethods = AccessTools.GetDeclaredMethods(typeof(Thread));
        Console.WriteLine($"7. Threadクラスの宣言済みメソッド数: {declaredMethods.Length}");

        // 8. Thread クラスの内部型 "StartHelper" を取得 (存在する場合)
        Type startHelperType = AccessTools.Inner(typeof(Thread), "StartHelper");
        Console.WriteLine($"8. Thread.StartHelper 内部型: {(startHelperType != null ? "取得成功" : "取得失敗")}");

        Console.ReadLine();
    }
}

AccessToolsを使用することで、煩雑なリフレクションAPIを直接扱うことなく、必要なメンバー情報を簡潔に取得できることがわかります。

インスタンスの詳細を探る Traverse

AccessToolsが主に型レベルのリフレクションを扱うのに対し、Traverseは特定のオブジェクトインスタンスに対して動的なリフレクションを実行し、その内部状態(プライベートフィールドを含む)を探索・操作するためのツールです。

以下の例では、Threadインスタンスの内部状態にアクセスし、プライベートフィールドの値を読み取ったり、プロパティを設定したり、メソッドを呼び出したりする方法を示します。

using System;
using System.Threading;
using HarmonyLib; // HarmonyLibをインポート

class InstanceTraversalExamples
{
    static void Main(string[] args)
    {
        var workerThread = new Thread(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("Worker thread finished execution.");
        });

        // Traverse オブジェクトを作成し、スレッドインスタンスをラップ
        var threadTraverser = Traverse.Create(workerThread);

        // 1. プライベートフィールド "_startHelper" を介してスレッドのデリゲート情報を取得
        // 注意: プライベートフィールド名はランタイムや.NETバージョンによって変更される可能性があります。
        ThreadStart threadDelegate = threadTraverser.Field("_startHelper")
                                                    .Field("_start") // _startHelper内の_startフィールド
                                                    .GetValue<ThreadStart>();
        Console.WriteLine($"1. スレッドのデリゲートメソッド名: {threadDelegate?.Method.Name ?? "N/A"}");

        // 2. プライベートフィールド "_threadState" の値を読み取る
        int currentState = threadTraverser.Field("_threadState").GetValue<int>();
        Console.WriteLine($"2. スレッド初期状態: {currentState}");

        // 3. "IsBackground" プロパティを Traversal 経由で設定
        threadTraverser.Property("IsBackground").SetValue(true);
        Console.WriteLine($"3. IsBackground プロパティ設定後: {workerThread.IsBackground}");

        // 4. "Start" メソッドを Traversal 経由で呼び出す
        threadTraverser.Method("Start").GetValue();
        Console.WriteLine("4. スレッドを開始しました。");

        workerThread.Join(); // スレッドが完了するのを待機
        Console.WriteLine("メインスレッド終了。");

        Console.ReadLine();
    }
}

Traverseは、通常のリフレクションではアクセスが難しいプライベートメンバーに対しても容易にアクセスできるため、複雑なオブジェクトのデバッグや動的な操作に非常に有用です。

パッチ適用時のデバッグを助けるロギング機能

HarmonyLibを使ったパッチ適用は強力ですが、問題が発生した際にその原因を特定するのは難しい場合があります。HarmonyLibには、パッチング処理の詳細を記録するロギング機能が組み込まれており、デバッグ作業を大幅に支援します。

グローバルなデバッグロギングを有効にするには、Harmony.DEBUG = true;を設定するだけです。これにより、HarmonyLibはデスクトップにharmony.log.txtというファイルを生成し、パッチ適用に関する詳細なILコードや処理情報が出力されます。

using System;
using HarmonyLib; // HarmonyLibをインポート

// パッチを適用する対象クラスの例
public class TargetClass
{
    public void DisplayMessage(string msg)
    {
        Console.WriteLine($"オリジナルメッセージ: {msg}");
    }
}

// TargetClass.DisplayMessage メソッドへのパッチ
[HarmonyPatch(typeof(TargetClass), nameof(TargetClass.DisplayMessage), new Type[] { typeof(string) })]
public class MessagePatch
{
    // メソッド実行前に呼ばれるPrefixパッチ
    public static void Prefix(ref string msg)
    {
        Console.WriteLine("Prefix: メッセージを変更します。");
        msg = "Hello from HarmonyLib!"; // メッセージを変更
    }
}

class HarmonyLoggingExample
{
    static void Main(string[] args)
    {
        // HarmonyLibのデバッグロギングを有効にする
        Harmony.DEBUG = true;

        var harmonyInstance = new Harmony("com.example.messagepatch");
        harmonyInstance.PatchAll(); // すべてのHarmonyPatchアノテーションを適用

        Console.WriteLine("パッチ適用後、TargetClassのメソッドを呼び出します。");
        var target = new TargetClass();
        target.DisplayMessage("元のテキスト");

        // HarmonyLibの内部ロギングメカニズムを直接利用
        FileLog.Debug("追加のカスタムデバッグメッセージをログに記録しました。");

        Console.ReadLine();
    }
}

上記のコードを実行すると、harmony.log.txtにはTargetClass.DisplayMessageメソッドにパッチが適用された際のILコードの変更点などが記録されます。また、FileLog.Debug()を使用することで、HarmonyLibのロギングインフラを利用して独自のデバッグ情報をログファイルに追加することも可能です。

タグ: HarmonyLib .NET リフレクション C# パッチング

7月30日 01:59 投稿