次の方法で共有



December 2009

Volume 24 Number 12

CLR 徹底解剖 - インプロセス サイドバイサイド

Jesse Kaplan | December 2009

CLR チームのブログ (英語) にご質問やコメントをお寄せください。

.NET Framework 4 の開発を進める過程で遭遇した難しい問題の 1 つは、新しい機能を追加しながら、以前のリリースとの互換性を維持することでした。互換性の問題を引き起こす可能性がある変更については、承認を必要とする厳格なプロセスに従い (申請した変更のほとんどは拒否されました)、数百種類に及ぶ実際のアプリケーションを使用して互換性を調べるラボ環境を運用して、意図しない問題が発生しないか確認しました。

しかし、バグの修正には必ず、その不適切な動作に依存するアプリケーションがある、というリスクが伴います。場合によっては、プライベート API の動作や例外の説明テキストなど、アプリケーションでは使用を控えるように警告されている依存関係が利用されていることもあります。

.NET Framework が登場してからは、アプリケーションの互換性の問題について優れた解決策を手に入れました。具体的には、複数のバージョンの .NET Framework を同じコンピューターに同時にインストールできるようになりました。この機能は、2 つの異なるバージョンを対象に構築され、同じコンピューターにインストールされている 2 種類のアプリケーションを、適切なバージョンでそれぞれ実行できるようにします。

アドインの問題

各アプリケーションがそれぞれ専用のプロセスを取得する場合は適切に機能しますが、アドインはかなり厄介な問題です。たとえば、COM アドインをホストする Outlook などのプログラムを実行しているとします。これらのアドインにはマネージ COM アドインが含まれ、2 つのバージョンのランタイムがコンピューターにインストールされています。そして、各アドインはどちらかのバージョンを対象に構築されています。この場合は、どのランタイムを選択すべきでしょう。古いランタイムに新しいアドインを読み込んでも、機能しないのは明らかです。

一方、互換性が高いため、古いアドインは、通常、新しいランタイムでも問題なく実行できます。できるだけすべてのアドインが機能するように、マネージ COM のアクティベーションには常に最新のランタイムが使用されます。古いアドインしかインストールされていない場合でも、そのアドインがいつアクティブになるかを知る手段がないため、この場合も最新のランタイムが読み込まれます。

このアクティベーション ポリシーの残念な弊害は、ユーザーが新しいアプリケーションを新しいバージョンのランタイムと共にインストールした場合、マネージ COM アドインを使用する、まったく無関係で、古いバーションを対象とするアプリケーションが、突然新しいランタイムで実行されるようになり、その結果、エラーが発生する可能性があることです。

.NET Framework 3.0 および 3.5 では、非常に厳格なポリシーによってこの問題を解決しました。各リリースでは追加方式の変更しか行わない、つまり、同じランタイムを基盤として、以前のバージョンに新しいアセンブリの追加のみを行うというものです。このポリシーによって、.NET Framework 2.0 を実行するコンピューターに 3.0 や 3.5 をインストールしても、互換性の問題は発生しなくなります。つまり、.NET Framework 3.5 でアプリケーションを実行している場合、実際には、いくつかのアセンブリが追加された .NET Framework 2.0 のランタイムでアプリケーションが実行されていることになります。しかし、これは、ガーベージ コレクター、JIT (Just-In-Time)、基本クラス ライブラリなどの重要な機能も含め、.NET 2.0 のアセンブリ自体に抜本的な変更を加えることができないことも意味します。

.NET Framework 4 では、既存のアドインの障害にならずに、コアのアセンブリの抜本的な強化も可能にする、高い互換性を実現する方法を実装しました。これで、.NET 2.0 と .NET 4 の両方のアドインを同じプロセス内で同時に実行できるようになります。この方法を "インプロセス サイドバイサイド" (In-Proc SxS: In-Process Side-by-Side) と呼びます。

In-Proc SxS は、ほとんどの互換性の問題を解決しますが、すべての問題を解決できるわけではありません。このコラムでは、主に、In-Proc SxS を開発することになった理由とそのしくみ、および In-Proc SxS によって解決されない問題について説明します。通常のアプリケーションやアドインを作成する場合は、ほとんどの場合、特に何もしなくても In-Proc SxS は機能します。つまり、適切な処理がすべて自動的に行われます。In-Proc SxS を利用できるホストを作成する場合は、新しくなったホスティング API と、これらの API を使用するうえでのガイドラインもこのコラムで説明するので参考にしてください。

