Help us understand the problem. What is going on with this article?

【Unity, C#】internalな型やメンバにアクセスするには、多分これが一番早いと思います

この記事は【unityプロ技】 Advent Calendar 2019の13日目の記事です。
この記事におけるソースコードは、全てPublic Domainです。

12/13 15:00 訂正あり
12/26 続編でました

TL;DR

  • C#にはIgnoresAccessChecksToAttributeっていう隠された属性があるよ。ググってもほとんど情報出てこないよ。
  • この属性を使えば、任意の外部アセンブリのinternalの型やメンバに対して自由にアクセスできるよ。ヤバいよ。
  • InternalsVisibleToAttributeAssemblyDefinitionFileと組み合わせることで、インテリセンスやブレークポイントも効くよ。リフレクションより格段に便利だよ。

おさらい:internalアクセス修飾子

C#にはアクセス修飾子と呼ばれる、型やメンバのアクセスを制限する仕組みがあります。
例えばクラスAからクラスBのprivateメソッドを呼ぼうとすると、コンパイルの時点で怒られますし、VisualStudioやVisualStudio Code、Rider等といったIDEのインテリセンスには、候補すら出てきません。

error CS0122: 'ApplicationTitleDescriptor' is inaccessible due to its protection level
error CS0117: 'EditorApplication' does not contain a definition for 'UpdateMainWindowTitle'
...

今回取り上げるinternalアクセスレベルは同一アセンブリ(=同じdll)内からのみアクセスできるアクセスレベルです。
このアクセスレベルを持つ型やメンバは、異なるアセンブリ(=別のdll)からは基本的にはアクセスできません。

internalな型やメンバにアクセスするモチベーション

internalな型やメンバにアクセスする必要があるの?
公開されている(publicな)APIだけで十分では?
苦労してinternalな型やメンバにアクセスして、何がうれしいの?

internalな型やメンバは、ライブラリ製作者がライブラリ利用者に対して隠蔽したい項目であり、APIやドキュメントは一般的に非公開です。
例えば、Unityに何らかのエディタ拡張を加えたい場合は、以下のようなUnityが公開しているAPI(publicなAPI)の利用が基本です。

御存知の通り、公開されているAPIだけでも「出来ないことなんて何も無いんじゃないの?」と思えるほど大量の項目が見つかります。
しかし、公開されていない(=非publicな)APIに目を向けると、さらに多くの事が実現可能なことに気づきます

例えば、【Unity】UIElements で Game ビューにテキストやボタンを追加するエディタ拡張のサンプル(@baba_s様)では、リフレクションを利用して、Unityが公開していないGameViewクラスへアクセスし、GameビューにオリジナルのGUIを追加しています。

var assembly = typeof( EditorWindow ).Assembly;
var type     = assembly.GetType( "UnityEditor.GameView" );
var gameview = EditorWindow.GetWindow( type );
...

このように、非publicなUnityの世界を知ることは、新しい気づきや便利な拡張機能のヒントになります。
そして、あなたが発見した気づきや便利な拡張機能は、ほかの誰かのヒントになるかもしれません。


ただし、大事なことなので二回言いますが、internalな型やメンバは、ライブラリ製作者がライブラリ利用者に対して隠蔽したい項目であり、APIやドキュメントは一般的に非公開です。
ググって得られる情報は限定的ですし、無慈悲にもAPIは通告なく変更されます。
IL化や難読化されていることは日常茶飯事で、誤って使えばフリーズする可能性すらあります。
ライブラリのライセンスによっては、明確に逆コンパイルやリバースエンジニアリングが禁止されています。

ありがたいことに、Unityは公式でGitHubにソースコードを公開しています。internal要素も探し放題です。太っ腹だね!

さあ、実現したいことをキーワードにソースコードを検索し、それっぽい名前の APIを見つけ、注意深くソースコードを読み、拡張機能の幅をくれぐれも自己責任で広げましょう!

開発環境

本記事では以下の環境でデモプロジェクトを開発しました。

  • macOS Mojave 10.14.6
  • Unity 2019.2.15f1
  • VisualStudio For Mac 8.3.8
  • DotNet Core 3.0.100

