次の方法で共有


CLR 徹底解剖

マネージ コードとネイティブ コードの相互運用性の推奨事項

Jesse Kaplan

目次

マネージ コードとネイティブ コードの相互運用性が適している場合
相互運用性テクノロジ: 3 つの選択肢
相互運用性テクノロジ: P/Invoke
相互運用性テクノロジ: COM 相互運用機能
相互運用性テクノロジ: C++/CLI
相互運用性アーキテクチャに関する注意点
API の設計と開発者の作業
相互運用性の境界のパフォーマンスと場所
有効期間管理

このようなコラムが 2009 年初頭の MSDN Magazine に登場することは多少奇異に思われるかもしれません。2002 年に Microsoft .NET Framework バージョン 1.0 が登場して以来、マネージ コードとネイティブ コードの相互運用性は、多少の変更はあれ、Microsoft .NET Framework によって同様の形式でサポートされてきました。API に関する詳細なドキュメント、ツールレベルのドキュメント、詳しいサポート情報が掲載された多数のページもすぐに参照できます。それでも、まだ不足している情報が相当に存在し、相互運用性の使用が適切な場面、考慮する必要がある設計上の注意事項、使用する相互運用性テクノロジの選択についての高度なアーキテクチャ ガイダンスが必要です。この記事では、これらの不足している情報を補います。

マネージ コードとネイティブ コードの相互運用性が適している場合

マネージ コードとネイティブ コードの相互運用性の使用はどのような場合に適しているかについて説明しているドキュメントは少なく、存在するドキュメントの内容間にも相反する記述が多々見受けられます。ガイダンスが実践に基づいていない場合もあります。そこで、最初に、このコラムで説明するガイダンスはすべて相互運用性チームがあらゆる規模の内部および外部顧客を支援してきた経験に基づいて作成されていることを一言添えておきます。

その経験から、相互運用性を使用した成功例となる 3 つの製品と、相互運用性の典型的な使用タイプについてまとめてみました。Visual Studio Tools for Office は Office のマネージ拡張ツールセットであり、筆者が相互運用性で最初に思い浮かべるアプリケーションです。このツールセットは、大規模なネイティブ アプリケーションでマネージ拡張またはアドインを有効にするという従来の方法で相互運用性を使用します。2 つ目の製品は Windows Media Center です。このアプリケーションはマネージ アプリケーションとネイティブ アプリケーションを混合して一から構築されました。Windows Media Center の大部分はマネージ コードで開発され、テレビ チューナーなどのハードウェア ドライバを直接処理する部分はネイティブ コードで記述されています。3 つ目の製品は Expression Design です。大規模な既存のネイティブ コードをベースとするこのアプリケーションは新しいマネージ テクノロジ、この場合は Windows Presentation Foundation (WPF)、を利用することによって次世代のユーザー エクスペリエンスを提供します。

これらの 3 つのアプリケーションは、相互運用性を使用する 3 つの最も一般的な理由を示しています。その理由とは、既存のネイティブ アプリケーションのマネージ拡張を使用できるようにするため、アプリケーションの大部分でマネージ コードの利点を活かし、低レベルな部分をネイティブ コードで記述するため、および既存のネイティブ アプリケーションに次世代のユーザー エクスペリエンスを追加して差別化するためです。

これまでのガイダンスでは、このような場合はアプリケーション全体をマネージ コードで単に書き換えるよう推奨していました。このアドバイスを実際に試してみれば、また、多くのユーザーがこの推奨に従うことを拒否している事実に照らしてみれば、これが多くの既存アプリケーションに適さない方法であることは明らかです。相互運用性は、ネイティブ コードでの既存投資の維持と新しいマネージ環境の利用を同時に実現する重要なテクノロジになるでしょう。別の理由でアプリケーションを書き換える場合、マネージ コードが適している可能性があります。とは言え、通常は、新しいマネージ テクノロジを使用し、相互運用性を使用しないようにするためだけにアプリケーションを書き換える気にはならないものです。

