🐳

IUnityLinkerProcessorでCodeStrippingからコードを守る

2023/12/19に公開

Applibot Advent Calender2023の19日目の記事になります。
前回は @16cho さんの GodotでMagicOnionやってみる という記事でした。

はじめに

本記事では IUnityLinkerProcessor を用いて動的に link.xml を生成する方法をお伝えします。
ManagedCodeStrippingの対象にして欲しくないコードを保護する方法について、なぜそのような実装が必要になったかという経緯や具体的な事例を踏まえてご紹介します。

Managed Code Stripping とは

まずはじめに、ManagedCodeStrippingについて触れておきます。この機能を用いると、ビルド時にアプリケーション側で利用していないコードを削除し、そのバイナリサイズを削除します。
参考 : ManagedCodeStrippingについて

使用していないアプリケーション側のC#コードはもちろんのこと、UnityEngineやcorelibなどのエンジン側で用意されているコードも削除してくれるため、適切に設定することでアプリケーションのバイナリサイズの削減や、ゲームの起動時間の短縮も期待できます。

基本的には、MonoBehaviorやScriptableObjectなどのルート型を継承するクラスから各クラスやそのメンバへの参照を辿り、到達できなかったクラスやメンバが削除対象となります。

この際、リフレクション経由でのみアクセスしているクラスやメンバーは、コードを使用している扱いにならないため、そのままではManagedCodeStrippingによるコード削除の対象になります。

これを回避するためのコード利用を開発者側で明記するための仕組みとして link.xmlPreserve属性があります。

リフレクション経由などのアクセスで削除されるコードに対してはこれらを適切に設定することで、本来必要なコードが削除されてしまう問題を回避できます。

ManagedCodeStrippingについての詳しい挙動に関してはこちらの記事で紹介されています。
Managed Code Strippingの挙動の検証と2020 LTSで利用できる新しいアノテーション属性の紹介

プロジェクトで発生した問題

私が所属しているプロジェクトでは途中からManagedStrippingLevelを High にしました。
すると、Zenjectを用いて依存注入しているクラスが軒並み消されていました。これはZenjectがリフレクション経由でクラスのコンストラクタを呼び出しているからです。クラス一つ一つに Preserve属性 をつけるとなると多くソースを触る + 抜けが出る、といったデメリットが生じます。そこで、Zenjectのコンテナにバインドしているクラスは自動的にStrippingの対象から外すようにしたいと考えました。

IUnityLinkerProcessorを使うまでの経緯

link.xmlへの書き込みにはアセンブリ名とクラス名が必要です。
Zenjectのコンテナにバインドしている部分には共通点があり、

Container.Bind<クラス名>();
Container.Bind<インターフェース>().To<クラス名>();

というルールが存在していました。(厳密にはルールはこれだけではありません。)
単純に.csファイルから正規表現などを使って探索すると、本体の対象ではないクラスが引っかかる可能性があります。(Bind<>()という構文が他のクラスでも使われる可能性が高いため。)
そこで、特定のクラスのメソッドであることを前提に探索する必要があります。コンパイル後のアセンブリではクラス名 + メソッド名を取得することができます。今回はこれを解析することでZenjectのクラスのBind<>()のみを対象にとることを目指しました。

IUnityLinkerProcessor のコールバックが呼び出されるUnityLinkerの時点ではコンパイルが終わっており、dllが生成されています。これは構文解析が終了した状態なので、ZenjectクラスのBind<>()といった探索が可能です。
dll情報を取得し、プロジェクトコード中のBindメソッドを呼び出している箇所をILのInstructionレベルで特定します。メソッド呼び出しのオペランドから、実際にバインドしているクラスをFQDN(Fully Qualifed Domain Name)が特定できるので、それを収集してlink.xmlを生成します。収集にはDLLバイナリを読み込んでそこに含まれる全情報を見ることができるMono.cecilを使いました。

  • link.xmlの適用を自動化したい
  • 構文解析が終わった状態のソースの取得ができる

以上の理由から IUnityLinkerProcessor を使用することを決めました。

IUnityLinkerProcessorについて