レイ・オジーのオフィス訪問

2005 年の終盤のことです。マイクロソフトの上層部の役員全員が、突然、各自のメインのコンピューターで電子メールを確認できなくなりました。理由には心当たりがありませんが、Outlook を開くと必ずクラッシュし、再起動してもまたクラッシュするという堂々巡りに陥っていました。Outlook の新しい更新プログラムが提供されたわけでもなく、この問題の原因になりそうなものも他にありませんでした。ほどなく、マネージ アドインからスローされるマネージ例外が原因であることが突き止められました。Office のマネージ アドインを担当している Visual Studio Tools for Office (VSTO) チームの私の友人 [編集者注: この "私" は共著者の Jesse Kaplan] が、このバグの最大の被害者の 1 人であり、時の最高技術責任者 (CTO) であったレイ・オジーのコンピューターでこの問題を診断するために送り込まれました。

レイ・オジーのオフィスに着くと、友人はすぐに .NET Framework 2.0 のベータ版が社内ベータ プログラムによって配置されていたことを確認でき、この問題を引き起こしている Office アドインを特定できました。CLR チームの互換性担当 PM の 1 人だった私は、このアドインをインストールして、調査を引き継ぎました。

何が問題になったかはすぐにわかりました。このアドインは、9 つの異なるスレッドを起動し、各スレッドが起動すると、それぞれのスレッドが処理したデータで初期化されるという競合状態に陥っていました (図 1 参照)。このコードが作成されたときはたまたまタイミングの問題が起きなかったのですが、.NET Framework 2.0 がインストールされると、前述の理由から、このアドインは自動的に本来の対象バージョンよりも新しい .NET 2.0 で実行されるようになりました。しかし、.NET 2.0 ではわずかに速くスレッドが開始されていたため、潜在していた競合状態が表面化するようになったのです。

図 1 問題の Office アドインのコード

Thread [] threads = new Thread[9];
for (int i=0; i<9; i++)
{
    Worker worker = new Worker();
    threads[i] = new ThreadStart(worker.Work);
    threads[i].Start(); //This line starts the thread executing
    worker.identity =i; //This line initializes a value that
                        //the thread needs to run properly
}

このアプリケーション エラーによって、互換性についての厳しい教訓を徹底的に学びました。どれほどアプリケーション エラーを引き起こす可能性がある動作の変更を避けても、パフォーマンスの強化といった簡単なことがきっかけで、開発とテストの基盤になったランタイム以外で実行されると、アプリケーションやアドインのエラーを引き起こし得るバグが表面化する可能性があるということです。意味のある形でプラットフォームを進化させながら、上記のようなアプリケーションを最新バージョンでまったく問題なく実行できるようにする方法などないことを悟ったのです。

インストールが引き起こす問題

互換性テストをしていて、次のようなアプリケーションに遭遇しました。このアプリケーションは、.NET 2.0 がアプリケーションの後にインストールされると問題なく動作しますが、1.1 (アプリケーションの開発基盤のバージョン) と 2.0 の両方がインストールされているコンピューターにインストールされるとエラーになりました。何が起きているかを解明するには時間がかかりましたが、インストーラー内で実行されるコードが原因であることを突き止めました。このコードも、やはり、開発基盤のバージョンよりも新しい 2.0 で実行されていて、今回は .NET Framework のディレクトリを見つけられないことが問題でした。

このディレクトリの検出ロジックは明らかに脆弱で、以下をご覧いただくとわかるように、実際、正しくありませんでした。

string sFrameworkVersion = System.Environment.Version.ToString();
string sWinFrameworkPath = session.Property["WindowsFolder"] +
"Microsoft.NET\\Framework\\v" +
sFrameworkVersion.Substring(0,8) + "\\";

しかし、このバグを修正しても、アプリケーションはインストール後に適切に稼働できませんでした。修正コードは次のとおりです。

string sWinFrameworkPath = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