相互運用性テクノロジ: 3 つの選択肢

.NET Framework には 3 つの主要な相互運用性テクノロジがあります。そのいずれを選択するかは、あるときは相互運用性のために使用する API の種類によって決められ、またあるときは開発要件および相互運用性の境界を制御する必要性によって決められます。Platform Invoke (P/Invoke) は主にマネージ コードとネイティブ コードの相互運用性テクノロジであり、マネージ コードから C スタイルのネイティブ API を呼び出す機能を提供します。COM 相互運用機能は、マネージ コードからネイティブ COM インターフェイスを使用する機能、またはマネージ API からネイティブ COM インターフェイスをエクスポートする機能を提供します。また、C++ でコンパイル済みのマネージ コードおよびネイティブ コードが混在するアセンブリを作成する機能を提供し、マネージ コードとネイティブ コードの橋渡し役を果たす C++/CLI (以前のマネージ C++) があります。

相互運用性テクノロジ: P/Invoke

P/Invoke は 3 つのテクノロジの中で最もしくみが単純で、主に C スタイルの API へのマネージ アクセスを提供します。P/Invoke では、各 API を個別にラップする必要があります。これは、ラップする API の数が少なく、シグネチャがそれほど複雑ではない場合には有効な選択肢です。ところが、可変長構造体、void *、オーバーラップする共用体など、マネージ API にない型の引数がアンマネージ API に多数存在する場合、P/Invoke の使用は相当難しくなります。

.NET Framework の基本クラス ライブラリ (BCL) には、多数の P/Invoke 宣言のシック ラッパーとなるさまざまな API のサンプルが含まれています。.NET Framework のアンマネージ Windows API をラップする機能のほとんどは P/Invoke で構築されています。Windows フォームも大部分がネイティブ ComCtl32.dll using P/Invoke で構築されています。

P/Invoke の使用を大幅に簡素化する有用なリソースもいくつかあります。まず、Web サイト pinvoke.net の wiki があります。元は CLR 相互運用性チームの Adam Nathan によって開設されたものですが、多数のユーザーから投稿された一般的な Windows API のシグネチャが豊富に揃っています。

Visual Studio から pinvoke.net を簡単に参照できるきわめて便利な Visual Studio アドインもあります。自分または他のユーザーのライブラリにある API が pinvoke.net に存在しない場合のために、相互運用性チームは、ヘッダー ファイルに基づいてネイティブ API のシグネチャを自動作成する P/Invoke シグネチャ生成ツール、P/Invoke Interop Assistant をリリースしました。このツールの動作中のスクリーンショットを次に示します。

fig01.gif

P/Invoke Interop Assistant を使用したシグネチャの作成

相互運用性テクノロジ: COM 相互運用機能

COM 相互運用機能は、マネージ コードから COM インターフェイスを使用する機能、またはマネージ API を COM インターフェイスとして公開する機能を提供します。TlbImp ツールを使用して、特定の COM tlb ファイルにアクセスするマネージ インターフェイスを公開するマネージ ライブラリを生成できます。TlbExp はこれと逆の操作を行い、マネージ アセンブリの ComVisible 型に相当するインターフェイスを使用して COM tlb ファイルを生成します。

アプリケーション内で、またはその拡張モデルとして、既に COM を使用している場合には、COM 相互運用機能はきわめて有効なソリューションです。また、COM 相互運用機能はマネージ コードとネイティブ コードとの間の完全な COM のセマンティクスを維持する最も簡単な方法です。CLR は基本的に Visual Basic 6.0 と同じ COM 規則に従っているため、特に、Visual Basic 6.0 ベースのコンポーネントとの相互運用性を保つ場合には、COM 相互運用機能は優れた選択肢です。

COM を内部的に使用していないアプリケーションや、完全な COM のセマンティクスを必要とせず、パフォーマンスの低下が許容されないアプリケーションでは、COM 相互運用機能はあまり役に立ちません。

