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

コンテンツ
Microsoft .NET Framework により、さまざまな開発者とベンダによって構築されたコンポーネントを選んで自分自身のアプリケーションに統合することが、非常に簡単になりました。とは言え、簡単になるのは、構築対象のコンポーネントを知っている場合に限られます。構築時にコンポーネントのことを把握していないなら (これは実は、アドインではよくあることです)、状況ははるかに難しくなります。開発者がよく疑問に思うのは、いつアプリケーションを拡張したらよいのだろう、ということです。例えば、アドインはデータベースに格納した方がよいのでしょうか、それともディスク上の方がよいのでしょうか。アクティブ化の型を取得するために、既知のインターフェイスにアドインを追加することについて検討する必要があるでしょうか。AppDomain、AppDomainManager、および AppDomainSetup を使用するための最善の方法は何でしょうか。
私たちは、新しい System.AddIn 名前空間を Base Class Library (BCL) に導入するにあたり、「CLR 徹底解剖」の 2 回の連載記事でこれらの問題やその他の質問を扱います。このライブラリは、Visual Studio
® の次のリリース (コードネーム "Orcas") で利用可能になります。このライブラリは、Community Technology Preview を通じて、完全リリース版の Visual Studio "Orcas" より先にダウンロードできます。また、
共通言語ランタイム (CLR) アドイン チームのブログ (英語) でも、情報を見つけることができます。
このシリーズのコラムでは、アプリケーションに拡張性を追加した後、長期にわたってバージョン管理を行う、という開発ライフサイクルを詳しく説明します。この第 1 回目のコラムでは、初めてアプリケーションに拡張性を追加するときに開発者が直面するすべての課題、および私たちの API を使用することにより開発者の作業がどのように簡単になるのか、について説明します。来月は、ホストの環境が変化してもアドインの機能を維持する、という比較的長期にわたる問題に焦点を合わせます。このコラムで作成したサンプル アプリケーションを取り上げて、次のバージョンに向けて大幅に手を加えます。そして、元のアドインの機能を維持するのに必要な手順を詳しく説明します。
それでは、最初は拡張性の課題と API の利点に関する話から始めましょう。System.AddIn 名前空間が追加され、マネージ アドイン モデルを構築するためのガイダンスがあるので、CLR では、アプリケーションの最初のバージョンに起こりがちな一般的問題のソリューション、およびアプリケーションの将来の互換性を確保するためのガイダンスを提供します。コラムで使用する用語をより深く理解するため、[拡張性関連用語] サイドバーを参照してください。
拡張可能ホストを構築する
ホストのオブジェクト モデルの定義が公開された後には、ホストとアドインの対話に 3 つの主要なフェーズがあります。それは、探索、アクティブ化、および有効期間管理です。探索には、利用可能なアドインを見つけて、どのアドインを使用したらよいのかを選択するための情報をホストまたはユーザーに十分に提供することが関係します。アクティブ化には、適切に分離されているセキュリティ サンドボックスに選択したアドインを読み込んで、起動することが関係します。有効期間管理とは、アプリケーションがアドインへの参照を保持している限り、アドインおよびアドインがホストと交換するオブジェクトを有効に保つ一方で、必要なときにアドインをすぐにシャットダウンする能力をホストに与えることを意味します。特に記載していない限り、ここのコードのすべては、Program.cs ファイルの一部です。これは、MSDN®Magazine の Web サイトからダウンロードできる SampleExtensibleCalculator.csproj に含まれています。
実行時のアドイン使用に関する詳細を説明する前に、SDK について、およびホストがアドインに提供するオブジェクト モデルを定義することについて説明しましょう。私たちのアドイン モデルは、さまざまなシナリオをサポートしますが、それらは通常、3 つのカテゴリのいずれかに該当します。
第 1 のカテゴリでは、アドインはホスト アプリケーションに対するサービスを提供します。たとえば、メール サーバーは、ウイルス スキャン、スパム フィルタリング、または IP 保護のためにアドインをホストすることがあります。ワード プロセッサには、スペルチェック用アドインが必要とされる場合があります。Web ブラウザは、ある種のファイルを処理するためにアドインをホストすることがあります。計算機の例は、このシナリオを示したものです。SampleCalculatorAddIn.cs のアドイン コードを見ると、アドインが Add、Subtract、Multiply、および Divide のサービス実装をホストに提供していることがわかります。
第 2 のカテゴリでは、ホストは動作をアドインに提供して、ホストを自動化する方法をアドインに定義させます。このシナリオでは、ホストはアドインに対するサービスを実際に提供します。Microsoft® Office のアドインの大半は、このカテゴリに属します。つまり、アドインを初期化するとすぐに、Office アプリケーションはルート オブジェクトをアドインに渡し、ホストを自動化できるようにします。
このモデルを使用して、ホストはアプリケーションが考えもしていなかった方法で、サードパーティにアプリケーションを拡張させることができます。これまでに、テキスト シンボルをリアルタイムの株価情報に自動的に置換する Microsoft Excel® のアドインや、Voice over IP (VoIP) プログラムをスタートアップして電話番号に電話をするドキュメントに、電話番号への仮想リンクを追加する Microsoft Word のアドインが公開されています。各種アプリケーション全体が、既存の 1 アプリケーションに対する 1 つの巨大なアドインとして構築されていることもあります。クライアント全体のフロントエンドが、Microsoft Outlook® に取り込まれているような、CRM (カスタマ リレーションシップ マネジメント) アプリケーションもいくつかあります。
最後のカテゴリは、ホスト特有の機能を使用するのではなく、ホストを主にスクリーン上での言わば定住先として使用するアドインです。一例としては、検索ツール バーがあります。つまり、このツール バーは、自己をブラウザ、Windows タスクバー、および電子メール クライアントに追加しながらも、ホストにかかわらず同じ機能を提供します。
サンプル アプリケーションでは、サービスとしてアドインを使用するために、ホストは SDK をアドインに提供します。このケースでは、ホストは簡単な計算機機能を実装するためにアドインを使用します。私たちのモデルでは、オブジェクト モデルには 3 とおりの非常に似通った定義があります。私たちは、これらのコンポーネントをいわゆる “パイプライン” で定義しています。つまり、ホストのビュー、アドインのビュー、および分離境界を越えるコントラクトです。パイプラインについては後でさらに詳細に説明することにして、今はホストのビューを見てみましょう。
次のコードは、ホストのコアなコードではなくホストのビューの一部なので、hostCalculatorView.csproj の CalculatorView.cs で見つけることができます。
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);
}
見ればおわかりのように、ホストのビューは、アドインに期待される機能を定義する抽象的な基底クラスの一種にすぎません。ホストは、このビューに対してプログラムを直接作ります。そして、ホストとそのアドインとの間に分離境界があること、およびアドインにホストの非常に異なったビューがあるかもしれないことには、決して気付かないのです。
探索
アプリケーションでアドインを使用する際の最初の手順は、アドインの探索です。アプリケーションには、利用可能なアドインを見つけて、どれを読み込んでアクティブ化したらよいのかを決めるための情報を十分に取得する方法が必要です。一般に、アプリケーションに応じて、ホストはアプリケーションのスタートアップ時、ドキュメントが読み込まれるとき、またはユーザーの要求に従い、この手順を実行します。
ハイレベルな検出要件の一部としては、指定された場所で特定の種類のアドインを見つける能力、ホストまたはユーザーが、最初にアドインをアクティブ化しなくても、どのアドインを使用するのかを自己決定できるほど十分な情報を提供すること、利用可能なアドインのリストを再計算しなくても済むように、賢くキャッシュすること、およびアドインがインストール時にキャッシュを更新できるようにすること、などがあります。これらの目標を達成するために、私たちがとった方法を紹介しましょう。最初に、アドインを探す場所を定義します。このサンプルでは、AddIns とコンポーネントはカレント ディレクトリにインストールされることを想定していますが、アプリケーションがいくつかの別の場所 (たとえば、ユーザーのアプリケーション データ ディレクトリ) を選ぶこともよくあります。
String addInRoot = Environment.CurrentDirectory;
次に、アドイン ストアが新たにインストールされたアドインとパイプライン コンポーネントを認識できるように、アドイン ストアを更新します。AddInStore クラスは、何が何に接続できるのかを決定する際に使用されます。この情報が、いわゆる “パイプライン” を作ります。
ストアには、ホストとそのアドインとの間のパイプラインを作るすべてのコンポーネントが含まれます。AddInStore は、AddInStore.Update API を使用して実行時に更新されることもあれば、コマンドライン ツールを使用してアドインのインストール時に更新されることもあります。その目的は、探索のスタートアップ パフォーマンスを強化することです。次のコードは、新しい AddIns がインストールされたかどうかをチェックします。これが最後に呼び出された後に、新しいアドインやパイプライン コンポーネントがインストールされていない場合は、すぐに呼び出し元に戻されます。
AddInStore.Update(addInRoot);
いったんキャッシュを更新すると、ホストは自己が構築したビュー (抽象的な基底クラス) に接続できるすべての利用可能なアドインを見つけようとします。このコードは、ルート ディレクトリに Calculator AddIns がないかを探し、結果を格納します。
Collection<AddInToken> tokens =
AddInStore.FindAddIns(typeof(Calculator), addInRoot);
AddInStore.FindAddIns メソッドは、探しているアドインの型、およびアドインが保存されている場所のパスを受け取って、AddInTokens のコレクションを返します。この時点では、アドイン アセンブリはまったく読み込まれておらず、アドイン コードは 1 つも実行されていないことに注意してください。各トークンは、一意のアドインと 4 つのプロパティ (名前、バージョン番号、発行元、および説明) について説明しています。これらは、読み込むアドインをアプリケーションまたはユーザーが決定できるように、アドインによって提供されます。今取り組んでいる計算機のシナリオで、ユーザーにトークンを表示する ChooseCalculator メソッドを構築しました。これにより、ユーザーは希望するものを選択できます。
AddInToken calcToken = ChooseCalculator(tokens);
アクティブ化、分離、セキュリティ、およびサンドボックス化
アプリケーションは、使用するアドインを決めたら、次はそのアドインをアクティブ化する必要があります。ホストは、ホストとしての観点からすると、要求された抽象的な基底クラス (すなわちビュー) を実装するインスタンスを必要とします。しかし、実際には、ホストが必要とするのはこれだけではないのが一般的です。つまり、ホストや他のアドインから分離されたアドインが必要ですし、そのアドインを特定のアクセス許可セットでサンドボックス化できる機能も必要です。
分離されたアドイン アセンブリは、別のアセンブリ内のコードとリソースに直接アクセスできません。.NET Framework では、分離のインプロセス ユニットとして AppDomains を使用します。AppDomains に慣れていない場合、AppDomains を軽量のプロセスとして捉えることができます。一般に、ホストは 1 つの AppDomainでコードを実行します。これは、ランタイムのスタートアップ時に作成される既定の AppDomain であるのが普通です。そして、ホストは各アドインが独自の AppDomain を持つことを望みます。アドインをホストおよびお互いから分離すると、いくつかの重要な利点が得られます。
- 各アドインは、独自のフォルダに入っています。このフォルダは、独自のアプリケーション ベースを持っており、ホストおよび他のアドインと競合することなく独自のアセンブリを読み込みます。そのため、アドインは、アドイン固有の機能に使用されることがある独自の構成ファイルを持つことができるようにもなります。
- ホストは、ホスト プロセスで自己や他のアドインに影響を与えることなく、1 つのアドインに発生したエラーをキャッチして、そのアドインをシャットダウン (そして、おそらく再起動も) できます。
- このアドインとその依存関係は、アンロードできます (詳細については、このコラムの「有効期間管理」で説明します)。
- アドインを特定のセキュリティのアクセス許可セットに対してサンドボックス化することができます (詳細はすぐ後で説明します)。
しかし、ホストとアドイン自体を開発するときには、ホストとアドインのビューを作る抽象的な基底クラスに対して直接プログラムを作成しているのですから、分離境界の制約について心配する必要はないことに注意してください。このバージョンでは、AppDomain レベルの分離をサポートしており、将来のバージョンではプロセス レベルの分離を追加することも検討しています。アドインと AppDomains の詳細については、「
AppDomain's and Addin's」(英語) を参照してください。
ホストは、アドインを読み込んでアクティブ化するセキュリティ コンテキストについて考える必要があります。ホスト アプリケーション自体は、完全な信頼で実行されているかもしれませんが、アドインの方はアクセス許可を下げて実行することを望む場合があります。既に存在するセキュリティと AppDomain クラスの API は、非常に強力なので、AppDomain のサンドボックス化に関してさまざまな設定を自由に調整できます。大半のケースでは、ホストは、FullTrust、イントラネット、またはインターネットのセキュリティ設定のいずれかをアドインに与えて、それが意味する内容をフレームワークに判断してもらうことを望みます。私たちの API を使用すると、これら 3 つの設定のいずれか 1 つを選ぶのは非常に簡単になりますが、必要に応じてセットアップ パラメータを微調整する機能も用意してあります。これらすべてを 1 行のコードにまとめると、次のようになります。このコードは、新しい AppDomain 内で選択された AddInToken をアクティブ化し、アセンブリをインターネット ゾーンにサンドボックス化します。
Calculator calculator =
calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);
この 1 行のコードは、選択されたアドインのアクティブ化されたインスタンスをホストに返します。それでは、このとき、背後では何が起きているのでしょうか。
AddIn フレームワークは、AppDomain を作成し、適切なアセンブリを AppDomain に読み込みます。次に、その AppDomain で実行されているコードに、指定されたセキュリティ レベル (このケースでは、インターネット アクセス許可が該当します) にふさわしいセキュリティ アクセス許容を与えます。そして、ApplicationBase を実際のアドイン アセンブリのための場所として設定し、依存関係とリソースを見つけられるようにします。次に、AppDomain の構成ファイルをアドインの DLL の隣に位置する構成ファイルとして設定します。最後に、ホストをアドインに接続するのに必要なパイプライン コンポーネントを取り付けます。
カスタムのアクセス許可セットをアドインに指定できるオーバーロードがある一方で、独自の AppDomain を提供できるようにするオーバーロードもあります。その結果、AppDomain とアドインのセキュリティ設定を微調整したり、有効期間とセキュリティ コンテキストが同じアドインをグループ化したり、AppDomain を再利用する際のパフォーマンス上の利点を有効にしたりできます。
図 1 は、これまでカバーした内容を使用するエンドツーエンドのホストを説明したものです。

