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

この記事は[Unity Advent Calendar 2021](https://qiita.com/advent-calendar/2021/unity)の23日目の記事です。

## TL;DR

- アセンブリに`InternalsVisibleToAttribute`属性が含まれている場合、指定したアセンブリからinternalアクセスを許可するよ
- Unity 2019.2以降は`asmref`を使うと、他の`asmdef`に`InternalsVisibleToAttribute`属性を追加してinternalアクセスを許可できるよ
- コンパイラを変更するパッケージ「[CSharpCompilerSettingsForUnity](https://github.com/mob-sakai/CSharpCompilerSettingsForUnity)」を使うと、アセンブリに`IgnoresAccessChecksToAttribute`属性を追加してinternal/privateアクセスできるよ

## 非publicな型やメンバにアクセスしたい！

以前、非publicな型やメンバにアクセスする方法について記事を書きました。

- [【Unity, C#】internalな型やメンバにアクセスするには、多分これが一番早いと思います](https://qiita.com/mob-sakai/items/f3bbc0c45abc31ea7ac0)
- [【Unity, C#】続・privateな型やメンバにアクセスするには、多分これが一番早いと思います](https://qiita.com/mob-sakai/items/a24780d68a6133be338f)

その中で、外部アセンブリからinternalアクセスする方法を3つ紹介しました。

|方法|利点|欠点|private<br>アクセス|生産性|実行速度|プレイヤービルド|
|---|---|---|--------|-------------|------------|----------|
|[【方法1】<br>Reflection](方法1reflection)|:o: 情報がたくさんある<br>:o: privateアクセス可|:x: コードの可読性・保守性が低い<br>:x: 低速な実行速度<br>:x: インテリセンス無効|:white_check_mark:|:ng:|:ng:|:white_check_mark: |
|[【方法2】<br>InternalsVisibleTo](方法2internalsvisibletoattribute)|:o: 手軽<br>:o: インテリセンスが有効<br>:o: 高速な実行速度|:x: privateアクセス未対応<br>:x: 「使用するライブラリ側」に属性が必要|:ng:|:white_check_mark:|:white_check_mark:|:ng:|
|[【方法3】<br>IgnoresAccessChecksTo](方法3ignoresaccesscheckstoattribute)|:o: privateアクセス可<br>:o: どんなライブラリに対しても有効<br>:o: 高速な実行速度|:x: コンパイルが面倒<br>:x: インテリセンス無効|:white_check_mark:|:ng:|:white_check_mark:|:white_check_mark:|

[方法1reflection]:https://qiita.com/mob-sakai/items/f3bbc0c45abc31ea7ac0#方法1reflection
[方法2internalsvisibletoattribute]:https://qiita.com/mob-sakai/items/f3bbc0c45abc31ea7ac0#方法2internalsvisibletoattribute
[方法3ignoresaccesscheckstoattribute]:https://qiita.com/mob-sakai/items/f3bbc0c45abc31ea7ac0#方法3ignoresaccesscheckstoattribute

これらの手法についていくつかのアップデートがありました。

## 【方法2.1】 (Unity 2019.2~) Assembly Definition Referenceを使って`InternalsVisibleToAttribute`属性を追加する

Unity 2019.2にて、**Assembly Definition Reference**機能が追加されました。

https://docs.unity3d.com/2019.4/Documentation/Manual/class-AssemblyDefinitionReferenceImporter.html

> （DeepL翻訳）
Assembly Definition Reference は、Assembly Definition への参照を定義するアセットです。
フォルダに Assembly Definition Reference アセットを作成すると、そのフォルダにあるスクリプトを参照する Assembly Definition に含めることができます（新しいアセンブリを作成するのではありません）。
子フォルダ内のスクリプトも、独自の Assembly Definition または Assembly Definition Reference アセットがない限り、含まれます。


要するに、別の場所にある`asmdef`に対してソースファイルを追加できる機能です。
これによって、配布パッケージをフォークしたり埋め込みすることなく拡張できるようになりました。
同じアセンブリ内のソースファイルとして扱われるためinternalアクセスが可能で、`partial class`であればprivateアクセスも可能です。
そして、`InternalsVisibleToAttribute`属性を追加することで、任意のアセンブリからでもinternalアクセスを許可できます。

例として、`Unity.TextMeshPro`アセンブリにある`TMP.TMP_ListPool<T>`という、リストプールクラスを外部から使用してみましょう。
なお、Unity 2021からは`CollectionPool<T0,T1>`が標準実装されているため、**このサンプル自体には利用価値は無い**です。

### ステップ1: Assembly Definition Reference (*.asmref)を作成する

まずは適当な場所にフォルダを作り、コンテキストメニュー`Create > Assembly Definition Reference`から`asmref`ファイルを作成し、対象となるAssembly Definitionとして`Unity.TextMeshPro`を指定しましょう。
同じフォルダに`asmdef`と`asmref`の両方があるとエラーになるので注意してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/3fb23b6b-9fdb-ac0b-4eea-b800ae1378bd.png)


### ステップ2: `AssemblyInfoExtensions.cs`を作成する

次に、コンテキストメニュー`Create > C# Script`から同じフォルダにcsファイルを作成し、`AssemblyInfoExtensions`と名付けましょう。
名前は適当でもいいですが、同じアセンブリに対して同名のソースファイルがあるとエラーになるので注意してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/c47a9b15-23da-aa18-8c4b-cc6b0b8b1b6d.png)


### ステップ3: `AssemblyInfoExtensions.cs`を編集する

これまでのステップで、`AssemblyInfoExtensions.cs`が`Unity.TextMeshPro`アセンブリに組み込まれました！
最後に、`InternalsVisibleToAttribute`属性を追加しましょう。
アセンブリ名は`asmdef`に記載されているアセンブリ名（**ファイル名では無いので注意！**）や、`Assembly-CSharp`等の特殊アセンブリ名を指定しましょう。

```csharp
// AssemblyInfoExtensions.cs
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Your-Assembly-Name")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Assembly-CSharp")]
```

### ステップ4: internalアクセスできるか試す

ステップ3で指定したアセンブリから、internalアクセスができるか試してみましょう。

```csharp
// TMP_ListPoolUser.cs (Assembly-CSharp)
using TMPro;
using UnityEngine;

public class TMP_ListPoolUser : MonoBehavior
{
    private void Start()
    {
        var l = TMP_ListPool<int>.Get();

        l.Add(0);
        l.Add(1);
        l.Add(2);

        Debug.Log($"List count is {l.Count}");

        TMP_ListPool<int>.Release(l);
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/f1a28d0a-c385-cfd1-405f-081d4de34db2.png)

無事、internalクラスにアクセスできました！

### 注意点

この手法の欠点は、**コンパイル済みアセンブリ（*.dll）に対して利用できない**ことです。
例えば、`UnityEngine`や`UnityEditor`等には対応できません。

ただし、対象となるアセンブリに`InternalsVisibleToAttribute`属性が含まれている場合、そのアセンブリ経由でinternalアクセスできます。
例えば`UnityEditor`は`Unity.TextMeshPro.Editor`等のアセンブリからはinternalアクセスできます。

```csharp
// UnityEditorアセンブリ内AssemblyInfo.cs
...
[assembly: InternalsVisibleTo("Unity.TextMeshPro.Editor")]
...
```

例として、`Unity.TextMeshPro.Editor`にasmref経由でソースファイルを追加し、「指定秒数後にアクションを実行するinternalメソッド`EditorApplication.CallDelayed`」を使ってみましょう。

```csharp
// TMPEditor_UnityEditor_internal.cs (Unity.TextMeshPro.Editor)
using UnityEngine;
using UnityEditor;

public class TMPEditor_UnityEditor_internal
{
    [MenuItem("Test/CallDelayed")]
    private static void CallDelayed()
    {
        Debug.Log("[CallDelayed] Start!");
        EditorApplication.CallDelayed(() => Debug.Log("[CallDelayed] Called!"), 5.0f);
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/e050da08-e84f-d1e8-072a-788296a962bf.png)


## 【方法3.1】 CSharpCompilerSettingsForUnityを使って`IgnoresAccessChecksToAttribute`属性を追加する

[CSharpCompilerSettingsForUnity](https://github.com/mob-sakai/CSharpCompilerSettingsForUnity)はUnityのコンパイルタスクの設定を変更するパッケージです。
コンパイラ（csc）を[OpenSesame.Net.Compilers.Toolset](https://www.nuget.org/packages/OpenSesame.Net.Compilers.Toolset)に差し替えることで、自動的に`IgnoresAccessChecksToAttribute`属性が追加され、他アセンブリに対してinternal/privateアクセスできるようになります。


### ステップ1: CSharpCompilerSettingsForUnityをインストールする

PackageManagerを開き、左上の「＋」ボタン→`Add package from git URL...`をクリックしてください。
`https://github.com/mob-sakai/CSharpCompilerSettingsForUnity` を指定し、Addボタンを押すと、git経由でパッケージがインストールされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/280089e4-126e-4f61-6d48-32f8d7e2ab52.png)

### ステップ2-1: (プロジェクト全体のコンパイラを変更する場合) Project Settingsからコンパイラパッケージを設定する

`Project Settings > C# Compiler`でプロジェクト全体で使用するコンパイラを差し替えられます。

- `Compiler Type`を`Builtin`から`Custom Package`に変更する
- `Name`を`OpenSesame.Net.Compilers.Toolset`に変更する
- `Version`を`4.0.1`に変更する
- 必要に応じてTarget Assembliesにアセットパスを追加する（必要最低限のアセンブリを対象にすることをおすすめします）
- 画面下部にある「Apply」をクリックする

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/22f289f0-6ec6-bc76-8886-5782430a7634.png)

### ステップ2-2: (asmdef単位でコンパイラを変更する場合) asmdefのインスペクタからコンパイラパッケージを設定する

CSharpCompilerSettingsForUnityはasmdef単位でのコンパイラ変更をサポートしています。
プロジェクトビューで`asmdef`を選択し、インスペクタにある`Enable C# Compiler Settings`にチェックしてください。
- `Compiler Type`を`Builtin`から`Custom Package`に変更する
- `Name`を`OpenSesame.Net.Compilers.Toolset`に変更する
- `Version`を`4.0.1`に変更する
- 下部にある「Apply」をクリックする

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/0a5f3a9f-c553-c617-1cdd-feb3c80c73b6.png)

ちなみに、一度設定しておけばCSharpCompilerSettingsForUnityをアンインストールしても動作します。
（同じフォルダにある`CSharpCompilerSettings_*.dll`を削除すると動作しなくなります）

### ステップ3:　ソースファイルを追加する

ソースコードではinternal/privateアクセスが可能です。
Unity 2021.1以降では「初回コンパイル前にユーザーコードが実行できない」「コンパイルフローが大幅に変更された」影響で、internal/privateアクセスは`#if CUSTOM_COMPILE 〜 #endif`に記述する必要があります。

```csharp
using System;
using TMPro;
using UnityEngine;

public class TMP_ListPoolUser_OpenSesame : MonoBehaviour
{
    private void Start()
    {
// Unity 2021.1以降では必須
#if CUSTOM_COMPILE
        var l = TMP_ListPool<int>.Get();

        l.Add(0);
        l.Add(1);
        l.Add(2);

        Debug.Log($"List count is {l.Count}");

        TMP_ListPool<int>.Release(l);
#endif
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158545/f1a28d0a-c385-cfd1-405f-081d4de34db2.png)

無事、internalクラスにアクセスできました！

### おまけ

CSharpCompilerSettingsForUnityの他の機能について紹介まで。

- C#バージョンの変更（〜C# 10）
    - .Net5/6等のAPIが必要な一部機能には対応していません。
- Analyzer/Source Generatorパッケージの適用
- dllとしてアセンブリを別ディレクトリに出力

## まとめ

|方法|利点|欠点|private<br>アクセス|生産性|実行速度|プレイヤービルド|
|---|---|---|--------|-------------|------------|----------|
|【方法2.1】<br>Assembly Definition Reference|:o: 手軽<br>:o: インテリセンスが有効<br>:o: 高速な実行速度|:x: privateアクセス未対応<br>:x: コンパイル済みアセンブリ(*.dll)は未対応|:ng:|:white_check_mark:|:white_check_mark:|:white_check_mark:|
|【方法3.1】<br>CSharpCompilerSettingsForUnity<br>& OpenSesameCompiler|:o: privateアクセス可<br>:o: どんなライブラリに対しても有効<br>:o: 高速な実行速度|:x: インテリセンス無効|:white_check_mark:|:ng:|:white_check_mark:|:white_check_mark:|


本記事に関するプロジェクトを以下のリポジトリにアップしました。
https://github.com/mob-sakai/UnityAdventCalendar2021