Microsoft Office は、マネージ コードとネイティブ コード間のやり取りに COM 相互運用機能を使用している最も顕著な例です。長い間 COM を拡張メカニズムとして使用し、Visual Basic for Applications (VBA) または Visual Basic 6.0 で使われることが特に一般的だった Office は COM 相互運用機能に好適です。

当初、Office は全面的に TlbImp に依存し、シン相互運用アセンブリをマネージ オブジェクト モデルとして採用していましたが、やがて Visual Studio Tools for Office (VSTO) 製品が Visual Studio に組み込まれると、このコラムで説明している原則の多くを取り入れたよりリッチな開発モデルに移行していきました。現在の VSTO 製品では、P/Invoke が大部分の BCL の基盤であることを忘れがちであるのと同様、COM 相互運用機能が VSTO の基盤になっていることを忘れかねないほどです。

相互運用性テクノロジ: C++/CLI

C++/CLI は、ネイティブ コードとマネージ コードの橋渡し役を果たし、C++ のマネージ コードとネイティブ コードを同じアセンブリ (または同じクラス) にコンパイルする機能や、アセンブリのマネージ部分とネイティブ部分の間で標準的な C++ の呼び出しを行う機能を提供します。C++/CLI を使用する場合、アセンブリのどの部分をマネージ コードにし、どの部分をネイティブ コードにするかを開発者が選択します。結果として生成されるアセンブリには MSIL (Microsoft 中間言語、すべてのマネージ アセンブリに見られる) とネイティブ アセンブリ コードが混在します。C++/CLI は、相互運用性の境界をほぼ完全に制御できるきわめて高機能な相互運用性テクノロジです。ただし、境界を開発者がほぼ完全に制御しなければならない点が短所でもあります。

C++/CLI は、静的型チェックが必要な場合、パフォーマンス要件が厳しい場合、およびより予測可能な終了処理が必要な場合に優れた橋渡し役になります。P/Invoke または COM 相互運用機能で要件が満たされるのであれば、一般に使い方は単純ですし、特に開発者が C++ に慣れていない場合には便利です。

C++/CLI を検討する際に注意すべき点がいくつかあります。まず、C++/CLI を使用して COM 相互運用機能の高速化を計画する場合、COM 相互運用機能は開発者に代わって行う処理が多いため、C++/CLI より低速になります。アプリケーションでの COM の使用が緩やかで、完全な COM 相互運用機能を必要としない場合には、適切なトレードオフになります。

ところが、COM 仕様の大部分を使用する場合、必要な COM のセマンティクスの部分を C++/CLI ソリューションに追加すると、処理が増え、パフォーマンスは COM 相互運用機能を使用した場合と大差なくなります。これはマイクロソフトの複数のチームの経験からわかったことで、結局、各チームは元どおり COM 相互運用機能を使用しています。

C++/CLI の使用に関するもう 1 つの重要な注意点は、C++/CLI はあくまでネイティブ コードとマネージ コードの橋渡し役を果たす機能であり、アプリケーションの大部分を記述するためのテクノロジではないということです。C++/CLI でアプリケーションの大部分を記述することも可能ですが、純粋な C++ または C#/Visual Basic 環境で記述した場合と比べて開発者の生産性がはるかに低下し、アプリケーションの起動速度も大幅に低下します。したがって、C++/CLI を使用する場合は、必要なファイルのみを /clr スイッチを指定してコンパイルし、アプリケーションのコア機能の構築には純粋なマネージ アセンブリまたは純粋なネイティブ アセンブリの組み合わせを使用してください。

相互運用性アーキテクチャに関する注意点

アプリケーションに相互運用機能を使用することを決定し、使用するテクノロジを選択した後は、API の設計や、相互運用性の境界に関するコーディングに必要な開発者の作業など、ソリューション設計時のいくつかの高度な注意点があります。ネイティブ コードとマネージ コードの遷移をどこで行うか、また、それによってアプリケーションのパフォーマンスにどのように影響する可能性があるかも考慮する必要があります。さらに、有効期間管理について、およびマネージ環境のガベージ コレクションとネイティブ環境の手動による確定的な方法を使用した有効期間管理との相違を埋める処理が必要かどうかについても検討が必要です。

