🐙

ILPostProcessorによる定数書き換え入門

2023/12/08に公開

Applibot Advent Calendar 2023」 8日目の記事になります。
前日は @mori_yuki さんの「電子工作初心者が電子楽器作ってみた」という記事でした!

はじめに

本記事ではILPostProcessorを用いた定数書き換えについて紹介していきます。
筆者は最近ILPostProcessorを使い始めたが、自由に使いこなせるレベルではないので、お見苦しい点も多々あるかと思います。
温かい目で見守っていただけますと幸いです。

ILPostProcessorについて

ILPostProcessorは、Unityが生成するILに対して手を加えることができる仕組みです。
Unityのコンパイル後に処理が実行され、特定の値の書き換えやメソッドの書き換えなどを行うことができます。

実行環境

  • OS: macOS 12.4
  • Unity: 2022.3.11f1(SILICON)

書き換え予定クラスの用意

Settings
├── ProjectDefine.cs
├── Settings.asmdef
└── TemporaryClass.cs

ProjectDefine.cs
今回書き換え予定のクラス
TemporaryClass.cs
MonoBehaviourを継承した動作確認用クラス
SceneのGameObjectにアタッチ
Settings.asmdef
ProjectDefineを含むAssemblyを作成する

書き換え予定のクラス
ProjectDefine.cs
namespace Settings
{
    public class ProjectDefine
    {
        /// <summary>
        /// 書き換えたい文字列
        /// </summary>
        public string Value { get; } = "Hello World";
    }
}

ILPostProcessorの下準備

Mono.Cecilの導入

UnityのPackage Manager Windowより

  1. [+]を押下
  2. 「Add package from git URL ...」を押下
  3. com.unity.nuget.mono-cecilを入力

クラスの準備

ILPostProcessor
├── ConstantRewritingPostProcessor.cs
├── ILPostProcessorUtility.cs
├── PostProcessorAssemblyResolver.cs
└── Unity.Constant.Rewriting.CodeGen.asmdef

ILPostProcessorUtility.cs
PostProcessorAssemblyResolver.cs
こちらの2ファイルは参考記事1より拝借いたしました
https://github.com/sune2/MethodChangeWithILPostProcessor/tree/master

ConstantRewritingPostProcessor.cs
書き換え処置を記載していくクラス

Unity.Constant.Rewriting.CodeGen.asmdef
Unity.XXXX.CodeGenの名前で作成したasmdef

asmdefの設定


AutoReferenced
falseに設定
Override References
trueに設定
AssemblyDifinitionReferences
書き換えたいAssemblyの参照を追加
Assembly References
Mono.Cecil周りのDLLを追加

  • Mono.Cecil.dll
  • Mono.Cecil.Mdb.dll
  • Mono.Cecil.Pdb.dll
  • Mono.Cecil.Rocks.dll

Platform
Editorのみ有効

ILを読む

SharpLabを用いて、ProjectDefine.csを調べる

ProjectDefine.csのIL
ProjectDefine.cs -> IL
.assembly _
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = (
        01 00 08 00 00 00 00 00
    )
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = (
        01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78
        63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = (
        01 00 07 01 00 00 00 00
    )
    .permissionset reqmin = (
        2e 01 80 8a 53 79 73 74 65 6d 2e 53 65 63 75 72
        69 74 79 2e 50 65 72 6d 69 73 73 69 6f 6e 73 2e
        53 65 63 75 72 69 74 79 50 65 72 6d 69 73 73 69
        6f 6e 41 74 74 72 69 62 75 74 65 2c 20 53 79 73
        74 65 6d 2e 52 75 6e 74 69 6d 65 2c 20 56 65 72
        73 69 6f 6e 3d 37 2e 30 2e 30 2e 30 2c 20 43 75
        6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50
        75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 30
        33 66 35 66 37 66 31 31 64 35 30 61 33 61 15 01
        54 02 10 53 6b 69 70 56 65 72 69 66 69 63 61 74
        69 6f 6e 01
    )
    .hash algorithm 0x00008004 // SHA1
    .ver 0:0:0:0
}

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Settings.ProjectDefine
    extends [System.Runtime]System.Object
{
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
    )
    .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
        01 00 00 00 00
    )
    // Fields
    .field private initonly string '<Value>k__BackingField'
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )

    // Methods
    .method public hidebysig specialname 
        instance string get_Value () cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x20a2
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld string Settings.ProjectDefine::'<Value>k__BackingField'
        IL_0006: ret
    } // end of method ProjectDefine::get_Value

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20aa
        // Code size 19 (0x13)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldstr "Hello World"
        IL_0006: stfld string Settings.ProjectDefine::'<Value>k__BackingField'
        IL_000b: ldarg.0
        IL_000c: call instance void [System.Runtime]System.Object::.ctor()
        IL_0011: nop
        IL_0012: ret
    } // end of method ProjectDefine::.ctor

    // Properties
    .property instance string Value()
    {
        .get instance string Settings.ProjectDefine::get_Value()
    }

} // end of class Settings.ProjectDefine