デモプロジェクトは以下のリポジトリで公開しています。
https://github.com/mob-sakai/MainWindowTitleModifierForUnity

今回のデモでやること

今回は、Unityエディタのタイトルバーのテキストを変更してみたいと思います。
タイトルバーに現在のブランチ名やゲームサーバの接続先環境名(sand/stag/prod等)を表示できると素敵ですよね。

ソースコードを見たところ、Unity2019.2から追加されたinternalな型とメンバを使えば簡単に実現できそうです。
具体的には、EditorApplication.updateMainWindowTitleイベントに新しいコールバックを追加し、EditorApplication.UpdateMainWindowTitle()を呼び出せば、タイトルバーのテキストを変更できそうです:

// こんな感じで実装すれば実現できそう
Action<ApplicationTitleDescriptor> cb = x => x.title = "なにかしらのエモいタイトル";
EditorApplication.updateMainWindowTitle += cb;
EditorApplication.UpdateMainWindowTitle();
EditorApplication.updateMainWindowTitle -= cb;

updateMainWindowTitleUpdateMainWindowTitleApplicationTitleDescriptorは全てinternalな型とメンバであり、このままでは当然コンパイルエラーになります。

error CS0122: 'ApplicationTitleDescriptor' is inaccessible due to its protection level
error CS0117: 'EditorApplication' does not contain a definition for 'UpdateMainWindowTitle'
...

なんとかしてアクセスしてみましょう!

【方法1】Reflection

publicでない型やメンバにアクセスするための方法として、まず思いつくのはReflection(リフレクション)です。
リフレクションにより、文字列を使ってpublicでない型やメンバにも動的にアクセスできます

リフレクションの利点は、internalな型やメンバだけでなく、privateな型やメンバにもアクセスできることです。
また、有名であるが故にググれば情報がたくさんあり、リフレクションだからこそできる事も多くあります
C#リフレクションTIPS 55連発(@gushwell様)という一読すべき記事もあります。

リフレクションを使ってinternalアクセスしてみる

実際にやりたいことは4行で済む内容でしたが、リフレクションで表現するとどうなるでしょうか?

using System;
using System.Linq;
using UnityEditor;
using System.Reflection;

namespace MainWindowTitleModifier
{
    public class Solution1_Reflection
    {
        [MenuItem("MainWindowTitleModifier/Solution1_Reflection", priority = 1)]
        static void Update()
        {
            // ApplicationTitleDescriptorのTypeを取得.
            Type tEditorApplication = typeof(EditorApplication);
            Type tApplicationTitleDescriptor = tEditorApplication.Assembly.GetTypes()
                .First(x => x.FullName == "UnityEditor.ApplicationTitleDescriptor");

            // 関係するイベントとメソッドのInfoを取得.
            EventInfo eiUpdateMainWindowTitle = tEditorApplication.GetEvent("updateMainWindowTitle", BindingFlags.Static | BindingFlags.NonPublic);
            MethodInfo miUpdateMainWindowTitle = tEditorApplication.GetMethod("UpdateMainWindowTitle", BindingFlags.Static | BindingFlags.NonPublic);

            // Action<object>をAction<ApplicationTitleDescriptor>に変換.
            Type delegateType = typeof(Action<>).MakeGenericType(tApplicationTitleDescriptor);
            MethodInfo methodInfo = ((Action<object>)UpdateMainWindowTitle).Method;
            Delegate del = Delegate.CreateDelegate(delegateType, null, methodInfo);

            // UpdateMainWindowTitleを呼び出す前後にイベントの追加/削除.
            eiUpdateMainWindowTitle.GetAddMethod(true).Invoke(null, new object[] { del });
            miUpdateMainWindowTitle.Invoke(null, new object[0]);
            eiUpdateMainWindowTitle.GetRemoveMethod(true).Invoke(null, new object[] { del });
        }

        static void UpdateMainWindowTitle(object desc)
        {
            // UnityEditor.ApplicationTitleDescriptor.title = "Solution1_Reflection"; と同様
            typeof(EditorApplication).Assembly.GetTypes()
                .First(x => x.FullName == "UnityEditor.ApplicationTitleDescriptor")
                .GetField("title", BindingFlags.Instance | BindingFlags.Public)
                .SetValue(desc, "Solution1_Reflection");
        }
    }
}