API の設計と開発者の作業

API の設計について考える場合、次の点を検討する必要があります: 相互運用層に関するコーディングはだれが行うか。開発者の作業を改善するため、または境界を構築するコストを縮小するための最適化を行う必要があるか。ネイティブ コードを作成した開発者にこの境界に関するコーディングも担当させるか。社内に他の開発者がいるか。アプリケーションの拡張を委託しているサードパーティの開発者に任せるか、それとも API をサービスとして使用するか。開発者の技術レベルはどの程度か。開発者はネイティブ コードのパラダイムに抵抗がないか。それともマネージ コードでなければ扱いにくいのか。

これらの事項に対する答えを出すことが、ネイティブ コードのシン ラッパーと、内部でネイティブ コードを使用するリッチ オブジェクト モデルの間のどこに設計モデルを定めるかを決定する役に立ちます。シン ラッパーでは、ネイティブ パラダイムが全面的に行き渡ります。開発者は境界を意識し、ネイティブ API に関するコーディングも意識的に行います。シック ラッパーでは、ネイティブ コードが関与していることを開発者にはほとんど意識させないようにすることができます。BCL 内のファイル システム API は、高度なマネージ オブジェクト モデルを提供するきわめてシックな相互運用層の好例です。

相互運用性の境界のパフォーマンスと場所

アプリケーションの最適化に時間をかける前に、相互運用機能のパフォーマンスの問題が存在するかどうかを判断することが重要です。多くのアプリケーションではパフォーマンスが重視される箇所に相互運用機能を使用しているため、この点には十分な注意が必要です。パフォーマンスを重視せず、ユーザーのマウス クリックに応答して相互運用機能を使用する多くのアプリケーションでは、ユーザー環境に遅延を生じるほど大量の相互運用機能の遷移は発生しません。とは言え、相互運用性ソリューションのパフォーマンスを検討する場合、相互運用機能の遷移回数と各遷移で渡すデータ量を減らすことを目標にする必要があります。

マネージ コードとネイティブ コード間で一定量のデータを受け渡す相互運用機能の遷移には、基本的に固定的なコストが生じます。この固定コストは選択した相互運用性テクノロジによって異なりますが、そのテクノロジが持つ機能が必要で選択した以上、テクノロジを変更するわけにはいきません。そこで、境界の通信頻度と境界を往来するデータ量の削減に着目する必要があるのです。

その実現方法は大部分がアプリケーションによって異なりますが、多くの場合に有効な一般的で柔軟性の高い方法は、通信量が多く、データ量の多いインターフェイスを定義している境界側にコードを記述して分離境界を移動することです。その基本的な考え方は、使用頻度の高いインターフェイスの呼び出しを一括処理する抽象層を作成することです。境界を越えてこの API を操作する必要があるアプリケーション ロジックの部分を移動し、入力と結果のみを境界間で受け渡すようにすると、さらに有効です。

有効期間管理

マネージ コードとネイティブ コードの有効期間管理の相違は、多くの場合に相互運用性の実装上の最大の課題になります。.NET Framework のガベージ コレクションベースのシステムとネイティブ環境の手動による確定的なシステムとの基本的な違いは意表をついた現れ方をするので、診断が困難な場合があります。

相互運用性ソリューションに関する問題は、まず、マネージ環境での使用が終了した後もマネージ オブジェクトがネイティブ リソースを保持する場合があり、これに相当の時間がかかるということです。ネイティブ リソースが限られていて、呼び出し元で使い終わった後すぐに解放されることを想定している場合 (データベース接続が好例) には、これがしばしば問題の原因になります。

