CLR 徹底解剖
.NET アプリケーションの拡張性、第 2 部
Jack Gudenkauf and Jesse Kaplan
コードのダウンロード : :
AddIns2007_03.exe
(177 KB)
Browse the Code Online

コンテンツ
前回のコラムでは、Base Class Library (BCL) に新しく System.AddIn 名前空間を導入し、ホスト アプリケーション全体に拡張性を持たせるためにユーザーが取り組む代表的なプロセスを調べました。また、包括的なアドイン ソリューションを提供するのに必要な機能を説明し、サンプル ホスト コードや、ホストで使用する新しい System.AddIn API についても見てきました。コラムの最後には、サンプル アドイン コードをいくつか紹介し、アドインの開発者は、ホストが提供するオブジェクト モデルを利用する際に、基になるメカニズムをまったく意識する必要がないことを明確に示しました。
前回は、System.AddIn パイプラインの説明を次回に持ち越し、ホストとアドインのバージョンが個別に変化したときに、互換性を確保できるモデルの利用について説明することを約束しました。今回は、まず、このモデルについて詳しく説明します。次に、先月レビューしたアプリケーションのバージョン 2.0 を見ていきます。ここでは、このバージョンを使用してパイプラインのしくみを詳しく調べ、このシステムを使用して、ホストが変更になってもアドインが機能し続ける方法を例を挙げて示します。
今回のコラムを読み進める前に、[用語] リンクを見ておくと役に立つかもしれません。また、System.AddIn 名前空間とその関連クラスは Visual Studio
® 2005 の次にリリース予定の Visual Studio (コードネーム "Orcas") に含められる予定です。この新しいライブラリは、Visual Studio "Orcas" の完全版がリリースされる前に、Community Technology Preview を通じてダウンロードできます。詳細については、CLR アドイン チームのブログ (
blogs.msdn.com/clraddins) (英語) を参照してください。
パイプライン アーキテクチャ
図 1 に、ホストとアドインの通信を可能にするパイプライン コンポーネントを示します。パイプライン コンポーネントは、アドインのアクティブ化とアドインとの通信に使用されます。
図の中の各コンポーネントは、実際には多種多様なアセンブリです。つまり、ホストとアドインの間で交換される複雑なオブジェクトの種類ごとに、境界の両側に対応するビュー、コントラクト、およびアダプタの各種類があります。図は分離境界を挟んで完全に左右対称になっていることに注意してください。このモデルに従った場合の一方の側を 図 2 に示します。ホストとアドインは、実際の実装を提供し、ユーザーに値を示す実体です。こうしたモデルの高度な目標の 1 つ、およびこのような設計パターンを使用する主な動機は、境界の一方の側または両側で変更が行われた場合でも、各コンポーネントが相互に通信および相互作用できるようにすることです。
図 1 ホストとアドイン間の通信パイプライン (画像を拡大するには、ここをクリックします)
図 1 は、重要な事実を数多く示しています。まず、ホストとアドインは分離境界で相互に切り離されています。それぞれは自身のビューしか参照しないので、通信に使用するコントラクト上に抽象層がもうけられます。さらに、ホストとアドイン自体には直接参照がないので、ホストまたはアドインを、通信を行う他のすべてのコンポーネントから相対的に分離できます。
図 2 ホスト/アドイン モデル (画像を拡大するには、ここをクリックします)
パイプライン コンポーネント
コントラクト : コントラクト アセンブリには、バージョン管理されない型 (コントラクト) を含みます。コントラクトは、分離境界をまたぐ通信メカニズムとして、その境界の両側に読み込まれます。ここでは、境界をまたぐすべての通信をコントラクトに制限することで、ホストとアドインの実装の形式が境界を越えて相手側に影響し、バージョン管理の問題の原因になることを防いでいます。このコラムのシステムでは、すべてのコントラクトが、"オブジェクト" 型として機能する基本 IContract から継承されます。コントラクト アセンブリの変更は許可されていないことに注意してください。ただし、ホストとアドインは通信に異なるコントラクトを使用でき、どちらの実体もそのことを認識しません。COM に精通していれば、次のコードを見ると、IContract の機能と IUnknown の機能 (QueryInterface、AddRef、および Release) が似ているのがわかります。
public interface IContract
{
int AcquireLifetimeToken();
int GetRemoteHashCode();
IContract QueryContract(string contractIdentifier);
bool RemoteEquals(IContract contract);
string RemoteToString();
void RevokeLifetimeToken(int token);
}
コントラクトは IContract から継承し、インターフェイスにする必要があります。そのため、固有の実装は許可されません。コントラクトは、統合と実装の形式を分離します。コントラクトのシグネチャは、mscorlib で定義されているシリアル化可能な型 (Int32, String、DateTime など)、他のコントラクト、およびコントラクト アセンブリで定義される列挙値に制限されます。また、コントラクト インターフェイスは Windows® Communication Foundation のコントラクト モデルとの調整も行い、このような形式での実装がないことを保証し、多重継承を許可します。
ビュー : ビューは、ホストとアドインのビューをそれぞれ表す抽象クラスと、ホストとアドイン間を流れる型を表す抽象クラスを含むアセンブリです。ホストとアドインに、両者間の通信に使用するコントラクトに依存しない安定した見方を与えることがビューの目的です。また、ビューによってホストとアドインのそれぞれの実装が境界の両側に分離されます。図では、ホストとアドインは自身のビューのみに依存し、ビュー自体には依存関係がないことが示されています。これにより、ホストやアドインに影響を与えずに、アダプタやコントラクトを取り替えることができます。
アダプタ : アダプタ アセンブリには、ビューとコントラクト間の変換に使用するアダプタ クラスを含みます。図の中ではアダプタが境界のどちら側に存在するかを説明するために、"側" という言葉を大まかに使用しています。実際には、呼び出しの方向、つまりアダプタがビューからコントラクトへの変換を行うか、コントラクトからビューへの変換を行うかによって決まります。したがって、呼び出しを双方向に行う場合、つまり、ホストからアドインを呼び出し、アドインからもホストを呼び出す場合は、境界の片 "側" に 2 つのアダプタがあります。
アダプタ アセンブリのクラスで、ビューからコントラクトへの変換を行うのはビューからコントラクトへのアダプタです。このクラスは、クラスのコンストラクタに渡されたビューを呼び出すことによってコントラクトを実装し、境界を越えてコントラクトとしてマーシャリングされます。コントラクトからビューへのアダプタは、コントラクトからビューへの変換を行うアダプタ アセンブリのクラスです。このクラスはビューから継承し、クラスのコンストラクタに渡されたコントラクトを呼び出すことによって、抽象メソッドを実装します。
図では、アダプタ アセンブリがビューとコントラクトの両方を参照していることを示しています。その結果、両者の間の接続が提供されます。アダプタは、ホストとアドイン間の接続を維持し、一方または両方に大幅な変更が加えられた場合でも機能し続けるようにする、接着剤の役割を果たします。この後、さらに詳しく見ていきましょう。
パイプラインの実装
では、サンプル コードを調べ、実装が実際にどのように動作するかを見てみましょう。今回は、前回説明したサンプル アプリケーションのバージョン 2.0 を使用して、ホストをどのように変更したか、およびホストとアドインがパイプラインでどのように繋がるかを調査します。その後、最初のバージョンのホストに対して前回ビルドしたアドインが、新しいバージョンに対しても変更しないで機能できることを保証するために、ここで紹介したモデルを使用する方法を見ていきます。
システムは、アドインをアクティブにし、そのアドインをホストに戻す際のみに関与します。これが行われたら、そこから離れ、パイプラインが型の流れを処理できるようにします。アクティベーション パスはホストとアドインの対話の最初の手順なので、詳しく見ることにしましょう。
アドインのアクティベーションは、ホストが使用可能なアドインを発見し、アクティブ化するアドインを選択した後に行われます (詳細については、ダウンロードに含まれる Program.cs、または前回のコラムを参照してください)。サンプルでは、次の簡単なコードでアクティベーションが行われます。
//Activate the selected AddInToken in a new AppDomain
Calculator calculator =
calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);
ホストの観点からは、単純に Activate を呼び出し、ビュー "Calculator" として型指定されて返されるアドインを受け取ります。ただし、この際、システムによってアドインが起動され、パイプラインに結び付けられます。
これを行うには、まず、アドインの実行場所となる AppDomain を作成し、アドイン、アドイン ビュー、アドイン側アダプタ、およびコントラクト アセンブリを、新しく作成したドメインに読み込みます。アドイン ビューは、図 3 ではアドイン ベースとして示されており、パイプラインがアクティベーションに使用するアドイン ビューを指定するために使用する単なる属性です。次に、システムは既定のコンストラクタとアドイン アダプタを使ってアドインのインスタンスを作成し、アドイン ベースとして型指定されたアドインをアダプタのコンストラクタに渡します。その後、コントラクト (この場合は ICalculatorContract2) として型指定されたアドインを AppDomain 境界を越えて渡し、ホスト側アダプタ アセンブリを読み込みます。さらに、コントラクトとして型指定されたアドイン アダプタをホスト アダプタのコンストラクタに渡して、ホスト アダプタのインスタンスを作成します。最後に、ホスト アドイン ビュー (この場合 Calculator) として型指定されたホスト側アダプタをホストに返します。
図 3 アクティベーション パス (画像を拡大するには、ここをクリックします)
これには多くのオーバーヘッドが生じるように思えますが、このモデルを使用して境界を越えて通信を行う場合のパフォーマンス コストは、実際には、境界を越えてインターフェイス上で直接通信を行う場合に比べても最小限に抑えられます。
アドインのアクティベーション パスを見てみると、アクティベーションは、実際にはアドインからホストに渡されているオブジェクトの一例にすぎないことがわかるでしょう。唯一の違いは、システム自体がアドインのインスタンスを作成し、コンポーネントに接続する点です。したがって、システムが接続するコンポーネントを識別し、コンポーネントのインスタンスを正しく作成するために、アクティベーションに関与する各コンポーネントは、コンポーネントの識別に適用されるカスタム属性を備えている必要があります。一般に、アクティベーションには各パイプライン コンポーネント内の 1 つの型が関与するので、コンポーネントに適用される属性の中から 1 つを備えることになります。ただし、唯一の例外はホスト ビューです。ホスト ビューは、API を通じて直接システムに提供されるので、属性を備える必要はありません。
コラムの例では、アドインは自身をサービスとしてホストに提供しています。そのため、アドインとホスト間で受け渡されるデータ型は double と string のみです。このため、サンプルにはアクティベーション パスに関与する型しか含まれていません。複雑なデータをホストとアドイン間で双方向に受け渡す例については、チームのブログを参照してください。
ホストとホスト ビューを変更する
前述のように、ホスト アプリケーションには、サービスを提供するアドインが必要でした。具体的には、アドインは Add メソッド、Subtract メソッド、Multiply メソッド、および Divide メソッドの形式で、基本的な電卓機能を提供する必要があります。
アドイン本来のホストのビューは次のようなものでした。
public abstract class Calculator {
public abstract double Add(double a, double b);
public abstract double Subtract(double a, double b);
public abstract double Multiply(double a, double b);
public abstract double Divide(double a, double b);
}
ただし、新しいバージョンでは、ホストはアドインから前回よりも多くの機能を要求し、電卓サービスが提供する機能に柔軟性を持たせることにしました。そのため、アドインの新しいホストのビューを次のように定義しました。
public abstract class Calculator {
public abstract string Operations { get; }
public abstract double Operate(
string operation, double a, double b);
}
ホストは、アドインの 4 つの特定の演算に依存するのではなく、電卓がサポートする演算の種類を認識し、こうした演算を実行できるようになりました。この種の変更は、あるサービスをアドインに依存しているホストでは一般的です。つまり、ホストは時間の経過と共にアドインからの追加機能が必要になることを検出するか、拡張ポイントに柔軟性を持たせることを決めます。このコラムのモデルでは、新しく更新されたビューに対してのみプログラミングを行い、この新しいビューにレガシ アドインを接続する場合はパイプラインに依存します。
サンプルでは、HostCalculatorView プロジェクトの CalculatorView2.cs 内に新しいビューの定義があります。ホストからこの新しいビューを使用する方法の詳細については、SampleExtensibleCalculator.csproj 内の Program.cs を参照してください。
ここでのカスタム コントラクト (ICalculatorContract2) は、分離境界をまたぐ通信に使用する型です。元のコントラクトは次のようになっていました。
[AddInContract]
public interface ICalculatorContract : IContract
{
double Add(double a, double b);
double Subtract(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b);
}
今回はアドインに関するホストのビューが変更され、別のコントラクトに移行されて、次のコントラクトを使用するようになりました。
[AddInContract]
public interface ICalculatorContract2 : IContract
{
string GetAvailableOperations();
double Operate(String operation, double a, double b);
}
ホスト ビューとホスト ビューに行われた変更は、通常、コントラクト内でミラー化されます。
アドイン ビューは、通常、ホスト ビューをミラー化し、コントラクトに行われたのと同じ方法で変更されます。このサンプルでアドイン ビューとホスト ビューが区別される点は、各ビューが含まれるアセンブリと、AddInBase (アクティベーションに使用される型) とマークされた属性のみです。
[AddInBase]
public abstract class Calculator2
{
public abstract string Operations{ get; }
public abstract double Operate(
string operation, double a, double b);
}
アドインは、アドイン基本クラスから継承し、アクティベーション時にアドインを特定するために属性を適用する必要があります。ダウンロードに含まれる完全な実装のサンプル コードでおわかりのように、アドインの定義は次のようになっています。
[AddIn("Basic V2 Add-in", Version="2.0.0.0")]
public class SampleV2AddIn : Calculator2
{
public override string Operations
{
get { ... }
}
public override double Operate(
string operation, double a, double b)
{
...
}
}
サンプル コードでは、アドインはホスト アプリケーションにサービスを提供しています。そのため、ホストの機能を拡張するには、ホストからアドインを呼び出す必要があります。このシナリオでは、アドイン側アダプタがビューからコントラクトへのアダプタの役割を提供しています。
図 4 に示すサンプル コードは CalculatorView2ToContract2AddInAdapter.cs にも含まれており、ダウンロードに含まれる AddInSideAdaptersV2.csproj プロジェクトでも確認できます。AddInAdapterAttribute は、アドイン アダプタ クラスを検索してアクティブ化するために、System.AddIn.Hosting API で使用される属性です。

