次の方法で共有


相互運用機能アセンブリを使った複雑な COM オブジェクトの処理 - Office のメニュー ボタンが機能停止した場合の対策

Peter Vogel
PH&V Information Services

August 2004

適用対象:
    Microsoft Office 2003 Edition

要約: メニュー ボタンが一貫して機能しない場合の対策や、 .NET Framework を使用してインスタンス化した COM オブジェクトのメモリを .NET がどのように管理するかなど、.NET Framework 内で COM オブジェクトを処理するときの一般的な問題とその対処方法について Peter Vogel が説明します。

目次

はじめに
ボタンが機能しない
COM および .NET ベースのメモリ管理
COM メモリを管理する
AppDomain クラスを使用して COM オブジェクトを管理する
ReleaseComObject を使用して COM オブジェクトを管理する
まとめ
著者紹介

はじめに

.NET Framework は .NET Framework および COM 間の通信を処理するためのいくつかのメカニズムを提供します。ただし、COM オブジェクトの処理時にメモリがリークしたり、COM オブジェクトが突然機能しなくなったりするのを防ぐため、.NET Framework による COM オブジェクトの管理機能についての適切な知識が必要です。

.NET Framework には COM オブジェクトを処理する方法が多数ありますが、最も便利な方法の一つに "相互運用機能アセンブリ" があります。相互運用機能アセンブリを使用すると、.NET ベースのマネージ コードがランタイム呼び出し可能ラッパー (RCW: Runtime Callable Wrapper) と対話し、.NET ベースのアプリケーション用の COM オブジェクトを管理します。RCW は、コンポーネントのアクティブ化およびパラメータのマーシャリング (文字列データ型の COM BSTR への変換など) を含め、COM オブジェクト処理に関する問題をいくつか解決します。マイクロソフトは、プライマリ相互運用機能アセンブリ (PIA) と呼ばれる、Microsoft Office System での処理用にカスタマイズされた相互運用機能アセンブリを提供しています。ただし、これらのツールは COM の有効期間の管理に関する問題には対処しません。そのような問題は、他のツールを使って認識および管理する必要があります。

ボタンが機能しない

簡単に解決できる問題が、時には、.NET ベースのアプリケーションから COM オブジェクトを処理するときの重要な問題を明らかにします。たとえば、Office アプリケーションの操作中に起こる一般的な問題の 1 つに、"機能しないコマンド ボタン" があります。つまり、 .NET ベースのコードから Microsoft Office Word 2003 メニュー バーに新しいメニュー ボタンを追加し、.NET ベースのアプリケーション内のルーチンをボタンの "クリック" イベントに関連付けると、 最初はすべてがうまく機能しているかのように見えます。

ところが突然、何の理由もなく、ユーザーがボタンをクリックしても "クリック" イベント ルーチンが実行されなくなります。ボタンの "クリック" イベントが不意に機能しなくなったわけです。

この問題は、ほとんどの場合、.NET ベースのコード内でメニュー ボタンを参照する変数の宣言方法に原因があります。変数をローカル (サブルーチンまたは関数内など) で宣言した場合、ルーチンが終了すると、変数がその適用範囲外になります。その変数が参照するオブジェクトに関連付けられたルーチンは、オブジェクトが破棄された時点で機能しなくなります。ところが、オブジェクトを参照する変数がその適用範囲外になった後にオブジェクトが破棄されるタイミングは、.NET ベース環境と COM 環境では異なります。

大半の開発者は、この問題の原因が、ルーチンが終了し変数が適用範囲外になった途端に "クリック" イベント コードが実行されなくなったことにあると考えます。確かに、Word を操作する COM アプリケーションで問題が発生するのは、そのときです。ところが、.NET Framework 環境ではオブジェクトが適用範囲外になっても、オブジェクトは必ずしも破棄されません。その代わり、変数が適用範囲外になった後も、最終的に .NET Framework がオブジェクトを破棄するまで、オブジェクトは保持されます。この間、ユーザーがメニュー ボタンをクリックすると "クリック" イベント コードは継続して実行され、.NET Framework がオブジェクトを破棄した時点で突然機能しなくなります。この動作は、オブジェクトの有効期間を管理する方法が COM と .NET Framework では基本的に異なること、つまりメモリの管理方法の相違を反映しています。

COM および .NET ベースのメモリ管理