うーん、つらみ。。。
やりたいことは単純なはずなのに、どうしてこうなった!って感じですね。

ともあれ、MainWindowTitleModifier > Solution1_Reflectionを実行すればタイトルバーのテキストを変えられるようになりました。

リフレクションの欠点

リフレクションの欠点は生産性が低いことと、実行速度が遅いことです。
やりたいことに対してやるべき準備が多く、IDEによるインテリセンスも効かないため、生産性はお世辞にも良くありません。
なぜリフレクションは遅いのか(POSTD様)によると、静的な呼び出しと比べて2〜3桁オーダーで実行速度が遅いようです。

単純なメソッド呼び出しやsetter/getterについて、リフレクションを使う分には悩むことも少ないでしょう。
ジェネリック、コンストラクタ、in/out/ref/paramsキーワード、関数のオーバーロード、非publicな型を含むAction<T>Func<T>、event等、要素が複雑になると、だんだんと長く、ツラくなってきます。めげずに頑張ってググりましょう。

おまけ:リフレクションを効率化して使う

  • TypeMemberInfoのインスタンスをDictionaryでキャッシュする
    • 一度生成したTypeMemberInfoのインスタンスを使いまわすことで、オーバーヘッドを回避します
  • FindMethod、FindProperty等のメソッドをラップする
    • Public/NonPublic、Instance/Staticを指定しなくても適切なMemberInfoを返すようなメソッドを作り、可読性を改善します
  • アセンブリからではなくstringから型を見つける
    • Type.GetType("UnityEditor.ApplicationTitleDescriptor, UnityEditor")のように、クラス名フルパス, アセンブリ名という文字列でも型を取得できます
    • このときの文字列はTypeインスタンスを使って、type.AssemblyQualifiedNameで取得できます
  • デリゲート化、式木、IL Emit

【方法2】InternalsVisibleToAttribute

InternalsVisibleToAttributeは、特定のアセンブリに対して、自身のinternalアクセスを許可させる属性です。
アセンブリに対する単体テストや、機能拡張に使うことが多く、そういったアセンブリはフレンドアセンブリと呼ばれます。
この属性の強力なところは、internalな要素をそのまま記述でき、VisualStudioやVisualStudio Code、Rider等のIDEでインテリセンスやブレークポイントが有効になることです。
テストコードを書くに当たり、IDEのサポートが得られるのはありがたいですね!

さて、InternalsVisibleToAttributeは、UnityEngine.dllやUnityEditor.dllといったUnity公式のライブラリにも存在します
例えばUnityEditor.dllのAssemblyInfo.csには次のように定義されています。

[assembly: InternalsVisibleTo("Unity.LiveNotes")]
[assembly: InternalsVisibleTo("Unity.Burst")]
[assembly: InternalsVisibleTo("Unity.Burst.Editor")]
[assembly: InternalsVisibleTo("Unity.Cloud.Collaborate.Editor")]
[assembly: InternalsVisibleTo("Unity.CollabProxy.Editor")]
[assembly: InternalsVisibleTo("Unity.CollabProxy.EditorTests")]
[assembly: InternalsVisibleTo("Unity.CollabProxy.UI")]
[assembly: InternalsVisibleTo("Unity.CollabProxy.UI.Tests")]
[assembly: InternalsVisibleTo("Unity.CollabProxy.Client")]
[assembly: InternalsVisibleTo("Unity.CollabProxy.Client.Tests")]
[assembly: InternalsVisibleTo("UnityEditor.Advertisements")]
[assembly: InternalsVisibleTo("Unity.PackageManager")]
...
//(以下、たくさんのInternalsVisibleToが羅列されている)
[assembly: InternalsVisibleTo("Unity.InternalAPIEditorBridgeDev.001")]
[assembly: InternalsVisibleTo("Unity.InternalAPIEditorBridgeDev.002")]
[assembly: InternalsVisibleTo("Unity.InternalAPIEditorBridgeDev.003")]
...