Figure 4 CalculatorViewToContractAddInAdapter
[AddInAdapter]
public class CalculatorViewToContractAddInAdapter : ContractBase,
ICalculatorContract2
{
private Calculator_view;
public CalculatorViewToContractAddInAdapter(
Calculator2 calculator) {
view = calculator;
}
public string GetAvailableOperations(){
return _view.Operations;
}
public double Operate(string operation, double a, double b){
return _view.Operate(operation, a, b);
}
}
アドイン アダプタは ContractBase クラスから派生します。このクラスは、System.AddIn.Pipelineで提供される基本クラスです。このクラスは、MarshalByRefObject、IContract、および ISponsor から派生します。ISponsor とホスト アダプタの有効期間管理については、この後説明します。アドイン アダプタも、カスタム コントラクトである ICalculatorContract から派生します。このコントラクトを使用して、新しいアドインを電卓の GetAvailableOperations メソッドと Operate メソッドに提供します。これらのメソッドはどちらもアドインのバージョン 2.0 (SampleV2AddIn.cs) で実装されます。アドイン アダプタ コンストラクタでは、アドイン (SampleV2AddIn) をアドイン ビュー (CalculatorContractsBase.Calculator) として取り込みます。コンストラクタは、(アドイン ベース Calculator 2 として型指定された) アドインを呼び出すことによって、ICalculatorContract2 の必要な機能を実装します。
サンプル コードでは、アドインから提供されたサービスを使用しているため、ホストの機能を拡張するために、ホストからアドインを呼び出す必要があります。このシナリオでは、ホスト側アダプタがコントラクトからビューへのアダプタの役割を提供しています。ホストは、アドイン アダプタから提供された機能を経由して呼び出しを行います。図 5 のコード (HostSideAdapters.csproj プロジェクトの CalculatorContract2ToView2HostAdapter.cs) がこの動作を示しています。

