May 2009
Volume 24 Number 05
CLR 徹底解剖 - CLR Binder について理解する
Aarthi Ramamurthy | May 2009
目次
必ずアセンブリの完全指定名を使用する
部分的なバインドを避ける
Fusion ログ ビューアを使用する
コンテキストについて理解する
最後の手段としての AssemblyResolve
GAC を使用する状況
CLR Binder には、必要なアセンブリを実行時に特定し、それらのアセンブリへのバインドを行う機能があるため、重要な .NET コードであると言えます。バインドを効率的かつ適切に機能させるには、いくつかのベスト プラクティスに従う必要があります。このコラムでは、このベスト プラクティスについて説明します。一部のプラクティスは単純ですが、きわめて重要です。アセンブリに適切な名前を付けることは、こうしたタスクの 1 つです。まずこの内容から見ていきましょう。
必ずアセンブリの完全指定名を使用する
アセンブリ名とファイル名の相違が混乱の元となることがよくあるため、詳しく見ていきます。ファイル名は、ファイル システム内のファイルの名前です (System.dll など)。一方、アセンブリ名は、固有の ID を確立するためにアセンブリに付与される名前です。マネージ コードでは、アセンブリがその中に含まれているコードに ID を提供します。ID が同じ 2 つのアセンブリは、同じ名前を持つ可能性があります (ただし、バージョン、シグネチャなどが異なる)。ファイル システムはアセンブリの読み込み元の 1 つにすぎません。アセンブリはバイト配列などからも読み込むことができます。ファイル名はアセンブリ名と同じにしておくことをお勧めします。その最もわかりやすい理由としては利便性が挙げられますが、アセンブリは通常、アセンブリ名を基に読み込まれるため、名前を揃えておくとローダーがアセンブリを見つけやすくなるという点も理由の 1 つです。アセンブリの完全修飾 ID は、アセンブリの簡易名、バージョン、カルチャ、公開キー トークンという 4 つのフィールドで構成されます。
Binder とその各種機能を効果的に使用する方法の 1 つは、(処理内容を完全に理解している場合を除き) 部分的なバインドを避けることです。部分的なバインドは、ユーザーがアセンブリ ID の一部だけを指定した場合に行われます。たとえば、次のように、簡易名が MySampleAssembly のアセンブリをユーザーが読み込もうとしていると仮定しましょう。
Assembly.Load("MySampleAssembly");
このとき、ユーザーはアセンブリ ID の他の 3 つのフィールドを指定しませんでした。これが部分的なバインドです。
これ以外に、ユーザーが Assembly.LoadWithPartialName() を使用してアセンブリを読み込むケースも考えられます。Assembly.LoadWithPartialName でも部分的なバインドを使用します。このメソッドは非常に特殊な場面で使用されます。通常のバインドの場合は使用しないでください。最新バージョンのフレームワークで旧式としてマークされているのは、このためです。
部分的なバインドを避ける
Binder には適切なアセンブリを読み込むのに必要な情報が揃っているわけではないので、部分的なバインドによって Binder の動作が一定でなくなる可能性があります。そのため、部分的なバインドは避けてください。LoadWithPartialName() を使用すると、Binder は単純に Load() を試みます。読み込みに失敗した場合、Binder は GAC 内で最も新しいバージョンのアセンブリを選択します。ただし、このアセンブリには現在のアプリケーションと互換性がない可能性があります。別のアプリケーションの更新プログラムによってこのアセンブリの新しいバージョンが GAC にインストールされた場合に、そのアセンブリと現在のアプリケーションの間に互換性がないとき、LoadWithPartialName() はこの新しいバージョンのアセンブリを選択して読み込むので、現在のアプリケーションが機能しなくなるおそれがあります。また、公開キー トークンなどの重要な属性を指定しないと、想定されている発行元からアセンブリが提供されたという保証がないため、不適切なアセンブリがバインドされ、読み込まれる可能性があります。
部分的なバインドが不適切だとすると、そもそもなぜ CLR でサポートされているのか、不思議に思われることでしょう。CLR 2.0 では LoadWithPartialName() は廃止されていますが、部分的に指定された参照でのアセンブリの読み込みは引き続きサポートされています。目的を十分に理解し、正しく使いさえすれば、部分的なバインドは非常に役立ちます。ただし、少なくとも読み込むアセンブリの公開キー トークンは指定する必要があります。そうしないと、このプロセスによってまったく異なる発行元からアセンブリが返される可能性があります。
また、特定のアセンブリを複数回読み込む必要がある場合や、アセンブリのバージョンを文字列でハードコーディングしない場合は、アセンブリの完全修飾名をアプリケーション構成ファイルで <qualifyAssembly> 要素の一部として指定し、Assembly.Load() でそのアセンブリの部分的な参照だけを指定することができます。こうすることで、コードをシンプルな状態に保つと同時に、目的のアセンブリを確実に読み込むことができます。ただしこの場合、各アプリケーションには、<qualifyAssembly> 要素を含む固有のアプリケーション構成ファイルが必要です。
通常は、バインドを予測できるように、目的のアセンブリのすべての要素が指定されている参照を指定する方が安全です。
Fusion ログ ビューアを使用する
時として、アセンブリのバインドに失敗することがあります。その原因は通常、例外にあります (一般に、FileNotFoundException、FileLoadException、BadImageFormatException)。Binder のしくみをある程度理解しておけば、直面している問題のデバッグに役立つことがよくあります。
Microsoft .NET Framework SDK には、アセンブリ バインディング ログ ビューア (fuslogvw.exe) というツールが含まれています。このツールは一般に Fusion ログ ビューアと呼ばれます。このツールでは、特定のバインドの処理を .html ファイルにログ記録できます。これは、Fusion ログ ビューアのユーザー インターフェイスを使用して表示可能です。
図 1 には、実際には存在しないアセンブリの読み込みを試みるコードが含まれています。このコードを実行すると、FileNotFoundException がスローされます。ユーザーがバインドの失敗のログ記録を有効にしている場合、Fusion ログ ビューアによって失敗がログに記録されます。
図 1 例外がスローされるコード
using System;
using System.Reflection;
class FusLogSample
{
public static void Main()
{
try
{
Assembly.Load("TheNonExistentAssembly");
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
ログ記録は一般にコストが高く、実行中のアプリケーションのパフォーマンスに影響を及ぼすため、ディスクへのログ記録は既定では無効になっています。バインドの失敗をデバッグするには、[設定] ボタンをクリックし、[バインドの失敗をディスクに記録する] をクリックします。すべてのバインドのログ記録を有効にするには、[すべてのバインドをディスクに記録する] をクリックします。
表示されたダイアログ ボックスで強調表示されているテキストをクリックすると、実際のログに移動できます (図 2 を参照)。最初の 2 行にアセンブリの読み込みに失敗したことが示され、HRESULT が記載されています。その次の 2 行には、CLR の読み込み元 (特定のディレクトリ) と、アセンブリの読み込みを開始した実行可能ファイルの名前が記載されています。
図 2 図 1 のコードの実行中に生成される Fusion ログ
** Assembly Binder Log Entry (2/23/2009 @ 12:29:19 PM) **
The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.
Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
Running under executable \\TKZAW-PRO-15\MYDOCS5\aarthir\My Documents\Visual Studio 2008\
Projects\Sample\Sample\bin\Debug\Sample.vshost.exe
--- A detailed error log follows.
=== Pre-bind state information ===
LOG: User = REDMOND\aarthir
LOG: DisplayName = TheNonExistantAssembly
(Partial)
LOG: Appbase = file://TKZAW-PRO-15/MYDOCS5/aarthir/My Documents/Visual Studio
2008/Projects/Sample/Sample/bin/Debug/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = NULL
Calling assembly : Sample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
===
LOG: This bind starts in default load context.
LOG: No application configuration file found.
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v2.0.50727\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file://TKZAW-PRO-15/MYDOCS5/aarthir/My Documents/Visual Studio
2008/Projects/Sample/Sample/bin/Debug/TheNonExistantAssembly.DLL.
LOG: Attempting download of new URL file://TKZAW-PRO-15/MYDOCS5/aarthir/My Documents/Visual Studio
2008/Projects/Sample/Sample/bin/Debug/TheNonExistantAssembly/TheNonExistantAssembly.DLL.
LOG: Attempting download of new URL file://TKZAW-PRO-15/MYDOCS5/aarthir/My Documents/Visual Studio
2008/Projects/Sample/Sample/bin/Debug/TheNonExistantAssembly.EXE.
LOG: Attempting download of new URL file://TKZAW-PRO-15/MYDOCS5/aarthir/My Documents/Visual Studio
2008/Projects/Sample/Sample/bin/Debug/TheNonExistantAssembly/TheNonExistantAssembly.EXE.
LOG: All probing URLs attempted and failed.
"Pre-bind state information" セクションの最初の行には、コードを実行したユーザーの名前が記載されています。次の行は、アセンブリの ID です。参照は一部のみ指定されていたので、ログに "Partial" と示されていることに注意してください。
次の行には、アプリケーション ドメインの ApplicationBase ディレクトリ (実行可能ファイルが格納されているディレクトリ) が示されています。
続く 4 つのフィールド (PrivatePath、Dynamic Base、AppName、および Cache Base) は、さまざまな方法でバインドに影響を及ぼすアプリケーション ドメイン固有のプロパティです。たとえば、privatePath 属性では、アセンブリの読み込み中に検索する ApplicationBase ディレクトリのサブディレクトリを指定します。
次の行には、Assembly.Load() が開始された親アセンブリの名前が示されています。その後の行には、このバインドが開始されたローダーのコンテキストが記載されています (ローダーのコンテキストについては、次のセクションで詳しく説明します)。
その次の 3 行は、ポリシー ファイルに関するログです。アプリケーション構成ファイル、発行者ポリシー ファイル、コンピュータ構成ファイルなどのポリシー ファイルは、アセンブリのバインド動作を構成するための手段です (これらの構成ファイルの詳細については、MSDN の「手順 1: 構成ファイルのチェック」を参照してください)。
最後の行には、Binder が目的のアセンブリを (AppBase 内の) さまざまなサブディレクトリで検索したことが示されています。そして最後に、アセンブリが見つからなかったことが明言されています。このように、Fusion ログにはバインドの失敗のデバッグに役立つ情報が含まれています。
.NET Framework 4.0 では、Binder は部分的なバインドに関して明示的に警告を行います。図 3 に、部分的なバインドに関係する Fusion ログの抜粋部分を示します。
図 3 部分的なバインドに関するログ エントリ
** Assembly Binder Log Entry (1/7/2009 @ 10:17:05 PM) **
The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.
Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework64\v4.0.AMD64chk\clr.dll
Running under executable E:\tests\LoggingDCR\PartialNames\LoadPartial.exe
--- A detailed error log follows.
=== Pre-bind state information ===
LOG: User = SampleUser
LOG: DisplayName = Lib
(Partial)
WRN: Partial binding information was supplied for an assembly.
WRN: A partial bind occurs when only part of the assembly display name is provided.
WRN: This might result in the binder loading an incorrect assembly.
WRN: It is recommended to provide a fully specified textual identity for the assembly,
WRN: that consists of the simple name, version, culture, and public key token.
WRN: See whitepaper https://go.microsoft.com/fwlink/?LinkId=109270 for more information and common solutions to this issue.
WRN: Detected case of partial bind:
WRN: Assembly Name: Lib | Domain ID: 1
ここでは、アセンブリに対して部分的なバインドの情報が指定されていたことが警告されています。次に示すように、Binder は同じアセンブリが複数のコンテキストに読み込まれた場合にも警告します。ローダーのコンテキストについては、後ほど取り上げます。
WRN: The same assembly was loaded into multiple contexts of an application domain.
WRN: This might lead to runtime failures.
WRN: It is recommended to inspect your application on whether this is intentional or not.
全体として見ると、Fusion ログはバインドの失敗をデバッグするうえでも、ランタイムがアセンブリを検出して読み込むしくみを理解するうえでも、非常に貴重なリソースであると言えます。
コンテキストについて理解する
ローダーのコンテキストとその存在理由に触れることなく Binder に関する記事を終わらせることはできません。ローダーのコンテキストは、混乱の元となることがよくあります。ローダーのコンテキストは、アセンブリを保持するアプリケーション ドメイン内の論理バケットと考えてください。アセンブリの読み込み方法に応じて、3 つのローダー コンテキストのいずれかに分けられます。
Load コンテキスト: 簡単に言うと、GAC、ApplicationBase、ApplicationBase の PrivateBinPath のいずれかに格納されており、Assembly.Load を使用して読み込まれるアセンブリは、すべて Load コンテキストに読み込まれます。AssemblyResolve イベントを使用して解決されるアセンブリも、このカテゴリに分けられます。
LoadFrom コンテキスト: ApplicationBase 外の特定のパスを指定してアセンブリの読み込みを試みたときに、アセンブリが Load コンテキストで見つからなかった場合、アセンブリは LoadFrom コンテキストに読み込まれます。
Neither コンテキスト: Assembly.LoadFile()、Assembly.Load(byte[])、または Reflection.Emit を使用してアセンブリの読み込みを試みた場合、アセンブリは Neither コンテキストに読み込まれます。
LoadFrom コンテキストに読み込まれるアセンブリの場合、Binder はまず、完全に一致するアセンブリ (ID と場所が同じもの) が Load コンテキストに存在するかどうかを確認します。そのアセンブリが存在する場合、Binder は LoadFrom コンテキスト内のアセンブリ情報を破棄し、Load コンテキストのアセンブリ情報を使用します。同じアセンブリであるかどうかを判断するうえで場所情報は重要なので、手短に説明します。.NET Framework 1.1 では、Binder が 2 つの処理 (まずアセンブリを LoadFrom コンテキストに配置し、Load コンテキスト内に一致するアセンブリ ID と場所が見つかった場合は Load コンテキストに昇格する処理) を実行するために使用していたため、場所情報は LoadFrom の 2 番目のバインドと呼ばれていました。
アセンブリは、可能な限り Load コンテキストに読み込むようにしてください。そのためには、アセンブリを GAC、ApplicationBase、または AppDomain の PrivateBinPath で見つけられるようにする必要があります。このコンテキストに読み込まれるアセンブリはそのままで NGen の恩恵を受けることができ、このコンテキスト内のアセンブリの依存関係は自動的に取得されます。
LoadFrom コンテキストへのアセンブリの読み込みには、固有の長所があります。パスを指定して、ApplicationBase 外の複数のアセンブリを読み込むことができるのです。
では、LoadFrom() を使用して読み込まれるアセンブリが Load() を使用して読み込まれるアセンブリと同じかどうかを確認しつつ、アセンブリの場所について説明します。2 つのアセンブリ内の型が同じでも、異なるパスから読み込まれた場合は、ローダー コンテキストの観点からは同一のものとは見なされません。したがって、同じアセンブリが同じアプリケーション ドメインに繰り返し読み込まれるが、読み込み先のコンテキストは異なり (Load と LoadFrom)、(アセンブリ ID の観点からは同じアセンブリであっても) Load コンテキストのアセンブリ内の型が LoadFrom コンテキストの型と同じであってはならないようなケースも考えられます。これは、LoadFrom の短所の 1 つです。また、LoadFrom コンテキスト内のアセンブリは、そのままでは NGen の恩恵を受けることができません。
Neither コンテキストについては、アプリケーションが AssemblyResolve イベントをサブスクライブしていない限り、このコンテキスト内のアセンブリへのバインドは不可能です。通常は、このコンテキストは使用しないでください。
そもそも、CLR にはなぜローダー コンテキストがあるのでしょうか。ローダー コンテキストは、アセンブリの読み込み時に読み込み順の独立性を保つのに役立ちます。また、異なるコンテキストに読む込む際に、アセンブリとその依存関係の分離手段を得ることができます。
最後の手段としての AssemblyResolve
CLR は、一連の手順 (記事「ランタイムがアセンブリを検索する方法」を参照) に従って目的のアセンブリを探し、バインドします。これらの手順をすべて実行してもアセンブリが見つからない場合、Binder は AssemblyResolve イベントを生成します。前に解決できなかったアセンブリを読み込むには、AssemblyResolve イベントをサブスクライブしてください。
.NET Framework 4.0 では、CLR は AssemblyResolve イベントを拡張し、どの親アセンブリまたは RequestingAssembly によって依存アセンブリの読み込みがトリガされたのかを示します。これは、アセンブリが他のアセンブリを参照しているときに、参照先のアセンブリに対して AssemblyResolve イベントが発生した場合に役立ちます。.NET Framework 3.5 以前のリリースには、親アセンブリ (参照元のアセンブリ) の ID を特定する手段は用意されていませんでした。
したがって、AssemblyResolve イベントが発生すると、現在のアセンブリ (Binder が通常の検索手段で見つけることのできなかったアセンブリ) とは別に親アセンブリも提供されるため、ユーザーは読み込みイベントをトリガしたアセンブリを把握できます。そしてイベント ハンドラは、渡された親アセンブリを活用できます。これにより、必要に応じて Neither コンテキストを活用し、バインドの分離を提供しやすくなります。
RequestingAssembly フィールドは、ResolveEventArgs によって公開されるもう 1 つのメンバです。
GAC を使用する状況
グローバル アセンブリ キャッシュ (GAC) は、コンピュータ全体で使用される、マネージ アセンブリのリポジトリです。GAC の主な目的は、コンピュータにインストールされている複数のマネージ アプリケーション間でアセンブリを共有できるようにすることです。たとえば、アドインを記述する際、単純にアドインの共通コードを 1 つのアセンブリに配置し、そのアセンブリを GAC に配置することができます。アドインの開発者は、(自分が記述した) すべてのアドインにこのアセンブリを共有させることができるようになりました。新しいアドインのインストールのたびに共有コンポーネントを再展開する必要はありません。GAC には、提供元の一元化という利点もあります。つまり、アドインの開発者は GAC に格納されている共通のアセンブリを提供できます。提供する更新プログラムをアドインごとに再展開する必要はありません。GAC を活用できるのはアドインの開発者だけではありません。すべてのマネージ開発者がアセンブリを GAC にインストールし、アプリケーション間で共有することができます。
では、アセンブリをアプリケーションの一部として (ApplicationBase 内に) 残す代わりに、GAC にインストールする必要があるのは、どのような場合でしょうか。複数のアプリケーション間で共有し、一元的に提供する必要のあるアセンブリがある場合に、GAC に配置することを検討してください。通常、共有されるフレームワークやコンポーネントがこのカテゴリに分けられます。
アセンブリを GAC に配置すべきではないのは、どのような場合でしょうか。アプリケーションを異なるコンピュータに xcopy で展開 (アプリケーションを含むディレクトリを xcopy コマンドでコピーして展開) できるようにする必要がある場合は、アセンブリを GAC に配置するのはおそらく最善の策ではありません。このようなシナリオでは、GAC 内のアセンブリもコンピュータ間で移動させる必要があります。
いずれの場合も、Binder は "優先されるのは常に GAC" というポリシーに従います。これは、アセンブリを GAC に配置する必要があるかどうかを判断する際に役立ちます。
余談ですが、GAC の扱いに慣れていない場合は、GAC に対しアセンブリのインストール、アンインストール、および一覧表示を行うことのできる GACUtil というツールを使用することをお勧めします。このツールは、.NET Framework SDK の一部として提供されます。詳細については、オンラインで「グローバル アセンブリ キャッシュ ツール (Gacutil.exe)」を参照してください。
.NET Framework 4.0 では、GAC にいくつかの変更が加えられています。アセンブリをグローバル ディレクトリに配置するという考え方は、CLR v1.1 で導入されました。.NET Framework 1.1 (CLR 1.1 が含まれる) と .NET Framework 2.0 (CLR 2.0 が含まれる) では、GAC は各 CLR 用に 2 つに分割されていました。こうすることで、異なるバージョンの CLR 間でアセンブリがリークするのを防いでいたのです。たとえば、.NET 1.1 と .NET 2.0 が同じ GAC を共有していたとしたら、.NET 1.1 アプリケーションがこの共有 GAC からアセンブリを読み込むときに、.NET 2.0 アセンブリを取得してしまい、結果として .NET 1.1 アプリケーションが動作しなくなる可能性があります。
.NET Framework 2.0 と .NET Framework 3.5 で使用される CLR のバージョンは、2.0 です。したがって、前の 2 つのフレームワーク リリースでは、GAC を分割する必要がありませんでした。古いアプリケーション (この場合は .NET 2.0) が動作しなくなるという問題は、.Net Framework 4.0 においても CLR 4.0 がリリースされた時点で再浮上します。そこで、CLR 2.0 と CLR 4.0 間の干渉の問題を避けるために、GAC はランタイムごとにプライベート GAC に分割されています。
GACUtil や Shfusion などのツールの動作は、.NET Framework 4.0 より前のシナリオでの動作とまったく同じです。また、発行者ポリシーの機能も変わりません (これらのポリシー ファイルは必ず GAC にインストールする必要があるため、あえて触れておきます)。主な違いは、CLR 2.0 アプリケーションで GAC 内の CLR 4.0 アセンブリを参照できないという点です。
締めくくりに、CLR Binder を最大限に活用するための方法と、.NET Framework 4.0 の Binder における機能強化点をいくつか紹介しました。
ご意見やご質問は clrinout@microsoft.com までお送りください。
Aarthi Ramamurthy は、マイクロソフトで CLR のプログラム マネージャを務めており、主にランタイムのアセンブリのバインドと読み込みに関する領域を担当しています。連絡先は aarthi@microsoft.com (英語のみ) です。
Mark Miller は CLR チームのテスト担当ソフトウェア開発エンジニアであり、フレームワークにおける多くのアンマネージの領域に取り組んでいます。連絡先は markmil@microsoft.com (英語のみ) です。