前提が長くなりました。ここから本編です。
IUnityLinkerProcessorは、UnityLinkerの実行時にコールバックを受け取ることができ、その内部でlink.xmlを生成、ファイルへのパスを返すことでlink.xmlを適用することが可能です。

  • int CallbackOrder
    基本的にはビルド時に呼ばれるコールバックの優先度を決めるフィールドです。
    特殊な場合を除き、ここは基本的に 0で問題ありません。

  • string GenerateAdditionaLinkXmlFile(BuildReport report, UnityLinkerBuildPipelineData data)
    link.xmlを生成してそのパスを返すことでlink.xmlを適用することが可能です。

具体実装について

先ほど紹介した Zenjectで依存注入するクラスが消えてしまう件の対策 を軸に紹介します。

  1. 全体設計
  2. link.xmlファイルパスの作成
  3. Mono.cecilを使った、保護対象となるクラスの収集
  4. XmlDocument / XmlNodeを使った、link.xmlの生成
  5. パスの返却

まずは全体的な設計についてです。

全体設計 : UnityLinkerProcessorBaseについて

自分が所属しているプロジェクトでは、Zenjectの他にもリフレクション経由でコンストラクタ呼び出しを行なっている部分が複数ありました。
収集対象毎に共通処理を書くのは非効率なので、まずは、GenerateAdditionalLinkXmlFileの実装や、保護対象を収集するメソッドを定義したクラスUnityLinkerProcessorBaseを作成しました。

public abstract class UnityLinkerProcessorBase : IUnityLinkerProcessor
{
    // コールバックが呼ばれる優先度 特に指定しないので 0
    public int callbackOrder => 0;

    // link.xmlのユニーク名
    protected abstract string LinkXmlName { get; }

    // 保護対象の収集を行うメソッド
    protected abstract void CollectEntries(LinkXmlEntryCollector collector, Assembly[] assemblies);

    // link.xmlのパスを返すコールバックメソッド
    public string GenerateAdditionalLinkXmlFile(BuildReport report, UnityLinkerBuildPipelineData data)
    {
        // パスの生成
        string path = System.IO.Path.Combine(data.inputDirectory, $"{LinkXmlName}.link.xml");
        
        // link.xmlの生成を行うインスタンス
        LinkXmlGenerator.LinkXmlGenerator generator = new LinkXmlGenerator.LinkXmlGenerator();

        // 保護対象の収集
        CollectEntries(new LinkXmlEntryCollector(generator), AppDomain.CurrentDomain.GetAssemblies());

        // link.xmlの生成
        generator.Generate(path);

        return path;
    }
}

以下、クラスの簡単な説明です。

  • UnityLinkerProcessorBase
    IUnityLinkerProcessorを利用する際の共通処理をまとめたクラス
  • CollectEntries
    保護対象の収集を行う抽象メソッド
  • LinkXmlGenerator
    収集した情報を元にlink.xmlの生成を行う
  • LinkXmlEntryCollector
    LinkXmlGeneratorに保護対象の情報を追加するメソッドを提供

このクラスをベースに保護対象の収集を行うクラスを実装します。
Zenjectで依存注入するクラスを取得するという目的のDILinkerProcessorです。

public class DILinkerProcessor : UnityLinkerProcessorBase
{
	protected override string LinkXmlName => "di";
	
	protected override void CollectEntries(LinkXmlEntryCollector collector, Assembly[] assemblies)
	{
		// 保護対象の収集を行います。
		// 詳しい中身は後で紹介します。
	}
	
	private static bool TryGetType(string fullName, AssemblyCollection assemblyCollection, out Type type)
	{
		// クラス名から`Type`に変換を行います。
		// 詳しい中身は後で紹介します。
	}
	
	private static bool TryParseInjectedTypeName(Instraction instraction, AssemblyCollection assemblyCollection, out Type type)
	{
		// Instractionから`Type`に変換を行います。
		// 詳しい中身は後で紹介します。
	}
}

TryGetTypeTryParseInjectedTypeNameの引数の型であるAssemblyCollectionではAssemblyDefinitionを安全に使うための仕組みを入れています。
AssemblyDefinitionはusingを使用した際は自動的にDisposeが呼び出されるのですが、今回の場合は複数クラスにまたがって使用されることに加え、リストによって管理したいため、明示的にDisposeを呼び出すクラスを作成し、使用しています。
以下にAssemblyCollectionを載せます。