.class private auto ansi sealed beforefieldinit Microsoft.CodeAnalysis.EmbeddedAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method EmbeddedAttribute::.ctor

} // end of class Microsoft.CodeAnalysis.EmbeddedAttribute

.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.NullableAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
        01 00 84 6b 00 00 02 00 54 02 0d 41 6c 6c 6f 77
        4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
        72 69 74 65 64 00
    )
    // Fields
    .field public initonly uint8[] NullableFlags

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            uint8 ''
        ) cil managed 
    {
        // Method begins at RVA 0x2059
        // Code size 24 (0x18)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldc.i4.1
        IL_0009: newarr [System.Runtime]System.Byte
        IL_000e: dup
        IL_000f: ldc.i4.0
        IL_0010: ldarg.1
        IL_0011: stelem.i1
        IL_0012: stfld uint8[] System.Runtime.CompilerServices.NullableAttribute::NullableFlags
        IL_0017: ret
    } // end of method NullableAttribute::.ctor

    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            uint8[] ''
        ) cil managed 
    {
        // Method begins at RVA 0x2072
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldarg.1
        IL_0009: stfld uint8[] System.Runtime.CompilerServices.NullableAttribute::NullableFlags
        IL_000e: ret
    } // end of method NullableAttribute::.ctor

} // end of class System.Runtime.CompilerServices.NullableAttribute

.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.NullableContextAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
        01 00 4c 14 00 00 02 00 54 02 0d 41 6c 6c 6f 77
        4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
        72 69 74 65 64 00
    )
    // Fields
    .field public initonly uint8 Flag

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            uint8 ''
        ) cil managed 
    {
        // Method begins at RVA 0x2082
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldarg.1
        IL_0009: stfld uint8 System.Runtime.CompilerServices.NullableContextAttribute::Flag
        IL_000e: ret
    } // end of method NullableContextAttribute::.ctor

} // end of class System.Runtime.CompilerServices.NullableContextAttribute

.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.RefSafetyRulesAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
        01 00 02 00 00 00 02 00 54 02 0d 41 6c 6c 6f 77
        4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
        72 69 74 65 64 00
    )
    // Fields
    .field public initonly int32 Version

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            int32 ''
        ) cil managed 
    {
        // Method begins at RVA 0x2092
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldarg.1
        IL_0009: stfld int32 System.Runtime.CompilerServices.RefSafetyRulesAttribute::Version
        IL_000e: ret
    } // end of method RefSafetyRulesAttribute::.ctor

} // end of class System.Runtime.CompilerServices.RefSafetyRulesAttribute


今回書き換えたいHello Worldは.ctorメソッド内ldstrに存在するため、そちらを書き換える方針で進める

ILについての理解を深める

今回のILについて