ネイティブ リソースが十分にある場合には、ガベージ コレクタでオブジェクトのファイナライザを呼び出し、暗黙的または明示的にファイナライザにネイティブ リソースの解放を任せることができます。リソースが限られている場合には、マネージ Dispose パターンが役立ちます。その場合、ネイティブ オブジェクトをマネージ コードに直接公開するのではなく、少なくとも IDisposable を実装するネイティブ オブジェクトをシン ラッパーでラップして標準の公開パターンに従います。この方法なら、リソース不足が問題になった場合に、マネージ コード内でネイティブ オブジェクトを明示的に破棄し、使い終わったリソースをすぐに解放することができます。

アプリケーションに影響を及ぼすことが多い有効期間管理に関する 2 つ目の問題は、多くの場合にガベージ コレクションが十分に機能していないと開発者によって認識されることです。つまり、メモリの使用量は増え続けているのに、何らかの理由によってガベージ コレクタが十分に動作せず、オブジェクトが削除されずに残ります多くの場合、残ったオブジェクトによって GC.Collect の呼び出しが追加され続けてこの問題が発生します。

通常、この問題の根本原因は、極小のマネージ オブジェクトが大量に保持され、そのまま残ることで、膨大なネイティブ データ構造が生じることにあります。その結果、ガベージ コレクタは自動調整を行い、不要なコレクションや役に立たないコレクションの処理に無駄な手間がかからないように試みます。また、プロセスの現在のメモリ不足の状態と、各ガベージ コレクションで解放されるメモリ容量を確認したうえで、新たなガベージ コレクションを行うかどうかを決定します。

この場合、1 回のコレクションで解放されるメモリ容量は小さく (ガベージ コレクタが把握できるのは解放されたマネージ メモリの容量のみです)、これらの小さいオブジェクトを解放することによって全体的なメモリ容量が大幅に減少することは認識されていません。そのため、メモリ使用量が増え続けているにもかかわらず、ガベージ コレクションの回数は減っていきます。

この問題を解決するには、ネイティブ リソースに対するこれらの小さいマネージ ラッパーの実際のメモリ コストを示すヒントをガベージ コレクタに与えます。そのために、.NET Framework 2.0 に API のペアが追加されました。先ほど使用したラッパーと同様のラッパーを使用して、限られたリソースに Dispose パターンを追加し、リソースを手動で明示的に解放するのではなく、ガベージ コレクタにヒントを指定するように機能を変更することができます。

ヒントを指定するために必要な処理は、このオブジェクトのコンストラクタで GC.AddMemoryPressure メソッドを呼び出してネイティブ オブジェクトのネイティブ メモリの概算コストを渡すことだけです。次に、オブジェクトのファイナライザ メソッドで GC.RemoveMemoryPressure を呼び出します。この 2 つの呼び出しは、ガベージ コレクタにこれらのオブジェクトの実際のコストおよびこれらのオブジェクトを解放した場合に解放される実際のメモリ容量を認識させるのに役立ちます。AddMemoryPressure と RemoveMemoryPressure の呼び出しの適切なバランスをとることが重要です。

マネージ環境とネイティブ環境との間の有効期間管理によく見られる不整合の 3 つ目は、個々のリソースまたはオブジェクトの管理よりもアセンブリまたはライブラリ全体に関するものです。ネイティブ ライブラリはアプリケーションで使い終わったら簡単にアンロードできますが、マネージ ライブラリは自らをアンロードすることはできません。代わりに、CLR には個別にアンロード可能で、アンロード時に該当ドメインで実行中のすべてのアセンブリ、オブジェクト、およびスレッドをクリーンアップするアプリケーション ドメインと呼ばれる分離単位があります。ネイティブ アプリケーションを構築する場合、使い終わったアドインのアンロードに慣れている開発者は、マネージ アドインごとに異なるアプリケーション ドメインを使用するという方法には、個々のネイティブ ライブラリをアンロードする場合と同様の柔軟性があることがわかるでしょう。

ご意見やご質問は clrinout@microsoft.com まで英語でお送りください。

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