Figure 5 CalculatorContractToViewHostAdapter
[HostAdapter]
public class CalculatorContractToViewHostAdapter : Calculator
{
private ICalculatorContract2 _contract;
private System.AddIn.Pipeline.LifetimeTokenHandle _handle;
public CalculatorContractToViewHostAdapter(
ICalculatorContract2 contract)
{
_contract = contract;
_handle = new LifetimeTokenHandle(contract);
}
public override string Operations {
get { return _contract.GetAvailableOperations(); }
}
public override double Operate(string operation, double a, double b) {
return _contract.Operate(operation, a, b);
}
}
この例では、ホスト アダプタ クラス (CalculatorContractToViewHostAdapter) を検索してアクティブ化するために、System.AddIn.Hosting API によって HostAdapterAttribute が使用されます。ホスト アダプタは、ホスト ビューから派生します。その名前からもわかるように、ホスト ビューはホストからのアドインのビューです。ホスト アダプタ コンストラクタでは、カスタム コンストラクタ (この場合は ICalculatorContract2) として型指定されたアドイン アダプタを取り込みます。
ホスト アダプタのコードとアドイン アダプタのコードはよく似ていることがわかります。主な違いは、一方がコントラクトからビューに変換するのに対して、もう一方がビューからコントラクトに変換する点です。ホスト アダプタでは、コンストラクタにコントラクトを取り込んで、ビューから継承するのに対して、アドイン アダプタではその反対のことを行います。ここに示したコードの中で他に興味深いのは LifetimeTokenHandle のみでしょう。これについては、この後説明します。
ホストはアドインを経由して機能を呼び出すため、アドインの AppDomain に常駐するリモート オブジェクトへの参照を保持する必要があります。そのため、オブジェクトの有効期間は、アドインではなく、ホストが判断する必要があります。ここでは、リモート処理を基にビルドした簡単なメカニズムを用意しています。このメカニズムでは、アドインがアドインの AppDomain に用意したオブジェクトへの参照をホストが保持している間はそのオブジェクトが存続していることを保証します。ホストがこの参照を解放すると、ガベージ コレクタ (GC) によってホストが参照していたオブジェクトのガベージ コレクションが行われ、リモート オブジェクトのリース期間が満了します。こうした有効期間管理のメカニズムによって、ホストとアドインの基になるメカニズムから複雑性が排除されます。アドインの開発者もホストの開発者も、使用および提供しているオブジェクト (ビュー) が、リソース用のメモリを備え、GC によって管理されるローカル リソースであるかのようにプログラミングを行うことができます。
こうした参照メカニズムは、ホストに実装される形式でも、アドインに実装される形式でも機能します。アドインの有効期間を判断する場合も同じメカニズムを使用します。ホストがアドイン (オブジェクトの 1 つにすぎません) への参照を解放すると、アドインがアンロードされます。そのアドインが、ホストが所有する AppDomain で実行されている最後のオブジェクトの場合は、パイプラインによって AppDomain がアンロードされます。
CalculatorContract2ToView2HostAdapter.cs 内の以下のサンプル コードは、有効期間管理が驚くほど容易に実装されることを示しています。
private System.AddIn.Pipeline.LifetimeTokenHandle _handle;
_handle = new System.AddIn.Pipeline.LifetimeTokenHandle(contract);
LifetimeTokenHandle クラスは、境界を越えて受け渡されるオブジェクトの有効期間を管理するために、IContract を実装するクラス (ほとんどの場合はシステム提供の ContractBase クラス) と連携して機能します。
ContractToViewAdapter (コントラクトの受け取り側) で LifetimeTokenHandle クラスのインスタンスが作成されるときに、コントラクトから LifetimeToken を取得します。GC では、そのアダプタへの参照が存在しなくなったと判断すると、オブジェクトのファイナライズを行ってから、LifetimeToken を破棄します。一方 ViewToContractAdapter (コントラクトの送信側) では、アダプタが未解決の LifetimeToken を監視し、参照カウントがゼロ (0) になった時点で、自身のガベージ コレクションを行うことができます。さらに、これがアドインの AppDomain で、ホストからそのドメインへの参照が他にない場合は、AppDomain がアンロードされます。
ホストとアドインの互換性
ホスト (ホスト ビュー) またはそのデータ型のバージョンが変化すると、古いアドインを新しいホスト ビューで機能させるために、ホストでは 2 つ目のアドイン側アダプタを作成します。このアダプタは、新しいコントラクトから継承し、新しいデータ型を理解し、そのデータ型を元のアドイン ビューに変換します。ホストまたはパイプラインの開発者は、新しいメソッドに既定の機能を提供する必要があります (図 6 参照)。
このようなアダプタのサンプル コードは、ダウンロードに含まれる AddInSideV1toV2Adapter.csproj にあります。実装方法の完全な詳細については、サンプルを参照してください。ただし、重要な部分はクラスとクラスのコンストラクタの定義です。
図 6 新しいホストと古いアドインの下位互換性 (画像を拡大するには、ここをクリックします)
図 7 で重要なのは V1 アドインを V2 コントラクトに適合させている部分です。ここでは V2 コントラクト (ICalculatorContracts2) から継承し、V1 アドイン ベースをコンストラクタ (Calculator) に取り込むアドイン アダプタをビルドする必要があるだけです。