これらの名前を持つアセンブリは、UnityEditor.dllのフレンドアセンブリです。
よって、上記の名前を持つアセンブリを生成できれば、UnityEditor.dllに対してinternalアクセスできますすっごーい!

Unityでアセンブリを生成するための仕組みといえば、AssemblyDefinitionFileです。
AssemblyDefinitionFileについては@toRisouP様がUnity Assembly Definition 完全に理解したにて詳しく解説されております。(こちらも【unityプロ技】 Advent Calendar 2019の記事です)

なお、AssemblyDefinitionFileによって生成されるアセンブリ名は、インスペクタ内のNameフィールドで設定します。
アセット名ではないことに注意しましょう。

InternalsVisibleToを使ってinternalアクセスしてみる

適当なAssemblyDefinitionFileを作成し、インスペクタビューからNameを変更しましょう。
ここでは、Unity.InternalAPIEditorBridgeDev.001という、実にそれっぽい名前(おそらくUnityの中の人がテストするためのアセンブリ名)を指定します。

次に、Unity.InternalAPIEditorBridgeDev.001.asmdefがあるディレクトリ以下に、次のようなスクリプト(Solution2_InternalsVisibleToAttribute.cs)を追加し、Unity.InternalAPIEditorBridgeDev.001アセンブリに組み込みましょう。

using System;
using UnityEditor;

namespace MainWindowTitleModifier
{
    public class Solution2_InternalsVisibleToAttribute
    {
        [MenuItem("MainWindowTitleModifier/Solution2_InternalsVisibleToAttribute", priority = 2)]
        static void Update()
        {
            Action<ApplicationTitleDescriptor> cb = x => x.title = "Solution2_InternalsVisibleToAttribute";
            EditorApplication.updateMainWindowTitle += cb;
            EditorApplication.UpdateMainWindowTitle();
            EditorApplication.updateMainWindowTitle -= cb;
        }
    }
}

リフレクションが完全に消え、イメージしたようなスッキリしたコードになりました!
直接呼び出しになるので、実行速度も申し分ありませんし、IDEのサポート(インテリセンスやブレークポイント)によって格段に生産性が上がりました。
MainWindowTitleModifier > Solution2_InternalsVisibleToAttributeを実行すれば、同様にタイトルバーのテキストを変えられます。

InternalsVisibleToの欠点

AssemblyDefinitionFileInternalsVisibleToAttributeを使ったinternalアクセスは、非常に手軽な方法です。
しかし、残念ながら、全てにおいて完璧とは言えません:

  • privateアクセスができない
    • 必要な場合、リフレクションを使ってください
  • ライブラリ側にInternalsVisibleToAttributeが定義されている必要がある
    • 「ライブラリ側に」属性があることが条件です
    • 例えばライブラリのアップデート等で、ライブラリ側からInternalsVisibleToAttributeが変更・削除されると、コンパイルエラーになります
  • ランタイムでは使用できない場面が多い
    • UnityEngine.dll等は、ビルド時にInternalsVisibleToAttributeが綺麗サッパリなくなります
    • これは、エディタ用とは異なるランタイム用アセンブリが使用されるためです
  • アセンブリ名が衝突するとエラー
    • パッケージを外部に公開する場合、誰かが同じフレンドアセンブリの名前を使い、アセンブリ名が衝突する可能性があります
    • これは潜在的な不具合と言えます

難儀な欠点が多いですが、逆に言えば、クローズドな環境でエディタ向けの機能を実装するのであれば、この方法だけでも十分に使えそうです。

【方法3】IgnoresAccessChecksToAttribute

IgnoresAccessChecksToAttributeは、ほとんど情報が出回っていない(MSDNにも載ってない)ミステリアスな属性です。
私はNo InternalsVisibleTo, no problem – bypassing C# visibility rules with Roslynで偶然知りました。
要約すると:

  • InternalsVisibleToAttributeとは逆の方向に作用する。つまり、ライブラリ利用者側に設定することで、ライブラリに対するinternalアクセスを許可する
  • フルネームはSystem.Runtime.CompilerServices.IgnoresAccessChecksToAttribute
  • Base Class Libraryに載っていないが、ランタイム(CLR/CoreCLR)で作用する
  • csc.exeMsBuildを使わずに自力でコンパイルする際に、CSharpCompilationOptions.TopLevelBinderFlagsに対して特定のフラグを立てると有効になる

