NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。
いたるところで書かれてはいるものの、個人的に、それその記事ひとつで理解できるというようなズバピタな記事がなかったので学習メモ。ていうか、調べてたらキリがなくなってきたので、いったんフィックスしたい。
ランタイム上で暗躍する RCW
.NET から COM オブジェクトを扱うということは、マネージドからアンマネージにアクセスするということであり、すなわちマーシャリングが必要となる。これは、ランタイム呼び出し可能ラッパー(RCW: Runtime Callable Wrapper)なるしくみによって行われる。
図: クライアント --> RCW --> COM オブジェクト
通常、.NET クライアント(アプリケーション)が直接的に扱うのは、生身の COM オブジェクトではなく、そのラッパーたる RCW である。ランタイム(.NET Framework)によってサポートされるため意識しづらいが、RCW というプロキシを挟んで COM オブジェクトにアクセスしているのだ。これは重要な事実であり、認識しておかなければならないことだ。
COM オブジェクトとメモリ
さて、ここで、COM プログラミングの世界をすこしだけのぞいてみる。COM の規定によれば、COM オブジェクトたるもの「自分のメモリは自分で解放する責任がある」とのこと。そして、それを実現するためには、参照カウントというテクニックが用いられる。
「参照カウント」とはすこしあいまいなネーミングだ。どうせなら「被参照数」と呼びたいところ。要するに、参照カウントとは、その COM オブジェクトが他からどれだけ参照されているかを示す数値である。COM オブジェクトは、自分が使ったメモリを自分で解放するために、自身への参照数を自分で管理していて、参照数が "0"になったら他からの参照がなくなった(=用済み)と判断して、自分自身のメモリを解放するのである。
COM オブジェクトは健気な奴で、いらなくなったら、自らの居場所を明け渡すわけだけれど、だけどもそれ故に、いま誰が必要とし、必要としなくなったのかは、使う側から通達しなくてはならない。因果なものだが、ロジカルな帰結だ。いつまでもメモリに居座らせず、任務を終えた COM オブジェクトは解放してやらないといけない。
GC と参照カウント
.NET の世界に戻ろうか。.NET でメモリ管理といえばガベージ コレクション(GC)である。COM オブジェクトを内包する RCW は、マネージドであり、もちろん GC の管理対象だ。しかし、COM オブジェクトは、自分のメモリは自分で解放する責務を負っている。
ここに、メモリ管理のアプローチの違いからくるミスマッチが生まれる。RCW は GC の管理下にあるが、内包する COM オブジェクトが GC の対象でないので、結局、アプリケーション作成者が RCW を通じて COM オブジェクトが使用するメモリを、参照カウントというしくみで管理しなければならない、ということになる。
RCW に宿る参照カウント
前述したように、.NET の世界から COM オブジェクトにアクセスするには、それをラップした RCW を通じて行うのだが、COM オブジェクト上の参照カウントというのは、プライベートなメンバなのである。
つまりどういうことかというと、RCW は、COM オブジェクト上の参照カウントを直接見ることはできないということであり、それはすなわち、RCW 上にも参照カウントを持たなければならない、ということを意味する。見えないのだから、別立てで、擬似的に管理してやるしかないというわけ。
そう、.NET デベロッパーは、この RCW 上の参照カウントを管理してやるのだ。そして、これが非常にめんどくさい。
Excel における COM オブジェクト
ここからは、実例に沿って説明した方がわかりやすいので、みんな大好き Excel くんにご登場願おう。
レガシー VB では、自動参照カウンタなるものが提供されるので、たとえば、hoge.xls を開いて、1 つ目のシートの A1 セルの値を表示しようと思えば、以下のように書けた。VB6 は手元にないので、VBScript / WSH で書いてみる。
Sub ShowRangeA1()
Dim excel
Dim book
Set excel = CreateObject("Excel.Application")
Set book = excel.Workbooks.Open("hoge.xls")
Dim a1val
a1val = book.Sheets(1).Range("A1").Value
book.Close False
excel.Quit
WScript.Echo a1val
End Sub
これは、参照カウントを VBscript 側で勝手に管理してくれるからこそ可能な書き方である。どういうことか。まずは、セル(Range)を扱うまでに必要となる、Excel が内包する COM オブジェクトの階層構造を知らなければならない。
- Application
- Workbooks
- Workbook
- Worksheets
- Worksheet
- Range
Excel アプリケーションがあって、Excel は MDI だからアプリケーションごとに複数のブックを持っていて、その中から必要なブックを指定し、ブックの中には複数のシートがあるから、そこから特定のシートを指定し、そしてシート上の多数のセルから任意のひとつを指定し、その値を得る。セルの値を取ってくるということは、つまりそういうことであり、上記の要素、その一つひとつがすべて COM オブジェクトなのである。
参照カウント プログラミング
そう、上記に挙げた COM オブジェクトごとにそのラッパーである RCW も存在し、そして、それぞれに参照カウントを持っていて、それらすべてを管理しなければいけないのだ。それが、.NET で COM を扱うということなのだ。文章だけではわかりづらい。ともかく、上記のコードを C# で書き換えてみることとしよう。
public void ShowRangeA1()
{
Excel.Application xlApp = new Excel.Application();
Excel.Workbooks xlBooks = xlApp.Workbooks;
Excel.Workbook xlBook = xlBooks.Open(@"hoge.xls",
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);
Excel.Sheets xlSheets = xlBook.Worksheets;
Excel.Worksheet xlSheet = (Excel.Worksheet)xlSheets[1];
Excel.Range xlCells = xlSheet.Cells;
Excel.Range xlRangeA1 = (Excel.Range)xlCells[1, 1];
string a1val = (string)xlRangeA1.Value2;
Marshal.ReleaseComObject(xlRangeA1);
Marshal.ReleaseComObject(xlCells);
Marshal.ReleaseComObject(xlSheet);
Marshal.ReleaseComObject(xlSheets);
xlBook.Close(false, Type.Missing, Type.Missing);
Marshal.ReleaseComObject(xlBook);
Marshal.ReleaseComObject(xlBooks);
xlApp.Quit();
Marshal.ReleaseComObject(xlApp);
MessageBox.Show(a1val);
}
管理するとは、つまり、RCW をすべて変数に保持しておくということであり、そしてその参照カウントをデクリメントするのが Marshal.ReleaseComObject メソッドである。見るからにめんどくさい。処理の本質ではないコード、タイプ量が激増している。
が、これだけでも十分めんどくさいのに、実は、例外発生時にも確実にデクリメントを行うためには、RCW を生成するごとに、try finally してやらなければいけないのである。詳しくはじゃんぬねっとさんの記事を参考にしていただきたいが、こうなると、超絶ネスト構造の一丁上がりである。
そんなの絶対に書きたくない。なにがあろうと絶対にイヤである。しかるに結局、じゃんぬねっとさんが書かれているように、もし COM オブジェクトを扱うのなら、COM 専用の言語で、つまり、自動参照カウンタが提供される言語で実装するのがベターだ、ということになるのである。
他にも実装の方法はいくつかあるようなのだけれど、いまの僕にはちょっとハードルの高いお話になっているので、その辺は今回は割愛ということで。
参考記事
関連記事
- [C#] 入力データとしての Excel ファイル (2009/03/13)
- [C#] Excel COM をエレガントに扱う試み (2007/07/09)