.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
publicなコンストラクタメソッド(.ctor)を定義している
IL_0000: ldarg.0
0番目の引数(つまり、このコンストラクタメソッド自身のインスタンス)を評価スタックにロード
IL_0001: ldstr "Hello World"
文字列"Hello World"を評価スタックにロード
IL_0006: stfld string Settings.ProjectDefine::'<Value>k__BackingField'
評価スタックの最上位にある値(つまり、"Hello World")をフィールド'<Value>k__BackingField'に保存
IL_000b: ldarg.0
再度、0番目の引数(つまり、このコンストラクタメソッド自身のインスタンス)を評価スタックにロード
IL_000c: call instance void [System.Runtime]System.Object::.ctor()
基底クラスSystem.Objectのコンストラクタを呼び出す
すべての.NETクラスが暗黙的にSystem.Objectを継承するための一般的なパターンである
IL_0011: nop
何もしない命令(no operation)
デバッガがブレークポイントを設定できるようにするためにしばしば使用される
IL_0012: ret
メソッドの終了と戻り値の返却を示す

.ctorについての理解を深める

.ctorとは、コンストラクターである
ProjectDefine.csではコンストラクタが定義されていないが、C#のクラスでは、明示的にコンストラクターを書かなくても、暗黙的にコンストラクターが1つ作られる
プロパティ初期化を定義した際はコンストラクタで初期化されるっぽい

PostProcessorの作成

定数書き換えのPostProcessor
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil.Cil;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;

namespace Constant.Rewriting.CodeGen
{
    public class ConstantRewritingPostProcessor : ILPostProcessor
    {
        public override ILPostProcessor GetInstance() => this;

        public override bool WillProcess(ICompiledAssembly compiledAssembly)
        {
            // Settings Assemblyのみを対象とする
            return compiledAssembly.Name == "Settings";
        }
        
        public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
        {
            if (!WillProcess(compiledAssembly))
            {
                return null;
            }

            using var assembly = ILPostProcessUtility.AssemblyDefinitionFor(compiledAssembly);
            var diagnosticMessageList = new List<DiagnosticMessage>();
            foreach (var type in assembly.MainModule.Types)
            {
                // Settings.ProjectDefine型を探す
                if (type.FullName != "Settings.ProjectDefine")
                {
                    continue;
                }

                // Settings.ProjectDefineのコンストラクタを取得する
                var methodDefinition = type.Methods.FirstOrDefault((method) => method.Name == ".ctor");
                
                if (methodDefinition == null)
                {
                    diagnosticMessageList.Add(new DiagnosticMessage()
                    {
                        MessageData = ".ctorが見つかりませんでした。",
                        DiagnosticType = DiagnosticType.Warning
                    });
                    continue;
                }

                var postProcessor = methodDefinition.Body.GetILProcessor();
                
                // ldstrを取得する
                var ldstrInstruction = postProcessor.Body.Instructions.FirstOrDefault((instruction) => instruction.OpCode == OpCodes.Ldstr);
                if (ldstrInstruction == null)
                {
                    diagnosticMessageList.Add(new DiagnosticMessage()
                    {
                        MessageData = "ldstrが見つかりませんでした。",
                        DiagnosticType = DiagnosticType.Warning
                    });
                    continue;
                }
                
                // ldstrのオペランドを書き換える
                ldstrInstruction.Operand = "Hello ILPostProcessor";
                return ILPostProcessUtility.GetResult(assembly, diagnosticMessageList);
            }
            return new ILPostProcessResult(null, diagnosticMessageList);
        }
    }
}

ProjectDefineのコンストラクタ.ctorにてプロパティが初期化されているため、SettingsのAssemblyからこちらを検索
LdstrのオペランドをHello ILPostProcessorに書き換えた

結果


実行時に書き変わっていることを確認

検証

ProjectDefine.csに複数の定義が存在する場合のILについて検証する

定義が2つ存在する場合の ProjectDefine
namespace Settings
{
    public class ProjectDefine
    {
        /// <summary>
        /// 書き換えなくてもいい文字列
        /// </summary>
        public string Value2 { get; } = "Hello Japan";