結局、インストーラーは caspol.exe へのパスを取得し、.NET Framework での実行許可をアプリケーションに付与するために、.NET Framework のディレクトリを探していました。アプリケーション自体は 1.1 CLR で実行されていても、インストーラーは 2.0 CLR での実行許可をインストーラー自体に付与しているため、パスを発見した後でもエラーになりました。問題のコードは次のとおりです。

System.Diagnostics.Process.Start(sWinFrameworkPath + "caspol.exe " + casPolArgs);

In-Proc SxS による互換性

わかったことは、このようなケースのすべてにおいて問題の種は、プラットフォームに大幅な変更や追加を施しながらも、最新バージョンで古いバージョンと同じようにアプリケーションを実行できるようにすることは不可能であるということでした。

最初から、.NET Framework では、複数のバージョンを同じコンピューターにサイドバイサイド インストールできるようにして、各アプリケーションに実行基盤とするバージョンを選択させることで、この問題の解決を図っていました。

ただし、残念ながら、1 プロセスにつき 1 つのランタイムの欠点は、複数の独立したアプリケーションが同じプロセス内で実行されるマネージ COM コンポーネントや機能拡張シナリオの場合、すべてのアプリケーションにとって有効な選択肢がないことでした。つまり、コンポーネントの中には必要とするランタイムを利用できないものがあり、どれほど私たちが互換性の維持に努めても、コンポーネントの何割かではエラーが発生するということです。

同じプロセスにランタイムの複数のバージョンを読み込む新しい機能は、このような問題を解決します。

基本原則

私たちが下したいくつかの決定と、このコラムで後ほど説明する細かい動作を理解しやすくするため、この機能の設計時に指針とした基本原則をお伝えしておきましょう。

  1. .NET Framework の新しいバージョンをインストールしても、既存のアプリケーションには影響がない。
  2. アプリケーションとアドインは、それぞれの開発およびテストの基盤に使用した .NET Framework のバージョンで実行される。
  3. ライブラリを使用する場合など、ライブラリ開発の基盤となった .NET Framework でコードを実行できない場合があるため、やはり旧バージョンとの 100% の互換性の確保に努める必要がある。

すべての既存のアプリケーションとアドインは、それぞれの開発基盤であり、実行時に使用するように構成されている .NET Framework のバージョンで継続して実行されるようにし、明示的に新しいバージョンを要求しない限り、新しいバージョンを認識しないようにします。この原則はマネージ アプリケーションでは定石でしたが、マネージ COM アドインとランタイムをホストする API のコンシューマーにも適用されるようになりました。

アプリケーションが開発基盤としたバージョンのランタイムで実行されるようにするだけでなく、新しいランタイムに簡単に移行できるようにする必要もあるため、.NET 2.0 と同じかそれ以上の互換性を .NET Framework 4 では確保しています。

動作の概要

.NET Framework 4 のランタイム (およびこれ以降のすべてのランタイム) は、プロセス内に共存させて実行できます。古いバージョンのランタイム (1.0 ~ 3.5) にこの機能を移植していませんが、バージョン 4 以降は、古いランタイムのいずれか 1 つと同じプロセス内で実行できるようにしています。つまり、同じプロセス内にバージョン 4、5、および 2.0 を読み込むことはできますが、1.1 と 2.0 を同じプロセスに読み込むことはできません。.NET Frameworks 2.0 ~ 3.5 はすべて 2.0 のランタイムを基盤に実行されるため、これらのバージョン間には競合はありません (図 2 参照)。

.NET Framework のバージョン        
  1.1 2.0 ~ 3.5 4 5
1.1 N/A ×
2.0 ~ 3.5 × N/A
4 N/A
5 N/A

図 2 同じプロセスへの読み込みの可否

既存のアプリケーションやコンポーネントは、.NET Framework 4 ランタイムがインストールされても、何の違いも認識しません。引き続き、開発基盤に使用したランタイムで実行されます。.NET 4 を基盤に開発されたアプリケーションとマネージ COM コンポーネントは、.NET 4 ランタイムで実行されます。.NET 4 ランタイムを利用する必要があるホストは、明示的に .NET 4 ランタイムを要求する必要があります。

In-Proc SxS 機能がもたらすユーザー別メリット