COM は決定的モデルを実装してメモリを解放します。COM では、各オブジェクトに、そのオブジェクトを使用するクライアントの数を記録する参照カウンタがあります。クライアントがオブジェクトを参照するごとにカウンタが上がり、クライアントがオブジェクトの参照を停止するとカウンタが下がります。カウンタが 0 になるとオブジェクトは即座にメモリから削除されます。C 言語を使用する開発者は参照カウンタを明示的に管理しますが、Visual Basic では、参照カウンタを下げる動作は直接行われません。Visual Basic では、変数が適用範囲外になるか、変数を "なし" (Nothing) に設定すると、その変数が参照するオブジェクトの参照カウンタが下がります。そして、オブジェクトを参照する変数が 1 つであった (またはそれが最後の変数だった) 場合にのみ、COM はメモリから即座にオブジェクトを削除します。

一方、.NET Framework は非決定的アプローチを使ってメモリを解放します。.NET Framework では、使用されなくなったオブジェクトをチェックするガベージ コレクション プロセスの一部として、オブジェクトがメモリから削除されます。ガベージ コレクション プロセスは 必要に応じて行われ、通常は、メモリが制限されたときに行われます。.NET Framework では、変数を "Nothing" に設定することは、その変数が参照するオブジェクトがいつか削除される可能性があることを意味します。非決定的ファイナライズ プロセスおよび .NET Framework でのガベージ コレクションの詳細については、MSDN ライブラリで「.NET Framework Developer's Guide」の記事「ガベージ コレクションのプログラミング」を参照してください。

メモリを再要求する .NET ベースのアプローチによって "クリック" イベントに問題が発生します。.NET ベースのアプリケーションでは、Word を管理する RCW を参照する変数はルーチンの終了時に適用範囲外になりますが、COM オブジェクトを管理する RCW はガベージ コレクションの対象になる可能性があるだけです。最終的には、RCW はガベージ コレクションの対象になり、その時点で、RCW が管理していた COM オブジェクトが破棄されます。この時初めて、.NET ベースのアプリケーションが "クリック" イベントとの関連を失い、ボタンが機能しなくなります。

.NET ベースのアプリケーションで、アプリケーションの有効期間中 COM オブジェクトが保持されるようにするだけであれば、問題は簡単に解決できます。RCW を参照する変数をモジュールまたはクラス レベルで宣言します。そうすると、アプリケーションが完了するまで変数が適用範囲外になりません。Microsoft Visual Basic .NET では、"クラス" レベルでオブジェクトを宣言するコードは次のように記述されます。

Public Class WordManager
    Inherits System.Windows.Forms.Form
Private WithEvents mi As Microsoft.Office.Core.CommandBarButton

COM メモリを管理する

ところが、アプリケーションの完了前に COM オブジェクトを解放したい場合、 メモリが再要求されるべきときに再要求されないという、別の問題に行き当たります。.NET ベースのアプリケーションでは、COM オブジェクトの RCW への参照を "Nothing" に設定できますが、COM オブジェクトは必ずしもメモリから削除されるとは限りません。たとえば、Word を繰り返しロードおよびアンロードして一連のドキュメントを処理するアプリケーションでは、使用した Word のインスタンスが次々とメモリ内に保持されるので、メモリがどんどん不足していきます。メモリの影響を最小限に抑えるように COM オブジェクトを管理するのは、あなたの責任です。

もう一度言いますが、Office オブジェクト モデルの COM オブジェクトは、起こり得る問題の一例に過ぎません。大規模な COM サーバーに多く見られるように、さまざまな Office アプリケーションには数百ものオブジェクトが含まれ、通常のタスクにおいて複数のオブジェクトのインスタンス化が必要とされます。たとえば、次のコードは PIA を使用して Word アプリケーションを起動し、RCW を使用してドキュメントを作成します。

Dim wrd As New Microsoft.Office.Interop.Word.Application
Dim doc As Microsoft.Office.Interop.Word.Document
doc = wrd.Documents.Add()

このコードは、Word "アプリケーション" オブジェクトを管理する RCW、その "ドキュメント" コレクション、および "ドキュメント" オブジェクトの 3 つのオブジェクトをメモリ内にロードします。プロセスの次のステップでドキュメントに大きいサイズの画像をロードする場合、COM オブジェクトによって確保されるメモリの量は膨大になる可能性があります。

不要になったオブジェクトを解放したい場合、Word オブジェクトを参照する変数を "Nothing" に設定します。

wrd = Nothing
doc = Nothing

残念ながら、メモリ不足のためにガベージ コレクションが行われた場合でも、ガベージ コレクションはこれらの COM オブジェクトで使用されるメモリを再要求しないかもしれません。ガベージ コレクションはメモリからのオブジェクトの削除に知的に関連します。短期間しか使用されなかったオブジェクトは、長期間使用されたオブジェクトよりも、ガベージ コレクションの対象になる優先度が高くなります。短期間しか使用されなかったオブジェクトの優先度が高くなるということは、不要になった COM オブジェクトをすぐに解放した場合、オブジェクトがガベージ コレクションの対象になる可能性が高くなるということです。