        /// <summary>
        /// 書き換えたい文字列
        /// </summary>
        public string Value { get; } = "Hello World";
    }
}
ProjectDefine.csのIL
ProjectDefine.cs -> IL
.assembly _
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = (
        01 00 08 00 00 00 00 00
    )
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = (
        01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78
        63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = (
        01 00 07 01 00 00 00 00
    )
    .permissionset reqmin = (
        2e 01 80 8a 53 79 73 74 65 6d 2e 53 65 63 75 72
        69 74 79 2e 50 65 72 6d 69 73 73 69 6f 6e 73 2e
        53 65 63 75 72 69 74 79 50 65 72 6d 69 73 73 69
        6f 6e 41 74 74 72 69 62 75 74 65 2c 20 53 79 73
        74 65 6d 2e 52 75 6e 74 69 6d 65 2c 20 56 65 72
        73 69 6f 6e 3d 37 2e 30 2e 30 2e 30 2c 20 43 75
        6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50
        75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 30
        33 66 35 66 37 66 31 31 64 35 30 61 33 61 15 01
        54 02 10 53 6b 69 70 56 65 72 69 66 69 63 61 74
        69 6f 6e 01
    )
    .hash algorithm 0x00008004 // SHA1
    .ver 0:0:0:0
}

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Settings.ProjectDefine
    extends [System.Runtime]System.Object
{
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
    )
    .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (
        01 00 00 00 00
    )
    // Fields
    .field private initonly string '<Value2>k__BackingField'
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )
    .field private initonly string '<Value>k__BackingField'
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = (
        01 00 00 00 00 00 00 00
    )

    // Methods
    .method public hidebysig specialname 
        instance string get_Value2 () cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x20a2
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld string Settings.ProjectDefine::'<Value2>k__BackingField'
        IL_0006: ret
    } // end of method ProjectDefine::get_Value2

    .method public hidebysig specialname 
        instance string get_Value () cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x20aa
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld string Settings.ProjectDefine::'<Value>k__BackingField'
        IL_0006: ret
    } // end of method ProjectDefine::get_Value

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20b2
        // Code size 30 (0x1e)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldstr "Hello Japan"
        IL_0006: stfld string Settings.ProjectDefine::'<Value2>k__BackingField'
        IL_000b: ldarg.0
        IL_000c: ldstr "Hello World"
        IL_0011: stfld string Settings.ProjectDefine::'<Value>k__BackingField'
        IL_0016: ldarg.0
        IL_0017: call instance void [System.Runtime]System.Object::.ctor()
        IL_001c: nop
        IL_001d: ret
    } // end of method ProjectDefine::.ctor

    // Properties
    .property instance string Value2()
    {
        .get instance string Settings.ProjectDefine::get_Value2()
    }
    .property instance string Value()
    {
        .get instance string Settings.ProjectDefine::get_Value()
    }

} // end of class Settings.ProjectDefine

.class private auto ansi sealed beforefieldinit Microsoft.CodeAnalysis.EmbeddedAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method EmbeddedAttribute::.ctor

} // end of class Microsoft.CodeAnalysis.EmbeddedAttribute

.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.NullableAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
        01 00 84 6b 00 00 02 00 54 02 0d 41 6c 6c 6f 77
        4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
        72 69 74 65 64 00
    )
    // Fields
    .field public initonly uint8[] NullableFlags

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            uint8 ''
        ) cil managed 
    {
        // Method begins at RVA 0x2059
        // Code size 24 (0x18)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldc.i4.1
        IL_0009: newarr [System.Runtime]System.Byte
        IL_000e: dup
        IL_000f: ldc.i4.0
        IL_0010: ldarg.1
        IL_0011: stelem.i1
        IL_0012: stfld uint8[] System.Runtime.CompilerServices.NullableAttribute::NullableFlags
        IL_0017: ret
    } // end of method NullableAttribute::.ctor

    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            uint8[] ''
        ) cil managed 
    {
        // Method begins at RVA 0x2072
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldarg.1
        IL_0009: stfld uint8[] System.Runtime.CompilerServices.NullableAttribute::NullableFlags
        IL_000e: ret
    } // end of method NullableAttribute::.ctor

} // end of class System.Runtime.CompilerServices.NullableAttribute