class AssemblyCollection : IDisposable
{
    public AssemblyDefinition[] AssemblyDefinitions { get; }
    public AssemblyCollection(Assembly[] assemblies)
    {
        List<AssemblyDefinition> targetAssemblyDefinition = new List<AssemblyDefinition>();
        foreach (Assembly assembly in assemblies)
        {
            // dynamic アセンブリの場合はReadAssemblyができないためスキップ
            if (assembly.IsDynamic)
            {
                continue;
            }
            
            AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(assembly.Location);
            targetAssemblyDefinition.Add(assemblyDefinition);
        }

        AssemblyDefinitions = targetAssemblyDefinition.ToArray();
    }

    public void Dispose()
    {
        if (AssemblyDefinitions != null)
        {
            foreach (AssemblyDefinition assemblyDefinition in AssemblyDefinitions)
            {
                assemblyDefinition.Dispose();
            }
        }
    }
}

link.xmlファイルの作成

link.xmlファイルを作成するためのパスを生成します。
ファイルの生成は LinkXmlGeneratorの内部のxmlDocument.Save(path)で行っています。

パスを生成するにあたって注意点がいくつかあります。

  • data.inputDirectory
    生成するlink.xmlは、GenerateAdditionalLinkXmlFileメソッドメソッドの引数として渡されてくる datainputDirectory 内部に配置する必要があります。
  • ファイル名
    複数のファイルに分けてlink.xmlを生成する場合、link.xmlの名前が被ると最後に生成したもののみ適用されてしまいます。
    本来、Assets/ にlink.xmlを配置する際は命名を link.xml に固定する必要があります。しかし、IUnityLinkerProcessor を使う場合はファイルパスを直接ビルドパイプラインに渡しているので、命名の統一は必要ありません。ユニークな名前を適用するようにしましょう。
    私が取った手法としては、GenerateAdditionalLinkXmlFileを実装するabstractクラスを作り、abstractな string LinkXmlName フィールドを定義することで、継承先のクラスでlink.xmlにつける名前を決定できるようにしました。
string path = System.IO.Path.Combine(data.inputDirectory, $"{LinkXmlName}.link.xml");

Mono.cecilを使った、保護対象となるクラスの収集

次に保護対象のクラスの情報を収集します。

  • LinkXmlEntryCollectorについて
    クラスの情報を収集する CollectEntries メソッドがLinkXmlGeneratorに対して情報を渡すためのクラスです。
    LinkXmlGeneratorのアクセスを制限し、クラス情報の収集のみを行えるようにしています。
public class LinkXmlEntryCollector
{
    private readonly LinkXmlGenerator _generator;

    public LinkXmlEntryCollector(LinkXmlGenerator generator)
    {
        _generator = generator;
    }

    public void AddType(Type type) => _generator.AddType(type);
    public void AddMethod(MethodInfo method) => _generator.AddMethod(method);
    public void AddAssembly(Assembly assembly) => _generator.AddAssembly(assembly);
}

収集のロジック

実際の収集は AppDomain.CurrentDomain.GetAssemblies() を起点として行います。
この時点で構文解析が終了した状態のコードを取得出来ます。
先ほど示したDILinkerProcessorの中身を実装していきます。
まずはCollectEntriesメソッドです。

protected override void CollectEntries(LinkXmlEntryCollector collector, Assembly[] assemblies)
{
    // AssemblyDefinitionのリストに変換
    using AssemblyCollection assemblyCollection = new AssemblyCollection(assemblies);

    foreach (AssemblyDefinition assemblyDefinition in assemblyCollection.AssemblyDefinitions)
    {
        foreach (TypeDefinition typeDefinition in assemblyDefinition.MainModule.GetTypes())
        {
            foreach (MethodDefinition methodDefinition in typeDefinition.GetMethods())
            {
                if (!methodDefinition.HasBody)
                {
                    continue;
                }
                foreach (Instruction instruction in methodDefinition.Body.Instructions)
                {
                    if (TryParseInjectedTypeName(instruction, assemblyCollection, out Type type))
                    {
                        if (type == null)
                        {
                            continue;
                        }            
                        collector.AddType(type);
                    }
                }
            }
        }
    }    
}

for文でmethodDefinitionBodyInstructionを取得し、それを元にType型をLinkXmlEntryCollectorAddType()で対象に加えています。

以下に関数内で使用してるTryParseInjectedTypeName及び、TryGetTypeを示します。
このクラスはInstractionから対象となるTypeを探索しています。
Instractionからメソッド呼び出しの命令を取得し、その中のオペランドに対して、正規表現を用いて文字列の特定の部分を抽出しています。

