COM のリフレクションツールを公開します

今回紹介する ComUtility は、COM (Compornent Object Model) オブジェクト用のリフレクションツールです。各種デバッガで COM オブジェクトを参照するとプロパティの一覧やその値がツリー表示されたりしますが、そういう機能を実現する物です。ツールと言っても UI は存在せず、リフレクションに必要な機能を提供する COM オブジェクトです。

## どんな問題を解決するのか？

COM は、異なるモジュール同士を結びつける、Windows OS の血脈とも言える仕様と実装です。COM を利用する事で私たちはIE の機能や Excel の機能を無理なく自分のアプリケーションで再利用できます。COM のより詳しい記事は Wikipedia 等をご覧下さい。

 * https://ja.wikipedia.org/wiki/Component_Object_Model

COMを利用して IE や Excel のアプリケーションを開発しようとしたら、当然ですがアクセスするための仕様を憶えなければならないです。マイクロソフト等大手の COM ベンダーのサイトには詳しい仕様書があるのですが、なかなか欲しい情報にたどり着けない事も多いです。例えば以下のような事で困ります。

 * CreateObject() するために必要な ProgID は分かったが、それを提供しているタイプライブラリが自分のマシンのどのフォルダに存在するのか分からない。
 * Unknown インターフェースが返された時に、どのインターフェースで QI すれば有意な情報が得られるのか分からない。そもそもどのインターフェースで QI 可能なのかも分からない事がある。
 * 返された COM オブジェクトが持つプロパティの一覧を取得するのは結構面倒臭い。簡単になんとかしたい。
 * 言語環境によってはオブジェクトのプロパティ名を文字列として扱えない。扱いにくい。
 * ある COM オブジェクトが返された時に、それを構築するのに必要な CoClass や ProgID が分からない。そのため自分で同じ物を生成する術が不明だったりする。
 * 使っている言語環境の配列や連想配列が、どういうインターフェースを持っているか分からず、自作 COM オブジェクトに配列等を渡す手段に困る。
 * 作った人がもう居なかったりして何がなんだかサッパリ分からない。

ComUtility はこれらの問題解決を手助けします。

## 入手方法とライセンス

MITライセンスです。ソースはGitHubに置きました。

 * https://github.com/matsumoto-sp/ComUtility

バイナリは Releases ページから取得して下さい。

 * https://github.com/matsumoto-sp/ComUtility/releases

## 利用方法と動作環境

ComUtiliry は独立したアプリケーションでは無く、COM オブジェクトです。プログラムコード内でオブジェクトを構築して利用される事を想定しています。例えば JScript や VBScript では、以下のように構築します。

### VBScript

```VB:build.vbs
' リフレクターの構築
Set reflector = CreateObject("ComUtilityLib.Reflector")
```

### JScript

```JavaScript:build.js
// リフレクターの構築
var reflector = WScript.CreateObject("ComUtilityLib.Reflector");
```

Windows OS 環境化で COM サポートのある言語環境なら大抵利用可能だと思います。 VBS, JScript, C#.NET, VB.NET, VBA 等で動作を確認しました。

## インストール

ダウンロードした msi ファイルをダブルクリックし、後は指示に従って下さい。現在、インストール先が選べないのですが、インストールされるファイルは以下2つです。

 * C:\Program files\ComUtility\ComUtility.dll
 * C:\Program files (x86)\ComUtility\ComUtility.dll

## アンインストール

ダウンロードした msi ファイルを再実行するか、またはコントールパネルの「プログラムと機能」から「Com Utility」を削除して下さい。



## サンプルプログラム

まずはサンプルプログラムを紹介します。以下のスクリプトは IE コンポーネントの各種情報を表示します。JScript 版と VBScript 版を紹介しますが、同じ内容です。

### VBScript

```VB:get_ie_info.vbs
' リフレクターの構築
Set reflector = CreateObject("ComUtilityLib.Reflector")

' IEオブジェクトの構築
Set ie = CreateObject("InternetExplorer.Application")

' IE オブジェクトが持つインターフェースの一覧を得る
WScript.Echo "# IEコンポーネント・インターフェース一覧"
For Each item In reflector.ScanIf(ie)
    WScript.Echo(" * " & item.Name)
Next
WScript.Echo

' IE オブジェクトのプロパティ一覧
WScript.Echo("# IEコンポーネント・プロパティ一覧")
For Each item In reflector.ScanDispIf(ie).Properties
    WScript.Echo(" * " + item.Name)
Next
WScript.Echo

' IE オブジェクトを構築できる ProgID を検索
WScript.Echo("# IEコンポーネント・CLSID, ProgID 一覧")
For Each item In reflector.SearchCoClass(ie)
    WScript.Echo(" * " & item.CLSID & ", " & item.ProgID)
Next
WScript.Echo
```
### JScript