エンド ユーザーおよびシステム管理者: 単独であれ、アプリケーションと併せてであれ、新しいバージョンのランタイムをインストールしても、コンピューターに何も影響がなく、すべての既存のアプリケーションが前と同じように動作することに不安を覚えることがありません。

アプリケーション開発者: In-Proc SxS がアプリケーション開発者に与える影響はほとんどありません。これまで常に、アプリケーションはそれぞれの開発基盤となった .NET Framework のバージョンで既定で実行されてきましたが、この動作に変更はありません。アプリケーション開発者に影響がある唯一の変更は、古いバージョンのランタイムを対象に開発されたアプリケーションがインストールされるコンピューターにそのバージョンが存在していない場合、自動的に新しいバージョンで実行されなくなったことです。代わりに、ユーザーにオリジナルのバージョンをダウンロードするように求め、すぐにダウンロードできるようにリンクを提供することになります。

どの .NET Framework のバージョンで実行するかを指定できる構成オプションは現在も提供されているため、古いアプリケーションを新しいランタイムで実行できますが、自動的には処理されません。

ライブラリ開発者およびコンシューマー: In-Proc SxS では、ライブラリ開発者が抱えている互換性の問題は解決されません。直接参照または Assembly.Load* によってアプリケーションに直接読み込まれるライブラリは、引き続き、ライブラリを読み込むアプリケーションのランタイムと AppDomain に直接読み込まれます。つまり、アプリケーションが .NET Framework 4 のランタイムで実行されるように再コンパイルされていても、.NET 2.0 を対象に開発された依存アセンブリを持つ場合、それらの依存アセンブリも .NET 4 ランタイム上に読み込まれます。したがって、依然として、サポートする .NET Framework のすべてのバージョンでライブラリをテストすることをお勧めします。これは、高いレベルの下位互換性が引き続き確保されている理由の 1 つです。

マネージ COM コンポーネント開発者: これまで、マネージ COM コンポーネントは、コンピューターにインストールされている最新バージョンのランタイムで自動的に実行されました。現在でも、.NET Framework 4 以前に開発されたコンポーネントは最新のランタイムに対してアクティブになりますが (3.5 以前)、新しいコンポーネントはすべて、開発の基盤にしたバージョンに対して読み込まれます (図 3 参照)。

マネージ COM コンポーネント: コンポーネントが実行されるバージョン        
コンポーネントのバージョン 1.1 2.0 ~ 3.5 4 5
コンピューター/プロセスの状態

インストール済みのバージョン: 1.1、3.5、4、5

読み込まれているバージョン: なし

3.5 3.5 4 5

インストール済みのバージョン: 1.1、3.5、4、5

読み込まれているバージョン: 1.1、4

1.1 読み込まれません* 4 5

インストール済みのバージョン: 1.1、3.5、4、5

読み込まれているバージョン: 3.5、5

3.5 3.5 4 5

インストール済みのバージョン: 1.1、3.5、5

読み込まれているバージョン: なし

3.5 3.5 既定では読み込まれません** 5

*これらのコンポーネントは、これまでと同様、読み込まれません。

**コンポーネントを登録するときに、コンポーネントがサポートするバージョンの範囲を指定できるようになりました。したがって、5 以降のランタイムでのテストが完了している場合は、そのコンポーネントを 5 以降のランタイムで実行するように構成できます。

       

図 3 マネージ COM コンポーネントとランタイムの相互運用性

シェル拡張開発者: シェル拡張とは、Windows シェル内の拡張ポイントに対して幅広く使用される一般的な名称です。よく使用されるものとしては、ファイルとフォルダーの右クリックによるコンテキスト メニューに追加される拡張機能と、ファイルとフォルダーのカスタム アイコンまたはアイコン オーバーレイを提供する拡張機能の 2 つが挙げられます。