Figure 1 アドインを探索からアクティブ化までホストする
static void Main()
{
// In this sample we expect the AddIns and components to
// be installed in the current directory
String addInRoot = Environment.CurrentDirectory;
// Check to see if new AddIns have been installed
AddInStore.Rebuild(addInRoot);
// Look for Calculator AddIns in our root directory and
// store the results
Collection<AddInToken> tokens =
AddInStore.FindAddIns(typeof(Calculator), addInRoot);
// Ask the user which AddIn they would like to use
AddInToken calcToken = ChooseCalculator(tokens);
// Activate the selected AddInToken in a new AppDomain set sandboxed
// in the internet zone
Calculator calculator =
calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);
// Run the read-eval-print loop
RunCalculator(calculator);
}
有効期間管理
どのアドイン モデルにも不可欠の部分は、オブジェクトの有効期間の内部管理に使用されるメソッドです。COM 有効期間管理は、参照カウントに完全に依存していましたが、CLR 有効期間管理は、ガベージ コレクタに依存しています。CLR リモートでは、スポンサ/タイムアウト アプローチを採用しています。システムは、AppDomains やプロセス境界を越える必要があります。そして、これらのシナリオでは、CLR のガベージ コレクタはもはや適切なサポートを提供できません。したがって、有効期間管理をホストとアドインの開発者に対して完全に透過的にするだけでなく、パイプライン (オブジェクト モデル) 開発者にとってできるだけ簡単にする有効期間管理のシステムを提供する必要があります。来月には、有効期間管理のソリューションを提供し、コラムでさらに詳細に説明します。
ホスト アプリケーションは、長期実行サービスであったり、メモリに制約があったり、または何かの理由で実行時にアドインをアンロードできる必要があったりする場合があります。ランタイム自体は、アセンブリが読み込まれた AppDomain をアンロードするときに、アセンブリのアンロードしか許容できません。つまり、従来の DllUnload は備えていません。これは、AppDomain 境界が値を追加するさらに別の方法です。AppDomain 境界は、プロセスを繰り返さなくても、システム リソースを再利用できるようにします。
ホストがアドインをすぐにシャットダウンする場合、どのアドインがどの AppDomain に属しているのかを追跡することを心配する必要はありません。その役割を果たす、AddInController と呼ばれるクラスを用意してあります。シャットダウンするアドインへの参照を持っている限り、わずか 1 行のコードで追跡することができます。
// Retreive the AddInController for my add-in and then shut it down
AddInController.GetAddInController(calculator).Shutdown();
私たちのモデルでは、アドインをシャットダウンするホストの能力とは別に、ホストとアドインは、自己の有効期間管理があたかもガベージ コレクタによって完全に制御されるかのように振舞うことができます。何かの開放を希望する場合には、参照を空にするか破棄するだけで済みます。実際には、背後では、参照カウント システムとランタイムのリモート サービスを組み合わせて、有効期間を管理しているのです。
アドイン開発
開発者の観点からは、アドイン システムのしくみは隠されています。ホストのオブジェクト モデルの対話、およびアドインが拡張性をホストに提供する手段は、できるだけ透過的である必要があります。私たちのモデルを使用してアドインを書くのは、非常に簡単です。アセンブリへのアドイン アセンブリ エントリを定義して、アドインについて説明的な情報をホストに提供するカスタム属性が採用されています。属性がいったん適用された後は、アドインがしなければならないのは、抽象的な基底クラスで定義されたメソッドを実装することだけです。
アドインのサンプル コード (コード ダウンロードに含まれる SampleCalculatorAddIn.csproj の一部として、SampleCalculatorAddIn.cs にあります) では、アドイン開発者向けのパイプラインのコードは含まれていないことに注意してください。たとえば、リモートと有効期間管理を処理するしくみは、省略されています (図 2)。