private static bool TryParseInjectedTypeName(Instruction instruction, AssemblyCollection assemblyCollection, out Type type)
{
    type = null;
    if (instruction.OpCode != OpCodes.Callvirt)
    {
        return false;
    }

    string operand = instruction.ToString();
    foreach ((Regex pattern, int groupIndex) in _ZENJECT_PATTERNS)
    {
        Match match = pattern.Match(operand);
        if (match.Success == false)
        {
            continue;
        }

        if (TryGetType(match.Groups[groupIndex].Value, assemblyCollection, out Type resultType)) 
        {
            type = resultType;
            return true;
        }
    }

    return false;
}

private static bool TryGetType(string fullName, AssemblyCollection assemblyCollection, out Type type )
{
    type = null;

    foreach (AssemblyDefinition assembly in assemblyCollection.AssemblyDefinitions)
    {
        foreach (TypeDefinition typeDefinition in assembly.MainModule.GetTypes())
        {
            if (typeDefinition.FullName == fullName)
            {
                // Assembly-Csharp外は型取得のためにアセンブリ修飾名での検索が必須のため文字列を生成
                string assemblyQualifiedName = $"{typeDefinition}, {assembly.FullName}";
                type = Type.GetType(assemblyQualifiedName);
                return true;
            }
        }
    }

    return false;
}

オペランドに対して行った正規表現のパターンが_ZENJECT_PATTERNSです。
Container.Bind<対象のクラス>() Container.Bind<対象のクラスのインターフェース>().To<対象のクラス>() となっているものを対象に検索しました。

readonly (Regex Pattern, int IndexGroup)[] _ZENJECT_PATTERNS = new[]
{
    // callvirt Zenject.ConcreteIdBinderGeneric`1<!!0> Zenject.DiContainer::Bind<TargetType>()
    (new Regex(@"Zenject.ConcreteIdBinderGeneric`1<!!0> Zenject.DiContainer::Bind<(.*?)>" + Regex.Escape("()"), RegexOptions.Compiled),
        1),
    // callvirt Zenject.FromBinderGeneric`1<!!0> Zenject.ConcreteBinderGeneric`1<FromType>::To<TargetType>()
    (new Regex(@"Zenject.FromBinderGeneric`1<!!0> Zenject.ConcreteBinderGeneric`1<(.*?)>::To<(.*?)>" + Regex.Escape("()"), RegexOptions.Compiled),
        2),
};

XmlDocument / XmlNodeを使った、link.xmlの生成

対象のクラスやメソッドの情報を取得することができたら、それを元にlink.xmlを生成します。
以下がlink.xmlの生成を担っているLinkXmlGeneratorです。
クラス全体を紹介します。

public class LinkXmlGenerator
{
    private readonly List<Type> _types = new List<Type>();
    private readonly List<MethodInfo> _methods = new List<MethodInfo>();
    private readonly List<Assembly> _assemblies = new List<Assembly>();
	
    // 保護対象を追加していくメソッド
    // LinkXmlEntryCollectorからのみ呼ばれることを想定
    public void AddType(Type type)
    {
        _types.Add(type);
    }

    public void AddMethod(MethodInfo method)
    {
        _methods.Add(method);
    }

    public void AddAssembly(Assembly assembly)
    {
        _assemblies.Add(assembly);
    }
    
    // UnityLinkerProcessorBaseから呼び出される生成関数
    public void Generate(string path)
    {
        // 情報を合わせる
        IReadOnlyList<AssemblyNode> assemblyNodes = MargeTargetInfo();

        // xmlの生成
        XmlDocument xmlDocument = new XmlDocument();
        GenerateXml(xmlDocument, assemblyNodes);

        // xmlの出力
        xmlDocument.Save(path);
    }
    