```JavaScript:get_ie_info.js
// リフレクターの構築
var reflector = WScript.CreateObject("ComUtilityLib.Reflector");

// IEオブジェクトの構築
var ie = WScript.CreateObject("InternetExplorer.Application");

// IE オブジェクトが持つインターフェースの一覧を得る
WScript.Echo("# IEコンポーネント・インターフェース一覧");
var ifs = reflector.ScanIf(ie);
var eIfs = new Enumerator(ifs);
while (!eIfs.atEnd()) {
    WScript.Echo(' * ' + eIfs.item().Name);
    eIfs.moveNext();
}
WScript.Echo('');

// IE オブジェクトのプロパティ一覧
WScript.Echo("# IEコンポーネント・プロパティ一覧");
var props = reflector.ScanDispIf(ie).Properties;
var eProps = new Enumerator(props);
while (!eProps.atEnd()) {
    WScript.Echo(' * ' + eProps.item().Name);
    eProps.moveNext();
}
WScript.Echo('');

// IE オブジェクトを構築できる ProgID を検索
WScript.Echo("# IEコンポーネント・CLSID, ProgID 一覧");
var coclasses = reflector.SearchCoClass(ie);
var eCoClasses = new Enumerator(coclasses);
while (!eCoClasses.atEnd()) {
    WScript.Echo(' * ' + eCoClasses.item().CLSID + ', ' + eCoClasses.item().ProgID);
    eCoClasses.moveNext();
}
WScript.Echo('');
```

実行結果は以下の通りです（環境によって多少の差があるかも知れません）。

```
F:\work\tools>cscript sample1.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

# IEコンポーネント・インターフェース一覧
 * IUnknown
 * IMultiQI
 * IDispatch
 * IWebBrowserApp
 * IWebBrowserPriv
 * IWebBrowserPriv2
 * IEFrameAuto
 * IXMicTestMode
 * IWebBrowserPriv6
 * IWebBrowserHandler
 * IServiceProvider
 * IWebBrowserPriv3
 * IHlinkFrame
 * ITargetNotify
 * ITargetFrame2
 * ITargetFramePriv
 * IConnectionPointContainer
 * ITargetFramePriv2
 * IOleCommandTarget
 * IUrlHistoryNotify
 * IWebBrowser2
 * ITargetFrame
 * IWebBrowser
 * ISecMgrCacheSeedTarget

# IEコンポーネント・プロパティ一覧
 * Offline
 * Silent
 * RegisterAsBrowser
 * RegisterAsDropTarget
 * TheaterMode
 * AddressBar
 * Resizable

# IEコンポーネント・CLSID, ProgID 一覧
 * {EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B}, Shell.Explorer.1
 * {8856F961-340A-11D0-A96B-00C04FD705A2}, Shell.Explorer.2
 * {0002DF01-0000-0000-C000-000000000046}, InternetExplorer.Application.1
 * {D5E8041D-920F-45E9-B8FB-B1DEB82C6E5E},
 * {C08AFD90-F2A1-11D1-8455-00A0C91F3880},
```

# ComUtilityLib.Reflector の仕様

ComUtilityLib.Reflector は、以下4つのメソッドを持つオブジェクトです。

 * ScanIf
 * ScanDispIf
 * SearchCoClass
 * IsCollection

### ScanIf

引数で与えられたオブジェクトが持つインターフェースを列挙します。返される結果は配列で、それぞれの要素は以下のプロパティを持つオブジェクトです。

 名前 | 内容 | 備考
 ----+----+----
 Name | インターフェース名 | 空欄の場合あり。
 IID | インターフェースの GUID文字列 | {} カッコで括られた文字列です。
 TypeLib | 参照されるタイプライブラリのGUID | 空欄の場合あり。

#### ScanIf が行っている事

システムに登録された COM インターフェースの一覧は、レジストリの HKCR\Interface 以下に登録されています。ScanIF は HKCR\Interface を列挙し、システムに登録された全インターフェースで QI を実行し、利用可能なインターフェースの一覧を得ています。こういう構造のため、すごく遅いです。環境にもよると思いますが、2～3秒程度掛かる処理だと思って下さい。