これらの拡張機能は標準の COM モデルを利用して公開され、特徴としては、どのアプリケーションで使用される場合でもインプロセスで読み込まれることです。このことと、1 プロセスで 1 つの CLR しか使用できないことが、マネージ シェル拡張の問題を引き起こしていました。以下に詳しく説明します。

  • シェル拡張をランタイム バージョン N を対象として開発しました。
  • N よりも新しいバージョンを対象に開発されたアプリケーションでも、N よりも古いバージョンを対象に開発されたアプリケーションでも、コンピューター上のすべてのアプリケーションにこのシェル拡張を読み込める必要があります。
  • アプリケーションが拡張機能よりも新しいバージョンを対象に開発されている場合、互換性の問題がない限り、基本的に問題はありません。
  • アプリケーションが拡張機能よりも古いバージョンを対象に開発されている場合、間違いなくエラーが発生します。古いランタイムでは、明らかに、新しいランタイムを基盤に開発されたシェル拡張は実行できません。
  • アプリケーションのマネージ コード コンポーネントより先にシェル拡張が読み込まれた場合、シェル拡張の .NET Framework のバージョンとアプリケーションの .NET Framework のバージョンが競合し、壊滅的な影響をもたらす可能性があります。

このような問題のために、マネージ コードを使用したインプロセス シェル拡張の開発は、公式に推奨 (およびサポート) されないようになりました。これは私たちにとってもお客様にとっても痛みを伴う決断でした。詳細については、この問題について説明している MSDN フォーラム (https://social.msdn.microsoft.com/forums/ja-JP/netfxbcl/thread/1428326d-7950-42b4-ad94-8e962124043e、英語) を参照してください。シェル拡張は非常によく利用されていて、特定の種類のアプリケーションの開発者がネイティブ コードを書かなくても済む可能性が最も高い場所の 1 つです。残念ながら、1 つのプロセスで 1 つのランタイムしか使用できないという制限のために、マネージ コードを使用したインプロセス シェル拡張をサポートできませんでした。

1 プロセスに複数のランタイムを混在できるようになったことで、コンピューター上の任意のアプリケーションにより、インプロセスで実行されるものも含め、マネージ シェル拡張の作成を基本的にサポートできるようになりました。ただし、.NET Framework 4 よりも古いバージョンを使用して作成されたシェル拡張は現在でもサポートされません。それらの古いバージョンのランタイムが他のバージョンと併せてプロセス内に読み込まれることはなく、多くの場合エラーを誘因するためです。

マネージでもネイティブでも、シェル拡張を開発する場合は、特別な注意を払い、シェル拡張がさまざまな環境で実行でき、他のコンポーネントと問題なく機能できることを確認する必要があります。製品 (RTM) 版の完成が近づいたら、Windows エコシステム内で適切に稼働する質の高いマネージ シェル拡張を開発するためのガイダンスとサンプルを提供する予定です。

マネージ コードのホスト: ネイティブ COM アクティベーションを使用してマネージ コードをホストする場合、複数のランタイムを操作するために必要な特別な作業はありません。これまでどおりコンポーネントをアクティブ化するだけで、図 3 のルールに従って、コンポーネントがランタイムに読み込まれます。

.NET Framework 4 より前のバージョンのホスト API を使用されたことがあれば、それらの API はどれも、プロセスに 1 つのランタイムしか読み込まれないことを前提としていることにお気付きかと思います。したがって、.NET のネイティブ API を使用するランタイムをホストする場合、ホストを変更して In-Proc-SxS 対応にする必要があります。複数のランタイムを 1 つのプロセスで使用できるようにするこの新たな取り組みの一環として、1 ランタイム対応の古いホスト API を廃止し、複数ランタイム環境に対応できる新しいホスト API のセットを追加しました。新しい API の完全なドキュメントは MSDN で公開する予定ですが、現在の API を使用したことがあれば、これらの API は比較的簡単に使用できるでしょう。

In-Proc SxS の開発時に直面した最も興味深い課題の 1 つは、既存の、1 ランタイム対応のホスト API の動作をどのようにして更新するかという問題でした。さまざまな選択肢がありましたが、このコラムで前に述べた原則に従うと、次のガイドラインが残されました。API は、.NET Framework 4 がコンピューターにインストールされている場合でも、以前とまったく同じ動作を返すように、動作する必要があるというものです。つまり、古い API は、各プロセスで 1 つのランタイムしか認識せず、コンピューターにインストールされている最新のランタイムが事前にアクティブ化されるような形でこれらの API を使用していたとしても、これらの API では、バージョン 4 より古いバージョンのうちで最新のランタイムしか使用されません。

バージョン番号 4 をこれらの API 明示的に渡すか、アプリケーションを特定の方法で構成することで、API を .NET Framework 4 ランタイムに "バインド" する方法はありますが、やはり、バインドできるのは、バージョン 4 のランタイムを明示的に要求して、"最新" バージョンを要求していない場合のみです。

まとめ: 既存のホスト API を使用するコードは、.NET Framework 4 がインストールされても引き続き利用できますが、これらのコードはプロセスに 1 つのランタイムしか読み込まれていないかのように処理されます。また、この互換性を維持するため、通常はバージョン 4 以前のランタイムしか利用できません。このような古い API のそれぞれにどのバージョンが使用されるかの詳細については、MSDN で公開する予定ですが、上記のいくつかの規則は、これらの動作がどのように決められているかを理解するうえで参考になるでしょう。複数のランタイムを利用する場合は、新しい API に移行する必要があります。

C++/CLI 開発者: C++/CLI (マネージ C++) は、同じアセンブリにマネージ コードとネイティブ コードを混在させ、大部分は開発者による操作を必要とすることなく、マネージ コードとネイティブ コード間の切り替えに対応できる興味深いテクノロジです。

このようなアーキテクチャーのため、新しいフレームワークでこれらのアセンブリを使用する方法には制限があります。根本的な問題の 1 つは、これらのアセンブリを 1 つのプロセスに何度も読み込めるようにした場合、マネージ データ セクションとネイティブ データ セクションを分離しておく必要があります。つまり、両方のセクションを 2 回読み込む必要がありますが、これはネイティブの Windows ローダーでは許可されていません。以下の制限がある理由の完全な詳細については、このコラムの範囲外になりますが、RTM が近づいてきたらどこかで提供する予定です。

基本的な制限は、.NET Framework 2.0 より古い C++/CLI アセンブリは、.NET 2.0 ランタイムにしか読み込めないことです。2.0 C++/CLI ライブラリを提供し、これをバージョン 4 以降で使用できるようにする場合、このライブラリを読み込む各バージョンでライブラリを再コンパイルする必要があります。このようなライブラリの 1 つを使用する場合、ライブラリの開発者から新しいバージョンを取得するか、最後の手段としては、アプリケーションを構成してバージョン 4 より古いランタイムがプロセスに読み込まれないようにする必要があります。

混乱からの解放

Microsoft .NET Framework 4 は、これまでで下位互換性のレベルが最も高い .NET リリースです。In-Proc SxS を導入することで、.NET 4 をインストールしただけで、既存のアプリケーションでエラーが発生することはなく、コンピューターに既にインストールされているものはすべて以前と同様に機能するようにしています。

エンド ユーザーは、直接インストールする場合でも、.NET Framework を必要とするアプリケーションと併せてインストールする場合でも、.NET Framework をインストールすることで、既にコンピューターにインストールされているアプリケーションのエラーを引き起こす心配をしないで済むようになります。

企業も IT プロフェッショナルも、各アプリケーションが使用するバージョンどうしが競合することを心配せずに、新しいバージョンの .NET Framework をすぐに、または必要に応じて徐々に採用できます。

開発者は、.NET Framework の最新バージョンを使用してアプリケーションを開発でき、開発したアプリケーションを導入しても安全であることを顧客に保証できます。

また、必要な .NET Framework が提供されるので、何も影響を受けず、新しいバージョンの .NET Framework をインストールしても、コードが引き続き機能すると確信が持てるため、ホストおよびアドイン開発者は気を揉まずにいることができます。                                                             

Jesse Kaplan は、マイクロソフトで CLR チームのマネージ/ネイティブ相互運用性のプログラム マネージャーを務めています。これまでに、互換性および拡張性を担当した経験があります。

Luiz Santos* は、CLR チームの元メンバーで、現在は SQL Connectivity チームのプログラム マネージャーを務め、SqlClient、ODBCClient、OLEDBClient など、ADO.NET マネージ プロバイダーを担当しています。*

この記事のレビューに協力してくれた技術スタッフの Joshua Goodman、Simon Hall、および Sean Selitrennikoff に心より感謝いたします。