Figure 7 CalculatorV1ViewToV2ContractAddInAdapter
[AddInAdapter]
public class CalculatorV1ViewToV2ContractAddInAdapter : ContractBase,
ICalculatorContract2
{
CalculatorContractsBase.Calculator _view;
public CalculatorV1ViewToV2ContractAddInAdapter(Calculator calc)
{
_view = calc;
}
public string GetAvailableOperations()
{
return "+, -, *, /" ;
}
public double Operate(string operation, double a, double b)
{
switch (operation)
{
case "+": return _view.Add(a, b);
case "-": return _view.Subtract(a, b);
case "*": return _view.Multiply(a, b);
case "/": return _view.Divide(a, b);
default:
throw new InvalidOperationException(
"This add-in does not support: " + operation);
}
}
}
この方式のメリットは、変更があっても古いアドインが引き続き機能し、新しいアドインが新しいオブジェクト モデルをビルドおよび利用できる点です。アプリケーション自体は異なるバージョンのアドインを監視する必要はなく、ホストはホスト ビューを 1 つだけ処理します。バージョンごとに異なるアドイン アダプタがホスト ビューを別のバージョンのアドインに変換します。これにより、ホストのコード ベースが非常に単純になり、ホストにはレガシ バージョンのアダプタを単に同梱しないことで、そのバージョンへのサポートを削除できます。
まとめ
このモデルを設計するにあたって目標にしたことは、バージョンの変化に柔軟に対応できる、分離可能なコンポーネントを動的に構成できるようにすることでした。アドイン開発者の作業が、この目標の達成に関係するしくみによって煩雑になることを確実に回避することに、主に注意を払いました。
System.AddIn モデルは、System.AddIn 名前空間にある BCL と、コンポーネント間の通信に適用される設計パターンのガイダンスから構成されます。今月のコラムでは、パイプライン アーキテクチャ、パイプラインの実装、有効期間管理、およびこのモデルを使用してホストとアドインのバージョンを独立させ互換性を確保する方法について説明しました。
ご意見やご質問は、
James clrinout@microsoft.com..
までお送りください。
Jack Gudenkauf は、Microsoft の CLR チームに所属するソフトウェア アーキテクトです。現在、.NET アーキテクチャの採用に関する問題を確認するため、ISV/VAR/SI のお客様の応対に精力的に当たっています。Jack は Development Manager、MIS Director、ソフトウェア エンジニアリング、およびコンサルティング サービスなどの役職、業務に携わっています。Microsoft 入社前は、自身で起業したコンサルティング会社 The Logical Choice を経営していました。
Jesse Kaplan は、Microsoft で CLR チームの Program Manager for Extensibility and Add-Ins を務めています。これまでの経験としては、互換性とマネージ/ネイティブ相互運用性などがあります。