COM相互運用とReflector

昨日紹介した(id:NyaRuRu:20040714#p1)MSDN MagazineのBITSの記事で,Jason Clark氏はCLRのCOM相互運用を用いてC#からBITSを利用しています.

Figure 1のコードは,属性があまりにもたくさん指定されているので(ComImportAttribute,GuidAttribute,MarshalAsAttributeなど),実際のコードが霞んでしまっている。実は,実際のコードなど存在しないのである。このインターフェイス,クラス定義は,CLRがBITS COM APIを呼び出すために使う,メタデータの形でのマーシャリング用のプレースホルダのようなものである。

実際のコードが必要ないことは,Figure 1をIDLの置き換えだと思えばそれ程不思議でもないでしょう.IDLはC++似の言語ですが, .NETの方法は属性に対応した好きな言語を利用できます.
Clark氏は,Figure 1のクラス定義をどのように作成したのでしょうか.これについてはその後に記述があります.

Figure 1のコードと,InteropBits.csサンプルファイルのすべての実装は,手作業で作ったものである。.NET Framework SDKには,ターゲットCOM APIを記述するTLBファイルがあれば,同様のコードを作成してマネージアセンブリにコンパイルするTlbImp.exeというツールが添付されている。BITS APIにはLIBファイルが添付されていないが,プラットフォームSDKには,インターフェイスを記述するBits.idlというインターフェイス定義ファイルが添付されている。

そこで,私はMIDL.exeツール(これもプラットフォームSDKに添付されている)でBits.idlからTLBファイルを作成し,次にTlbImp.exeを使ってTLBファイルに対応するマネージアセンブリを作った。次に,ILDasm.exeツールを使って,TlbImp.exeが生成したアセンブリを中間言語に逆アセンブルした。最後に,この中間言語をガイドラインとしてC#コードを書き,より使いやすく,また正しくなるように調整を加えた。C#を使うときのCOM Interopに対するこのようなアプローチは確かに面倒な作業だが,意のままの結果が得られるという点では,優れている。

最終的にC#で書き直すことを除けば,このプロセスは .NET Framework 開発者ガイドの「標準ラッパーのカスタマイズ」にも記されている通りです.
http://www.microsoft.com/japan/msdn/library/default.asp?url=/japan/msdn/library/ja/cpguide/html/cpconcomcallablewrapper.asp
さて,多くの場合COM Interfaceに関する最も多くの情報を持っているのはIDLファイルです.COMの抱えていた問題点として,MIDL.exeで生成したTLBにはIDLに記述されていた全ての情報が記されていないというものがあります.特にマーシャリングに関するいくつかの属性が失われてしまうことは,後で .NET Assemblyを編集しなければならない原因となります.IDLから直接 .NET AssemblyまたはManaged言語のソースコードに出力できればベストです.しかしこのようなツールは .NET Framework SDKに付属していません.Borlandが出しているidl2csというツールがCORBA形式のIDLをサポートしているようですが,Microsoft IDL (MIDL)に対応しているかは不明です.
http://www.borland.co.jp/tips/janeva/janeva001.html
Clark氏は「C#を使うときのCOM Interopに対するこのようなアプローチは確かに面倒な作業だが,意のままの結果が得られるという点では,優れている。」と述べています.私も同じ意見で,次のような理由で特にマーシャリング属性を微調整する際にはC#で直接記述する方が優れていると考えます.IDLの変更による調整はプロセスの工数が増加し,TLBを経由するために .NETで可能な全てのマーシャリング機能を利用できないという問題があります.MSILを直接編集する方法は,MSILが直接入力を前提に作られた言語ではないこと,入力支援も受けられないことなどの困難があります.さらにC#と別の言語を組み合わせる場合, Visual C# 2003のIDEでは最終的な .NET Assemblyを1つにまとめることができません.このような理由で,もし私が氏と同じ立場に立ったとしても氏と同じように最終的にはC#で書き直したでしょう.
さて,結局C#コードへ変換するのであれば,最初から .NET Assemblyを .NET Refectorで逆コンパイルする方法が使えそうです.
http://www.aisto.com/roeder/dotnet/
一般に特定のManaged言語で表現できるのは.NET Assemblyで表現可能なことのサブセットですが,今回はマーシャリング後の型を用いてクラスまたは構造体の定義が出来れば十分です*1.困った場合はIntPtrで受け渡しする方法があるので,Refectorの出力結果を多少修正すればほとんどの場合でうまく行くでしょう.
実際に逆コンパイルを試してみました.VisualStudio .NET 2003付属のPlatform SDKに含まれるbits.idl (BITS 1.5)からTLBを経て .NET Assemblyを生成し,さらに .NET Refectorで逆コンパイルを行います.このとき属性情報を出力するチェックを忘れずにオンにします.以下はFigure 1に該当する部分を抜き出して,ソースに改行を補って整形したものです.

[ComImport]
[Guid("5CE34C0D-0DC9-4C1F-897C-DAA1B78CEE7C")]
[CoClass(typeof(BackgroundCopyManagerClass))]
public interface BackgroundCopyManager : IBackgroundCopyManager
{
}

[ComImport]
[InterfaceType(1)]
[Guid("5CE34C0D-0DC9-4C1F-897C-DAA1B78CEE7C")]
public interface IBackgroundCopyManager
{
    // Methods
    [MethodImpl(MethodImplOptions.InternalCall | 3)]
    void CreateJob(
        [In, MarshalAs(UnmanagedType.LPWStr)] string DisplayName,
        [In, ComAliasName("BackgroundCopyManager.BG_JOB_TYPE")] BG_JOB_TYPE Type,
        [ComAliasName("BackgroundCopyManager.GUID")] out GUID pJobId,
        [MarshalAs(UnmanagedType.Interface)] out IBackgroundCopyJob ppJob);
    [MethodImpl(MethodImplOptions.InternalCall | 3)]
    void EnumJobs(
        [In] uint dwFlags,
        [MarshalAs(UnmanagedType.Interface)] out IEnumBackgroundCopyJobs ppenum);
    [MethodImpl(MethodImplOptions.InternalCall | 3)]
    void GetErrorDescription(
        [In, MarshalAs(UnmanagedType.Error)] int hResult,
        [In] uint LanguageId,
        [MarshalAs(UnmanagedType.LPWStr)] out string pErrorDescription);
    [MethodImpl(MethodImplOptions.InternalCall | 3)]
    void GetJob(
        [In, ComAliasName("BackgroundCopyManager.GUID")] ref GUID jobID,
        [MarshalAs(UnmanagedType.Interface)] out IBackgroundCopyJob ppJob);
}

Clark氏のコードと比較してどうでしょうか? 最終的にはこの出力コードをさらにチェックする必要がありますが,多くの人にとってMSILのコードを読むよりは不快指数の低い作業になるでしょう.実際のソースの修正もほとんどはエディタの置換・検索機能で済んでしまうでしょうが,Visual C# 2005のリファクタリング機能を試すのにもちょうどよい材料かもしれません.

*1:もちろん一般論としては,VisualBasic .NETではポインタ型を扱えず,JScript .NETでは構造体の宣言が出来ないといった言語ごとの差異に注意が必要です.