.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.NullableContextAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
        01 00 4c 14 00 00 02 00 54 02 0d 41 6c 6c 6f 77
        4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
        72 69 74 65 64 00
    )
    // Fields
    .field public initonly uint8 Flag

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            uint8 ''
        ) cil managed 
    {
        // Method begins at RVA 0x2082
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldarg.1
        IL_0009: stfld uint8 System.Runtime.CompilerServices.NullableContextAttribute::Flag
        IL_000e: ret
    } // end of method NullableContextAttribute::.ctor

} // end of class System.Runtime.CompilerServices.NullableContextAttribute

.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.RefSafetyRulesAttribute
    extends [System.Runtime]System.Attribute
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = (
        01 00 02 00 00 00 02 00 54 02 0d 41 6c 6c 6f 77
        4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65
        72 69 74 65 64 00
    )
    // Fields
    .field public initonly int32 Version

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            int32 ''
        ) cil managed 
    {
        // Method begins at RVA 0x2092
        // Code size 15 (0xf)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Runtime]System.Attribute::.ctor()
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: ldarg.1
        IL_0009: stfld int32 System.Runtime.CompilerServices.RefSafetyRulesAttribute::Version
        IL_000e: ret
    } // end of method RefSafetyRulesAttribute::.ctor

} // end of class System.Runtime.CompilerServices.RefSafetyRulesAttribute

ldstrが2つ存在するため、上述のPostProcessorではValue, Value2のうち最初に検索されたldstrのみが書き変わる

そこで、stfldを判定に使用する方法を考える

PostProcessorの作成 その2

定数書き換えのPostProcessor
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil.Cil;
using Settings;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;

namespace Constant.Rewriting.CodeGen
{
    public class ConstantRewritingPostProcessor : ILPostProcessor
    {
        public override ILPostProcessor GetInstance() => this;

        public override bool WillProcess(ICompiledAssembly compiledAssembly)
        {
            // Settings Assemblyのみを対象とする
            return compiledAssembly.Name == "Settings";
        }
        
        public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
        {
            if (!WillProcess(compiledAssembly))
            {
                return null;
            }

            using var assembly = ILPostProcessUtility.AssemblyDefinitionFor(compiledAssembly);
            var diagnosticMessageList = new List<DiagnosticMessage>();
            foreach (var type in assembly.MainModule.Types)
            {
                // Settings.ProjectDefine型を探す
                if (type.FullName != "Settings.ProjectDefine")
                {
                    continue;
                }

                // Settings.ProjectDefineのコンストラクタを取得する
                var methodDefinition = type.Methods.FirstOrDefault((method) => method.Name == ".ctor");
                
                if (methodDefinition == null)
                {
                    diagnosticMessageList.Add(new DiagnosticMessage()
                    {
                        MessageData = ".ctorが見つかりませんでした。",
                        DiagnosticType = DiagnosticType.Warning
                    });
                    continue;
                }

                var postProcessor = methodDefinition.Body.GetILProcessor();

                for (int i = 0; i < postProcessor.Body.Instructions.Count; i++)
                {
                    var instruction = postProcessor.Body.Instructions[i];
                    // 命令のオペコードがStfldでない場合はスキップ
                    if (instruction.OpCode != OpCodes.Stfld)
                    {
                        continue;
                    }
                    // 命令のオペランドにValueが含まれていない場合はスキップ
                    if (!instruction.Operand.ToString().Contains($"<{nameof(ProjectDefine.Value)}>k__BackingField"))
                    {
                        continue;
                    }
                    
                    // 含まれている場合、一つ前の命令を取得し、オペコードがLdstrでない場合はスキップ
                    var prevInstruction = postProcessor.Body.Instructions[i - 1];
                    if (prevInstruction.OpCode != OpCodes.Ldstr)
                    {
                        continue;
                    }
                    // 書き換え
                    prevInstruction.Operand = "Hello ILPostProcessor";
                }

                return ILPostProcessUtility.GetResult(assembly, diagnosticMessageList);
            }
            return new ILPostProcessResult(null, diagnosticMessageList);
        }
    }
}

stfldに<Value>k__BackingFieldが含まれているかを判定に使用し、含まれている場合1つ前の命令のオペランドを変更するようにした

結果


書き換えたい箇所のみ書き換えられていることを確認

方針