つまり、どんなライブラリに対してもinternalアクセスができるようになる、夢のような属性ってことですね!
さっそく、この属性を使ってみましょう。

IgnoresAccessChecksToAttributeを使ってinternalアクセスしてみる

先述の記事にある通り、IgnoresAccessChecksToAttributeを使うにはcsc.exeMsBuildを使わずに、自力でコンパイルする必要があります
自力でコンパイルする方法を紹介しますが、正直めんどくさいです。
興味のない方は「IgnoresAccessChecksToの準備がめんどくさいのでなんとかする」まで読み飛ばし推奨。


まず、C#プロジェクトに以下のコードを追加します。
こうすることで、「UnityEditorアセンブリに対するアクセスレベルのチェックを無視する」ようにコンパイラが解釈できます。

[assembly: System.Runtime.CompilerServices.IgnoresAccessChecksTo("UnityEditor")]
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)]
    internal class IgnoresAccessChecksToAttribute : System.Attribute
    {
        public IgnoresAccessChecksToAttribute(string assemblyName)
        {
            AssemblyName = assemblyName;
        }

        public string AssemblyName { get; }
    }
}

次に、C#プロジェクトとは別に、「アクセスチェックを無視できる」コンパイラのプロジェクトを作ります。コードは以下の通りです。

// あらかじめMicrosoft.CodeAnalysis.CSharpをnugetでインストールしておく
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class InternalAccessibleCompiler
{
    public static void Main (string [] args)
    {
        string inputCsProjPath = args [0];
        string outputAsemblyPath = args [1];
        string outputAsemblyName = Path.GetFileNameWithoutExtension (outputAsemblyPath);

        // C#プロジェクトを読み込みます.
        string[] csproj = File.ReadAllLines(inputCsProjPath);

        // dllとしてコンパイルさせるオプションを生成します.
        CSharpCompilationOptions compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
            .WithMetadataImportOptions(MetadataImportOptions.All);

        // BindingFlags.IgnoreAccessibility(1 << 22)を有効化します.
        typeof(CSharpCompilationOptions)
            .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic)
            .SetValue(compilationOptions, (uint)1 << 22);

        // プロジェクトからアセンブリ参照一覧を取得→IEnumerable<PortableExecutableReference>に変換します.
        Regex reg_dll = new Regex("<HintPath>(.*)</HintPath>", RegexOptions.Compiled);
        IEnumerable<PortableExecutableReference> metadataReferences = csproj
            .Select(line => reg_dll.Match(line))
            .Where(match => match.Success)
            .Select(match => match.Groups[1].Value)
            .Select(path => MetadataReference.CreateFromFile(path));

        // プロジェクトからシンボル一覧を取得→IEnumerable<string>に変換します.
        Regex reg_preprocessorSymbols = new Regex("<DefineConstants>(.*)</DefineConstants>", RegexOptions.Compiled);
        IEnumerable<string> preprocessorSymbols = csproj
            .Select(line => reg_preprocessorSymbols.Match(line))
            .Where(match => match.Success)
            .SelectMany(match => match.Groups[1].Value.Split(';'));

        // プロジェクトからソースコード一覧を取得→テキストとして読み込み→IEnumerable<SyntaxTree>に変換します.
        CSharpParseOptions parserOption = new CSharpParseOptions(LanguageVersion.Latest,
                preprocessorSymbols: preprocessorSymbols);
        Regex reg_cs = new Regex("<Compile Include=\"(.*\\.cs)\"", RegexOptions.Compiled);
        IEnumerable<SyntaxTree> syntaxTrees = csproj
            .Select(line => reg_cs.Match(line))
            .Where(match => match.Success)
            .Select(match => match.Groups[1].Value.Replace('\\', Path.DirectorySeparatorChar))
            .Select(path => Path.Combine(inputCsProjDir, path))
            .Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), parserOption, path));

        // コンパイルを実行し、dllを生成します.
        CSharpCompilation.Create(outputAsemblyName, syntaxTrees, metadataReferences, compilationOptions)
            .Emit(outputAsemblyPath);
    }
}

