この記事は【unityプロ技】 Advent Calendar 2019の13日目の記事です。
この記事におけるソースコードは、全てPublic Domain
です。
※12/13 15:00 訂正あり
※12/26 続編でました
TL;DR
- C#には
IgnoresAccessChecksToAttribute
っていう隠された属性があるよ。ググってもほとんど情報出てこないよ。 - この属性を使えば、任意の外部アセンブリのinternalの型やメンバに対して自由にアクセスできるよ。ヤバいよ。
-
InternalsVisibleToAttribute
やAssemblyDefinitionFile
と組み合わせることで、インテリセンスやブレークポイントも効くよ。リフレクションより格段に便利だよ。
おさらい: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)の利用が基本です。
- メニューの実行:EditorApplication.ExecuteMenuItem
- アセットインポートの事前/事後処理:AssetPostprocessor
- インスペクタや自前ウィンドウの実装:EditorWindow
- 次のフレームでアクションを実行する:EditorApplication.delayCall
- ツールバーにメニューを追加する:MenuItem
- コンテキストメニューを追加する:ContextMenu
- コンパイル後にスクリプトを実行する:Callbacks.DidReloadScripts
- パッケージマネージャUIの拡張:PackageManager.UI.IPackageManagerExtension
- etc.
御存知の通り、公開されている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;
updateMainWindowTitle
、UpdateMainWindowTitle
、ApplicationTitleDescriptor
は全て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等、要素が複雑になると、だんだんと長く、ツラくなってきます。めげずに頑張ってググりましょう。
おまけ:リフレクションを効率化して使う
-
Type
やMemberInfo
のインスタンスをDictionaryでキャッシュする- 一度生成した
Type
やMemberInfo
のインスタンスを使いまわすことで、オーバーヘッドを回避します
- 一度生成した
-
FindMethod、FindProperty等のメソッドをラップする
- Public/NonPublic、Instance/Staticを指定しなくても適切な
MemberInfo
を返すようなメソッドを作り、可読性を改善します
- Public/NonPublic、Instance/Staticを指定しなくても適切な
-
アセンブリからではなく
string
から型を見つける-
Type.GetType("UnityEditor.ApplicationTitleDescriptor, UnityEditor")
のように、クラス名フルパス, アセンブリ名
という文字列でも型を取得できます - このときの文字列は
Type
インスタンスを使って、type.AssemblyQualifiedName
で取得できます
-
-
デリゲート化、式木、IL Emit
- [雑記] 動的コード生成のパフォーマンス(++C++; // 未確認飛行 C様)を参照してください
【方法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
の欠点
AssemblyDefinitionFile
とInternalsVisibleToAttribute
を使ったinternalアクセスは、非常に手軽な方法です。
しかし、残念ながら、全てにおいて完璧とは言えません:
-
privateアクセスができない
- 必要な場合、リフレクションを使ってください
-
ライブラリ側に
InternalsVisibleToAttribute
が定義されている必要がある- **「ライブラリ側に」**属性があることが条件です
- 例えばライブラリのアップデート等で、ライブラリ側から
InternalsVisibleToAttribute
が変更・削除されると、コンパイルエラーになります
-
ランタイムでは使用できない場面が多い
- UnityEngine.dll等は、ビルド時に
InternalsVisibleToAttribute
が綺麗サッパリなくなります - これは、エディタ用とは異なるランタイム用アセンブリが使用されるためです
- UnityEngine.dll等は、ビルド時に
-
アセンブリ名が衝突するとエラー
- パッケージを外部に公開する場合、誰かが同じフレンドアセンブリの名前を使い、アセンブリ名が衝突する可能性があります
- これは潜在的な不具合と言えます
難儀な欠点が多いですが、逆に言えば、クローズドな環境でエディタ向けの機能を実装するのであれば、この方法だけでも十分に使えそうです。
【方法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.exe
やMsBuild
を使わずに自力でコンパイルする際に、CSharpCompilationOptions.TopLevelBinderFlags
に対して特定のフラグを立てると有効になる
つまり、どんなライブラリに対してもinternalアクセスができるようになる、夢のような属性ってことですね!
さっそく、この属性を使ってみましょう。
IgnoresAccessChecksToAttribute
を使ってinternalアクセスしてみる
先述の記事にある通り、IgnoresAccessChecksToAttribute
を使うには**csc.exe
やMsBuild
を使わずに、自力でコンパイルする必要があります**。
自力でコンパイルする方法を紹介しますが、正直めんどくさいです。
興味のない方は「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向けに公開しているパッケージを使います。
(デモプロジェクトには既にインストールされています。)
- このパッケージには
dotnet 2.1
以上が必要です- コマンドプロンプト(Windows)やターミナル(Mac)で、
dotnet --version
を実行したときに、2.1.xxx
以上が表示されていればインストール不要です - https://dotnet.microsoft.com/download からインストールしてください
- コマンドプロンプト(Windows)やターミナル(Mac)で、
- manifest.jsonの
dependencies
に"com.coffee.internal-accessible-compiler": "https://github.com/mob-sakai/InternalAccessibleCompilerForUnity.git"
を追加し、パッケージをインストールします - プロジェクトビューで
MainWindowTitleModifier.asmdef
を選択し、コンテキストメニュー(右クリック)からInternal Accessible Compiler > Setting
をクリックします
- 開いたウィンドウで、以下のように入力します
-
Compile
をクリックし、dllにコンパイルします
AssemblyDefinitionFile
やソースコードは、【方法2】InternalsVisibleToとアセンブリ名やクラス名が被らないようにする以外、ほとんど同じです。
コンパイルが完了すると、MainWindowTitleModifier.dll
が生成されます。
このままでは同じ名前を持つMenuItem
が衝突してしまうので、MainWindowTitleModifier.asmdef
のDefine 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 |
情報がたくさんある privateアクセス可 |
コードの可読性・保守性が低い 低速な実行速度 インテリセンスが無効 |
||||
【方法2】 InternalsVisibleTo |
手軽 インテリセンスが有効 高速な実行速度 |
privateアクセス不可 アセンブリ名が被るとエラー ライブラリ側に属性が必要 |
||||
【方法3】 IgnoresAccessChecksTo |
privateアクセス可 どんなライブラリに対しても有効 インテリセンスが有効 高速な実行速度 |
自力コンパイルが必要 シンボルの取り扱いに注意が必要 |
以下は、パッケージマネージャのUIを拡張するパッケージ、UpmGitExtensionを実装した際のワークフローです。
(実際にはdll生成の工程を自動化したり、AssemblyDefinitionFile
の参照関係を整理したり、dllを生成するために使ったディレクトリをパッケージからパージしてます。)
- 一旦、手軽な方法である【方法2】InternalsVisibleToで実装する
- 後でパージしやすいように、パッケージディレクトリに専用のディレクトリを作っておく
-
パッケージを公開する前に【方法3】IgnoresAccessChecksToでdllを生成する
- nugetに公開しているInternalAccessibleCompilerとシェルスクリプトで一括処理
- パッケージから「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訂正
まぁ私もこないだまで知らなくて教えてもらったのですがが。vs-mefとかで投下されてて効果大らすぃ。 https://t.co/w6AzJoxK4y
— neuecc (@neuecc) December 13, 2019
Twitterで@neuecc様からリプライをいただいたのですが、リンク先の**「*AllowPrivate」**というワードが引っかかりました。
参照しているソースコードを読む限り、IgnoresAccessChecksTo
以上のことはしていない様子...
これらの事実から導き出される答えは...
IgnoresAccessChecksTo
はprivateアクセスもできるということです!
よく見たら元記事でもprivateアクセスしてた...
軽くテストしてみたところ、確かにIDE上ではprivateアクセスがエラーとして表示されましたが、InternalAccessCompilerではエラーなくコンパイルできていました。
もちろん動作も問題ありませんでした。
具体的なワークフローの更新については別記事でまとめますので、もう少々お待ちくださいm(_ _)m。