注: ガベージ コレクションの機能および特性の詳細については、MSDN ライブラリで「.NET Framework Developer's Guide」の記事「ガベージ コレクタの基本とパフォーマンスのヒント」を参照してください。

.NET ベースのアプリケーションから COM オブジェクトを使用するとき、 "RCW" および COM という 2 種類のオブジェクトが関係します。ガベージ コレクションは "RCW" のサイズ (場合によっては小さいこともあります) のみを認識し、COM のサイズ (場合によっては大きいこともあります) は認識しません。そのため、.NET ベースのアプリケーションが RCW を解放する一方、メモリが不足しているにもかかわらず、ガベージ コレクションは RCW を再要求しない可能性があります。RCW がメモリ内に保持される限り、RCW の管理する COM オブジェクトもメモリ内に保持されます。

RCW にガベージ コレクションを強制すると問題が解決するかのように思われますが、.NET Framework におけるガベージ コレクションの強制はお勧めできません。たとえガベージ コレクションを強制したとしても、COM オブジェクトが削除されるとも限りません。これは、.NET Framework 内でガベージ コレクションを明示的に呼び出したとしても、ガベージ コレクションは "常に" 非決定的であるためです。

COM オブジェクトがメモリから解放されたことを確認するメカニズムには、AppDomain オブジェクトと ReleaseComObject メソッドの 2 つがあります。AppDomain を使用すると COM オブジェクトを管理する最も単純なソリューションを構築できますが、パフォーマンス コストが発生し、セキュリティが危険にさらされる可能性があります。ReleaseComObject を使用するとパフォーマンス コストは発生しませんが、注意深い計画とコーディングが必要です。

AppDomain クラスを使用して COM オブジェクトを管理する

.NET Framework では、AppDomain オブジェクトがアプリケーションを実行するための環境を別に提供します。AppDomain をアンロードすると、AppDomain が使用していたすべてのリソースがアンロードされるので、COM オブジェクトを管理するという観点から見ると AppDomain オブジェクトは魅力的です。つまり、 COM オブジェクトを作成するごとに AppDomain を作成し、COM オブジェクトをその中にロードするのです。AppDomain を使用すると、複数の COM オブジェクトを 1 つのドメインにロードし、一度にすべて破棄できるので、COM オブジェクトの操作が簡素化されます。

ただし、AppDomain の作成はコストがかかるので、パフォーマンスが重要な場合は AppDomain を使用しないでください。さらに、AppDomain をリモートで使用できるようにするとコード アクセス セキュリティが破壊されます (リモートで使用しない場合、AppDomain は安全です)。

次に示すのは、"MyDLL.MyObject" という型名の COM オブジェクトが、"MyDLL.DLL" という名前のファイル内にあることを確認するコード例です。この COM DLL への参照をアプリケーションに追加するには、[プロジェクト] メニューの [参照の追加] をクリックし、[参照] をクリックして MyDLL.DLL を選択します。

サーバー用の PIA が存在する場合、参照がプログラムに追加されます (PIA が存在しない場合、Visual Studio .NET が DLL 用の相互運用アセンブリを生成します)。相互運用アセンブリはサーバーのタイプ ライブラリ内の各クラスのエントリを含み、ライブラリ内の各オブジェクトの名前は元のクラス名に "Class" が付いたものになります。つまり、MyDLL.MyObject という名前のクラスからは "MyDLL.MyObjectClass" という名前が生成されます。

COM オブジェクトに参照を追加すると、CreateInstanceFromAndUnWrap メソッドを使ってオブジェクトを "AppDomain" にロードできるようになります。このメソッドは、 相互運用アセンブリへの完全なパス名と、クラス モジュールのエントリ ポイントの、2 つのパラメータを使います。"タイプ" (Type) オブジェクトを使って相互運用アセンブリの場所を決めることができます。"タイプ" オブジェクトの関連 "アセンブリ" (Assembly) オブジェクトには、相互運用アセンブリが見つかる場所を指定する "ロケーション" (Location) プロパティがあります。初期の状態では、不要なタイプ ライブラリ情報がロードされるのを防ぐために RCW がラップされますが、CreateInstanceFromAndUnWrap メソッドもそのラッパーを削除します。

次のコード例では、"MyDomain" という名前で "AppDomain" を定義し、サンプルの COM オブジェクトを "AppDomain" にロードします。そして、オブジェクトを使用し、完了すると、"AppDomain" をアンロードして COM オブジェクトを解放します。

Dim apd As AppDomain
Dim obj As MyDLL.MyObject
Dim objType As Type

objType = GetType(MyDLL.MyObject)
apd = AppDomain.CreateDomain("MyDomain")
obj = apd.CreateInstanceFromAndUnwrap( _
     objType.Assembly.Location, "MyDLL.MyObjectClass")