なお、このコードはコンパイル対象がUnityが生成したC#プロジェクトであることを前提にしています。
他のジェネレータで生成されたC#プロジェクトをコンパイルできないかもしれません。
(きっと、おそらくMSBuildパッケージを使えばたぶん解決できます。)

dotnetを使って実行し、以下のようにC#プロジェクトファイルと出力パスを指定すると、internalアクセス可能なdllを生成できます。

dotnet run -- C#プロジェクトパス 出力dllパス

なお、このコンパイラはnuget toolとしても公開しています。(オプションが若干違います)

https://www.nuget.org/packages/InternalAccessibleCompiler/

IgnoresAccessChecksToの準備がめんどくさいのでなんとかする

IgnoresAccessChecksToをUnityで使うには準備がめんどくさいです。
今回は、先述のコンパイラを同梱済みのUnity向けに公開しているパッケージを使います。
(デモプロジェクトには既にインストールされています。)

  1. このパッケージにはdotnet 2.1以上が必要です
    • コマンドプロンプト(Windows)やターミナル(Mac)で、dotnet --versionを実行したときに、2.1.xxx以上が表示されていればインストール不要です
    • https://dotnet.microsoft.com/download からインストールしてください
  2. manifest.jsonのdependencies"com.coffee.internal-accessible-compiler": "https://github.com/mob-sakai/InternalAccessibleCompilerForUnity.git"を追加し、パッケージをインストールします
  3. プロジェクトビューでMainWindowTitleModifier.asmdefを選択し、コンテキストメニュー(右クリック)からInternal Accessible Compiler > Settingをクリックします
  4. 開いたウィンドウで、以下のように入力します
    • Assembly Names To Access: UnityEditor
    • Output Dll Path: Assets/Editor/Solution3.IgnoresAccessChecksToAttribute/MainWindowTitleModifier.dll
  5. Compileをクリックし、dllにコンパイルします

AssemblyDefinitionFileやソースコードは、【方法2】InternalsVisibleToとアセンブリ名やクラス名が被らないようにする以外、ほとんど同じです。
コンパイルが完了すると、MainWindowTitleModifier.dllが生成されます。
このままでは同じ名前を持つMenuItemが衝突してしまうので、MainWindowTitleModifier.asmdefDefine Constraintsに適当な文字列を入力して、インポートされないようにしておきましょう。

MainWindowTitleModifier.dllのインポート後、MainWindowTitleModifier > Solution3_IgnoresAccessChecksToAttributeを実行すれば、タイトルバーのテキストを変えられます。

IgnoresAccessChecksToの欠点

どんなアセンブリに対してもinternalアクセスができるため、この方法は非常に強力です。
【方法2】InternalsVisibleToの欠点も少し克服していますが、それでも完璧ではありません:

  • privateアクセスができない
    • 必要な場合、やはりリフレクションを使ってください
  • 使うためにはコンパイルする必要がある
    • コンパイルという手間が発生します
    • 自動化するなり、運用でカバーしましょう
  • コード内でUNITY_2019等のシンボルを使う場合、各バージョンのdll生成が必要
    • コード内のシンボルは上記コンパイルの際に解決されます
    • そのため、UnityのバージョンアップでAPIが変更されると、APIのバージョン毎にdllを複数生成する必要があります
    • 生成したdllのインポート設定のDefine Constraintsから、特定のバージョンのdllのみをインポートするようにしましょう
    • 自動化するなり、運用でカバーしましょう

これまで紹介したinternal要素へのアクセス方法まとめ

今回は外部アセンブリからinternalアクセスする方法を3つ紹介しました。