Figure 2 簡単な計算機のアドイン
namespace SampleCalculatorAddIn
{
[AddIn("Sample Calculator AddIn", Version="1.0.0.0")]
public class SampleCalculatorAddIn : Calculator
{
public override double Add(double a, double b)
{
return a + b;
}
public override double Subtract(double a, double b)
{
return a-b;
}
public override double Multiply(double a, double b)
{
return a * b;
}
public override double Divide(double a, double b)
{
return a / b;
}
}
}
見ればおわかりのように、アドイン モデルには、アドイン開発者へのオーバーヘッドがほとんどまったくありません。ホストに対するアドインについて記述するために開発者が必要とするのは、カスタム属性を使用してから、ホストがアドインに必要とする機能を実装することだけです。このケースでは、計算機の簡単な四則演算だけです。
まとめ
このモデルを設計するにあたって私たちが目標としたことは、バージョンの変化に柔軟に対応できる、分離可能なコンポーネントを動的に構成できるようにすることでした。アドイン開発者の作業が、この目標の達成に関係するしくみによって煩雑になることを確実に回避し、ホストの仕事をできるだけ簡単にすることに、主に注意を払いました。
来月は、パイプライン、およびホストとアドインのバージョンとしてモデルを独立的に使用して互換性を可能にする方法について、さらに詳細に説明します。それまでの間は、私たちのチーム ブログにアクセスして、その他のサンプルをご覧ください。皆様からのフィードバックもお待ちしています。
ご意見やご感想は、
こちら clrinout@microsoft.com.
まで英語でお送りください。
Jack Gudenkaufは、Microsoft の CLR チームに所属するソフトウェア アーキテクトです。現在、.NET アーキテクチャの採用に関する問題を確認するため、ISV/VAR/SI のお客様の応対に精力的に当たっています。Jack は Developer Management、MIS Director、ソフトウェア エンジニアリング、およびコンサルティング サービスなどの役職、業務に携わっています。以前は、自分で起業したコンサルティング会社 The Logical Choice を経営していました。
Jesse Kaplanは、Microsoft で CLR チームの Program Manager for Extensibility and Add-Ins を務めています。これまでの経験としては、互換性とマネージ/ネイティブ相互運用性などがあります。