なお、あるオブジェクトで利用可能なインターフェースの一覧は、 [IMultiQI](https://docs.microsoft.com/en-us/windows/desktop/api/objidl/nn-objidl-imultiqi "IMultiQI") で取得可能です。しかし IMultiQI を実装していないオブジェクトも多いので、そんな時は重宝します。

### ScanDispIf

引数で与えられたオブジェクトが持っているメソッドとプロパティの一覧を得ます。返される結果は以下2つのプロパティを持ちます。

 名前 | 内容 | 備考
 ----+----+----
 Functions | メソッドの一覧 |
 Properties | プロパティの一覧 |

Fucntions は以下のプロパティを持つオブジェクトの配列です。

 名前 | 内容 | 備考
 ----+----+----
 Name | 名前 | 
 Vt | 戻り値の型 | 戻り値の型を示す数値で [VariantType](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.visualbasic.varianttype?view=netframework-4.8)列挙型です。
 Invkind | 呼び出し方法 | メソッドなのか、プロパティなのか等の情報を返します。 [INVKOKEKIND](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.comtypes.invokekind?view=netframework-4.8)列挙型です。
 IsCollection | N/A | 無視して下さい。
 Value | N/A | 無視して下さい。
 ValueUseful | N/A | 無視して下さい。

Properties は以下のプロパティを持つオブジェクトの配列です。

 名前 | 内容 | 備考
 ----+----+----
 Name | 名前 | 
 Vt | プロパティの型 | プロパティの型を示す数値で [VariantType](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.visualbasic.varianttype?view=netframework-4.8)列挙型です。
 Invkind | N/A | 有意な値が入っていません。無視して下さい。
 IsCollection | 配列かどうか | このプロパティがコレクションオブジェクトの時に真になるブール値です。
 Value | 値 | プロパティの値です。
 ValueUseful | 値の有効性 | プロパティの値は取得できない場合があります。この値は Value が有効な値であるかどうかを示すブール値です。

#### ScanDispId が行っている事

インターフェースのタイプライブラリにアクセスし、メソッドやプロパティの情報を取得し、invoke を行い値を取得しています。多くのデバッグ環境が提供している事と同じような事をやっています。なお、コレクションオブジェクトを検出しても、その中を再帰的に処理する事はしません。必要に応じてプログラムでカバーして下さい。その際、Parent 等と名前が付いたプロパティを辿る時は要注意です。無限ループにならないように適切に処理する必要があります。

### SearchCoClass

引数で与えられたインターフェースを持つオブジェクトを構築可能な CoClass を検索します。返される結果は以下のプロパティを持つ配列です。

 名前 | 内容
 ----+----
 CLSID | CLSID です。
 InprocServer32 | 実装されているファイルのフルパスです。
 ProgID | ProgID です。
 TypeLibInfo | タイプライブラリの情報です（下記参照）。

TypeLibInfo は以下のプロパティを持つオブジェクトです。

 名前 | 内容
 ----+----
 GUID | タイプライブラリに紐づけられた GUID を示す文字列です。中かっこ {} で括られています。
 Name | 名前です。開発環境の参照設定で列挙される文字列と同じ物です。
 VerMajor | メジャーバージョンコードの整数値です。
 VerMinor | マイナーバージョンコードの整数値です。
 TypeLibFile32 | 32ビット環境用のタイプライブラリのフルパスです。
 TypeLibFile64 | 64ビット環境用のタイプライブラリのフルパスです。

#### SearchCoClass が行っている事

COM オブジェクト（の持つインターフェース）から ProgID や CoClass を得たいという要望は私自身の事も含め多いと思うのですが、それは厳密には無理な話しです。なぜならインターフェースに1対1に紐づく物はインターフェースID であり、ProgID ではないからです。しかし実際にはひとつのインターフェースとひとつの ProgID が1対1で対応している事は非常に多いです。SearchCoClass はレジストリとタイプライブラリを検索し、その紐づけを確認します。

システムに登録されたタイプライブラリはレジストリの HKCR\TypeLib 以下に登録されています。ここに登録されているタイプライブラリをすべて開き、それぞれの CoClass が持っているインターフェースを調べ、入力として与えられたオブジェクトのインターフェースと一致するかどうかを調査しています。このような処理のため、SearchCoClass の1回目の実行は ScanIf 以上に重いです。10秒ぐらいかかるかも知れません。2回目以降はレジストリ情報をインデクシングしますので速くなります。

なお、結果は配列として返され複数の CoClass を得る場合がありますが、その中のどれかが入力オブジェクトに一致している保証はありません。そのうちのどれかひとつかも知れないし、どれでも無いかも知れないです。前述の通り厳密な紐づけは原理的にできないからなのですが、それでも列挙された CoClass は、入力オブジェクトと同一のインターフェースを備えている事が保証されます。

## 最後に

WEB アプリケーションが全盛の昨今ですが、IEコンポーネントや Excel コンポーネントの有用性は今でも高く、COM を経由して他言語からこれらを利用するデスクトップアプリケーションの需要はまだまだあると思います。私も普段は WEB 開発がメインなのですが、久しぶりに Excel 案件等が続いています。COM との付き合いは今後もありそうなので、以前から書き溜めていた断片的なツールを整理しておこうと思い、ついでに公開してみました。

だいぶ今さら感のあるツールだとは思いますが、よかったら使ってみて下さい。