. . .using the COM object. . .AppDomain.Unload(apd)

AppDomains の詳細については、.NET Framework Developer's Guide の記事「アプリケーション ドメイン」を参照してください。前述したように、ドメインに参照を渡す際は注意が必要です。"AppDomain" をリモートで使用できるようにすると、そのドメインのコード アクセス セキュリティが破壊されます。

ReleaseComObject を使用して COM オブジェクトを管理する

AppDomain オブジェクトは、COM オブジェクトの参照カウントを 0 にして、メモリが COM オブジェクトを解放するように強制する場合にも使用されます。COM オブジェクトが解放された後、RCW を解放してガベージ コレクションの対象にできます。ReleaseComObject の実装は AppDomain ソリューションほど単純ではありませんが、AppDomain の作成に必要なパフォーマンス コストが ReleaseComObject ではかかりません。

RCW に COM オブジェクトが作成されると、COM オブジェクトの参照カウンタが 1 に設定されます。1 つのプロセスにおいて、いくつの .NET ベースのクライアントが RCW を参照しても、COM オブジェクトの参照カウンタは 1 のままです。ただし、プロセス境界を越えて RCW を渡すと、COM オブジェクトの参照カウンタを上げることができるので、参照カウンタは常に 1 に留まるとは限りません。

.NET Framework では、RCW を System.Marshall クラスの ReleaseComObject メソッドに渡すことによって、COM オブジェクトの参照カウントを直接下げることができます。これを行うと、値の下がった COM オブジェクトの参照カウントが返されます。次の例では、Word をロードし、参照カウントを下げ、RCW を解放します。

Dim wrd As New Microsoft.Office.Interop.Word.Application
. . .working with Word. . .intRefCount = _
System.Runtime.InteropServices.Marshal.ReleaseComObject(wrd)
wrd = Nothing

ReleaseComObject によって返された値が 0 より大きい場合に対処するため、返された値が 0 になるまで ReleaseComObject を実行するループでメソッドを呼び出します。

Dim wrd As New Microsoft.Office.Interop.Word.Application
. . .working with Word. . .Do
    intRefCount = _
     System.Runtime.InteropServices.Marshal.ReleaseComObject(wrd)
Loop While intRefCount > 0
wrd = Nothing

プロセス内のすべてのクライアントで 1 つの RCW が共有されているので、ReleaseComObject メソッドを使用すると、他のクライアントが COM オブジェクトに依存していても COM オブジェクトを解放することができます。COM オブジェクトが解放された後に .NET ベースのアプリケーションから RCW を操作しようとすると、例外 System.Runtime.InteropServices.InvalidComObjectException が発生し、"基になる RCW から分割された COM オブジェクトを使うことはできません" という追加情報が表示されます。ここで、カウンタを下げることができるように、オブジェクトを使用するアプリケーションがいつ完了したかを知ることが重要です。確認方法の 1 つは、ReleaseComObject への呼び出しを、包含するオブジェクトの Finalizer/Dispose メソッドに置くことです。これにより、.NET ベースのファイナライズと、それに対応する COM オブジェクトの解放が1 対 1 にマッピングされます。

まとめ

この記事では、.NET Framework で COM オブジェクトを開発する際に発生する一般的な問題、および既存のソリューションを変更してこうした問題を修正する方法を説明しています。具体策として、アプリケーションで COM オブジェクトの使用が完了するまで、COM オブジェクトへの参照が適用範囲外になるのを防ぐ必要があります。アプリケーションが COM オブジェクトを解放するとき、COM オブジェクトがメモリから削除されることを確認してください。ここに挙げた 2 つのメカニズムのうち、AppDomain クラスを使用した方法は COM オブジェクトの有効期間を簡単に管理でき、ReleaseComObject を使用した方法は高いパフォーマンスが得られます。

著者紹介

Peter Vogel (MBA、MCSD) は PH&V Information Services の社長です。PH&V は .NET および XML 開発を専門としています。Peter は、Bayer AG、Exxon、Christie Digital、Canadian Imperial Bank of Commerce 各社のイントラネットおよびコンポーネントベースのシステムを設計、構築、およびインストールしました。

彼は Smart Access の編集者であり、『The Visual Basic Object and Component Handbook』 (Prentice Hall 出版) の執筆者でもあります。現在、『Web Parts for SharePoint Services and ASP.NET 2.0』 (Wrox 出版) を執筆中です。Peter は Learning Tree International の講師です。彼の書いた記事は Visual Basic ベースの開発専門の主要雑誌および Microsoft Developer Network (MSDN) ライブラリに掲載されています。Peter は北米、オーストラリア、ヨーロッパで開催される協議会にも参加しています。

この記事は A23 Consulting の協力の下で作成されました。