方法 利点 欠点 private
アクセス
生産性 実行速度 ランタイム
【方法1】
Reflection
:thumbsup:情報がたくさんある
:thumbsup:privateアクセス可
:thumbsdown:コードの可読性・保守性が低い
:thumbsdown:低速な実行速度
:thumbsdown:インテリセンスが無効
:white_check_mark: :ng: :ng: :white_check_mark:
【方法2】
InternalsVisibleTo
:thumbsup:手軽
:thumbsup:インテリセンスが有効
:thumbsup:高速な実行速度
:thumbsdown:privateアクセス不可
:thumbsdown:アセンブリ名が被るとエラー
:thumbsdown:ライブラリ側に属性が必要
:ng: :white_check_mark: :white_check_mark: :ng:
【方法3】
IgnoresAccessChecksTo
:thumbsup:privateアクセス可
:thumbsup:どんなライブラリに対しても有効
:thumbsup:インテリセンスが有効
:thumbsup:高速な実行速度
:thumbsdown:自力コンパイルが必要
:thumbsdown:シンボルの取り扱いに注意が必要
:white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:

以下は、パッケージマネージャのUIを拡張するパッケージ、UpmGitExtensionを実装した際のワークフローです。
(実際にはdll生成の工程を自動化したり、AssemblyDefinitionFileの参照関係を整理したり、dllを生成するために使ったディレクトリをパッケージからパージしてます。)

  • 一旦、手軽な方法である【方法2】InternalsVisibleToで実装する
    • 後でパージしやすいように、パッケージディレクトリに専用のディレクトリを作っておく
  • パッケージを公開する前【方法3】IgnoresAccessChecksToでdllを生成する
  • パッケージから「dllを生成するために使ったディレクトリ」をパージし、公開
    • .npmignoreにディレクトリを記載しておき、npm packしてパージする
    • packしたものはnpmレジストリにそのまま公開できる
    • packしたものを展開し、upmブランチにプッシュする
  • どうしてもprivateアクセスが必要な場面は【方法1】Reflectionを使う
    • Exposeクラスのような、リフレクション機能をラップするクラスを作っておくと楽
    • Expose.FromType(typeof(EditorApplication)).Call("UpdateMainWindowTitle");のように呼び出せる

雑記

UpmGitExtensionの実装において、internal要素へのアクセスは必須事項でした。
UnityやPackageManagerUIのAPIが日々アプデされる状況において、リフレクションのみで対応を続けるのは苦しいものでした。
製作者が見ても意味のわからないコード、名前か引数が変わりエラーを吐き続けるAPI、直ったと思いきや別ベクトルから迫りくるデグレ...
UnityやPackageManagerUIがマイナーアップデートする度にAPIは変更され、どこからか不具合が起き、改修を迫られ、心が折れかけました。2019.3なんて中身ほとんど別モンやんけ!

そんな中、今回紹介した方法を使うことで、手軽にinternalにアクセスできるようになり、テストやソースコードの使いまわし(UnityEditor内部のMiniJsonを使ったり)が容易になり、生産性やコードの見通しが格段に改善され、お肌の調子も良くなりました。
もし、あなたの近くでC#のリフレクションに苦しんでいるエンジニアを見かけましたら、そっと本記事のリンクをDMしてあげてください

終わりに

最後に、本当に大事なことなので三回言いますが、internalな型やメンバは、ライブラリ製作者がライブラリ利用者に対して隠蔽したい項目であり、APIやドキュメントは一般的に非公開です
ライブラリ製作者は、決して意地悪でinternalを使っているのではありません。
不用意にその型やメンバを扱うと、とんでもない結果になるかもしれないからinternalで隠蔽しているのかもしれません。
internalな型やメンバは、「そうあるべき正当な理由があってinternalになっている」ということを理解した上、くれぐれも自己責任で活用してください。

皆様、よいUnityライフを!

12/13 15:00訂正

Twitterで@neuecc様からリプライをいただいたのですが、リンク先の「*AllowPrivate」というワードが引っかかりました。
参照しているソースコードを読む限り、IgnoresAccessChecksTo以上のことはしていない様子...
これらの事実から導き出される答えは...
IgnoresAccessChecksToはprivateアクセスもできるということです!
よく見たら元記事でもprivateアクセスしてた...

軽くテストしてみたところ、確かにIDE上ではprivateアクセスがエラーとして表示されましたが、InternalAccessCompilerではエラーなくコンパイルできていました。
もちろん動作も問題ありませんでした。

具体的なワークフローの更新については別記事でまとめますので、もう少々お待ちくださいm(_ _)m。

12/26 続編でました

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away