書き換えたい値にAttributeをつける方針を取ってみる

アトリビュートクラスの用意

Settings
├── ProjectDefine.cs
├── RewritingAttribute.cs
├── Settings.asmdef
└── TemporaryClass.cs

RewritingAttribute.csを追加

書き換えたい箇所指定アトリビュートクラス
using System;

namespace Settings
{
    [AttributeUsage(AttributeTargets.Property)]
    public class RewritingAttribute : System.Attribute
    {
    }
}
アトリビュートをつけたProjectDefine.cs
using System;

namespace Settings
{
    public class ProjectDefine
    {
        /// <summary>
        /// 書き換えなくてもいい文字列
        /// </summary>
        public string Value2 { get; } = "Hello Japan";
        
        /// <summary>
        /// 書き換えたい文字列
        /// </summary>
        [Rewriting]
        public string Value { get; } = "Hello World";
    }
}

アトリビュートをつけたプロパティのみ書き換えられるように修正する

PostProcessorの作成 その3

定数書き換えのPostProcessor
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Settings;
using Unity.CompilationPipeline.Common.Diagnostics;
using Unity.CompilationPipeline.Common.ILPostProcessing;

namespace Constant.Rewriting.CodeGen
{
    public class ConstantRewritingPostProcessor : ILPostProcessor
    {
        public override ILPostProcessor GetInstance() => this;

        public override bool WillProcess(ICompiledAssembly compiledAssembly)
        {
            // Settings Assemblyのみを対象とする
            return compiledAssembly.Name == "Settings";
        }
        
        public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
        {
            if (!WillProcess(compiledAssembly))
            {
                return null;
            }

            using var assembly = ILPostProcessUtility.AssemblyDefinitionFor(compiledAssembly);
            var diagnosticMessageList = new List<DiagnosticMessage>();
            foreach (var type in assembly.MainModule.Types)
            {
                foreach (var property in type.Properties)
                {
                    if (!property.HasCustomAttributes)
                    {
                        continue;
                    }

                    var attribute = property.CustomAttributes.FirstOrDefault(attr => attr.AttributeType.FullName == typeof(RewritingAttribute).FullName);

                    if (attribute == null)
                    {
                        continue;
                    }

                    var constructors = type.GetConstructors();
                    foreach (var method in constructors)
                    {
                        var postProcessor = method.Body.GetILProcessor();
                        var instruction = postProcessor.Body.Instructions.FirstOrDefault(
                            inst => inst.Operand != null && inst.Operand.ToString().Contains($"<{property.Name}>k__BackingField")
                        );
                        
                        if (instruction == null)
                        {
                            continue;
                        }
                        
                        var prevInstruction = instruction.Previous;
                        if (prevInstruction.OpCode != OpCodes.Ldstr)
                        {
                            continue;
                        }
                        // 書き換え
                        prevInstruction.Operand = "Hello ILPostProcessor";
                    }
                    return ILPostProcessUtility.GetResult(assembly, diagnosticMessageList);
                }
            }
            return new ILPostProcessResult(null, diagnosticMessageList);
        }
    }
}

アトリビュートがついているプロパティと同じ名前のBackingFieldがコンストラクタ内に存在するか確認し、存在する場合は1つ前の命令を取得するように変更した
アトリビュートからプロパティ名を取得するようにしたため、ProjectDefineValue以外もアトリビュートをつければ書き換えられるようにした

結果


アトリビュートつけた箇所のみ書き換えられていることを確認

終わりに

本記事ではILPostProcessorで定数を書き換える方法について紹介しました。
今回実装した内容はGithub上で公開しておりますので、併せてご確認ください。
https://github.com/kozuka-hayato-ab/ILPostProcessorTest

以上、「Applibot Advent Calendar 2023」 8日目の記事でした。
明日は@daikidev111さんです!

参考記事

  1. Qiita -【Unity】ILPostProcessorとasmrefを使った外部Packageのメソッド書き換え
  2. Qiita - C#で手軽にILや内部を確認するなら「SharpLab」!
  3. 未確認飛行 C - 小ネタ 隠しメンバー

Discussion