    // 収集した情報を元にlink.xmlを生成
    private void GenerateXml(XmlDocument xmlDocument, IReadOnlyList<AssemblyNode> assemblyNodes)
    {
        XmlNode linkerNode = xmlDocument.AppendChild(xmlDocument.CreateElement("linker"));

        foreach (AssemblyNode assemblyNode in assemblyNodes)
        {
            XmlNode xmlAssemblyNode = linkerNode.AppendChild(xmlDocument.CreateElement("assembly"));
            if (xmlAssemblyNode.Attributes == null)
            {
                continue;
            }

            XmlAttribute fullNameAttribute = xmlDocument.CreateAttribute("fullname");
            fullNameAttribute.Value = assemblyNode.Value.FullName;
            xmlAssemblyNode.Attributes.Append(fullNameAttribute);

            if (assemblyNode.IsPreserve)
            {
                // preserve="all"を追加
                XmlAttribute preserveAttribute = xmlDocument.CreateAttribute("preserve");
                preserveAttribute.Value = "all";
                xmlAssemblyNode.Attributes.Append(preserveAttribute);
            }
            else
            {
                foreach (TypeNode typeNode in assemblyNode.Types)
                {
                    XmlNode xmlTypeNode = xmlAssemblyNode.AppendChild(xmlDocument.CreateElement("type"));
                    if (xmlTypeNode.Attributes == null)
                    {
                        return;
                    }

                    XmlAttribute typeFullNameAttribute = xmlDocument.CreateAttribute("fullname");
                    typeFullNameAttribute.Value = typeNode.Value.FullName;
                    xmlTypeNode.Attributes.Append(typeFullNameAttribute);

                    if (typeNode.IsPreserve)
                    {
                        // preserve="all"を追加
                        XmlAttribute preserveAttribute = xmlDocument.CreateAttribute("preserve");
                        preserveAttribute.Value = "all";
                        xmlTypeNode.Attributes.Append(preserveAttribute);
                    }
                    else
                    {
                        // method name="method"を追加
                        foreach (MethodInfo method in typeNode.Methods)
                        {
                            XmlNode xmlMethodNode = xmlTypeNode.AppendChild(xmlDocument.CreateElement("method"));
                            if (xmlMethodNode.Attributes == null)
                            {
                                return;
                            }

                            XmlAttribute methodNameAttribute = xmlDocument.CreateAttribute("name");
                            methodNameAttribute.Value = method.Name;
                            xmlMethodNode.Attributes.Append(methodNameAttribute);
                        }
                    }
                }
            }
        }
    }

    // collectorで集めた情報をlink.xmlに出力しやすいように独自のNode型に変換
    private IReadOnlyList<AssemblyNode> MargeTargetInfo()
    {
        List<AssemblyNode> result = new List<AssemblyNode>();

        //Distinct処理
        List<Assembly> assemblyList = _assemblies.Distinct().ToList();
        List<Type> typeList = _types.Distinct().ToList();
        List<MethodInfo> methodList = _methods.Distinct().ToList();

        // 情報集め
        // assembly
        foreach (Assembly assembly in assemblyList)
        {
            if (result.Exists(x => x.Value.FullName == assembly.FullName) == false)
            {
                result.Add(new AssemblyNode(assembly, true));
            }
        }

        // type
        foreach (Type type in typeList)
        {
            AssemblyNode targetAssemblyNode = result.Find(x => x.Value.FullName == type.Assembly.FullName);
            if (targetAssemblyNode == null)
            {
                // 新規AssemblyNodeを作成
                AssemblyNode assemblyNode = new AssemblyNode(type.Assembly, false);
                result.Add(assemblyNode);
                TypeNode typeNode = new TypeNode(type, true);
                assemblyNode.AddTypeNode(typeNode);
            }
            else
            {
                // 既存のAssemblyNodeに追加
                targetAssemblyNode.AddTypeNode(new TypeNode(type, true));
            }
        }

        // method
        foreach (MethodInfo method in methodList)
        {
            AssemblyNode assemblyNode =
                result.Find(x => x.Value.FullName == method.DeclaringType.Assembly.FullName);
            if (assemblyNode == null)
            {
                // 新規AssemblyNodeを作成
                assemblyNode = new AssemblyNode(method.DeclaringType.Assembly, false);
                result.Add(assemblyNode);
            }

            // Typeが存在するか探索
            if (assemblyNode.TryGetTypeNodeByTypeName(out TypeNode typeNode, method.DeclaringType.FullName))
            {
                // 存在したTypeにmethodを追加
                typeNode.AddMethodInfo(method);
            }
            else
            {
                // Typeを作成して追加
                typeNode = new TypeNode(method.DeclaringType, false);
                assemblyNode.AddTypeNode(typeNode);
                typeNode.AddMethodInfo(method);
            }
        }

        return result;
    }

    // アセンブリの情報を含むノード
    private class AssemblyNode
    {
        private readonly List<TypeNode> _types = new List<TypeNode>();

        public IReadOnlyList<TypeNode> Types => _types;

        public Assembly Value { get; }

        /// <summary>
        /// アセンブリ空間をpreserveするか否か
        /// </summary>
        public bool IsPreserve { get; }

        public AssemblyNode(Assembly assembly, bool isPreserve)
        {
            Value = assembly;
            IsPreserve = isPreserve;
        }

        public void AddTypeNode(TypeNode typeNode)
        {
            _types.Add(typeNode);
        }

        public bool TryGetTypeNodeByTypeName(out TypeNode typeNode, string typeName)
        {
            typeNode = _types.Find(x => x.Value.FullName == typeName);
            return typeNode != null;
        }
    }

    // Typeの情報を含むノード
    private class TypeNode
    {
        private List<MethodInfo> _methods = new List<MethodInfo>();

        public IReadOnlyList<MethodInfo> Methods => _methods;

        public Type Value { get; }

        /// <summary>
        /// そのTypeをPreserveするか否か
        /// </summary>
        public bool IsPreserve { get; }

        public TypeNode(Type type, bool isPreserve)
        {
            Value = type;
            IsPreserve = isPreserve;
        }

        public void AddMethodInfo(MethodInfo method)
        {
            _methods.Add(method);
        }
    }

    public interface IEntry
    {
    }
}

生成するにあたって注意しなければならない点として、link.xmlの構成を守る、と言うものがあります。
Unity公式のManagedCodeStrippingについてのドキュメントではlink.xmlの様々な書き方が掲載されていますが、今回はアセンブリ、クラス、メソッドの保護とpreserve属性についてのみ記載します。

  • linker 属性
    link.xmlの書き方としてlinker 属性の下に保護対象を宣言していくというルールがあります。
XmlNode linkerNode = xmlDocument.AppendChild(xmlDocument.CreateElement("linker"));
  • preserve 属性
    preserve 属性はそのアセンブリやクラスに含まれているすべての要素をStripping対象から外します。
    例えば、特定のアセンブリの全てを保護したい場合は、
XmlNode xmlAssemblyNode = linkerNode.AppendChild(xmlDocument.CreateElement("assembly"));

AssemlbyDefinition assembly = 対象のアセンブリ // ここではTargetAsseblyというアセンブリとします。
// アセンブリを保護する場合はアセンブリ名だけではなく、`fullname` で指定する必要がある。
XmlAttribute fullNameAttribute = xmlDocument.CreateAttribute("fullname");
fullNameAttribute.Value = assembly.FullName;
xmlAssemblyNode.Attribute.Append(fullNameAttribute);

// preserve属性を追加
XmlAttribute preserveAttribute = xmlDocument.CreateAttribute("preserve");
preserveAttribute.Value = "all";
xmlAssemblyNode.Attribute.Append(preserveAttribute);

このコードでは以下のようなlink.xmlが生成されます。

<linker>
    <assemlby fullname="TargetAssembly" preserve="all">
</linker>

もちろんクラスを指定することもできます。
その場合はCreateElement("assembly")の部分を CreateElement("type")に置き換えることで実現できます。

  • クラス、メソッドの保護
    クラスやメソッドを単体で保護する場合は、親となるアセンブリやクラスのノードの下に繋いていく必要があります。
    ここでは例としてTargetMethodを保護する部分を紹介します。
XmlNode xmlTypeNode = xmlAssemblyNode.AppendChild(xmlDocument.CreateElement("type"));

XmlAttribute typeFullNameAttribute = xmlDocument.CreateAttribute("fullname");
typeFullNameAttribute.Value = type.FullName;    //対象となるメソッドが含まれているTypeDefinition
xmlTypeNode.Attributes.Append(typeFullNameAttribute);

// xmlTypeNodeの子要素としてElementを作成
XmlNode xmlMethodNode = xmlTypeNode.AppendChild(xmlDocument.CreateElement("method"));

XmlAttribute methodNameAttribute = xmlDocument.CreateAttribute("name");
methodNameAttribute.Value = TargetMethod.Name;
xmlMethodNode.Attributes.Append(methodNameAttribute);

パスの返却

return pathで生成したlink.xmlのパスを返却したら適用完了です。

まとめ

今回はlink.xmlの自動生成の解決策の一つとして、IUnityLinkerProcessorを使用したlink.xmlの生成を行いました。
この記事で紹介した手法が何かの参考になれば幸いです。

以上Applibot Advent Calender2023の19日目の記事でした!

明日は @ref3000 さんです!

Discussion