COM オブジェクト

COM (Component Object Model)は、かなり端折って言うと、プログラミング言語をまたいでクラスやメソッドを使うための規格です。 マイクロソフトが作った規格で、ほぼWindows用(規格はオープンだし、Unix上での利用ガイドもあるものの、Windows以外ではあまり使われない)です。 DirectXなど新し目のWindows APIはCOMで実装されています。また、OfficeやInternet ExplorerなどのWindowsアプリはCOMを介して、自作のプログラムからアプリ中の機能を呼び出すことができます。

.NET Frameworkの型システムは、このCOMの発展形です。

COM 参照

C# からの COM 利用は、Visual Studio を使えば簡単にできます。 Visual Studio 上で、下図のように、「参照の追加」→「COM」→参照したいDLLを選んで「OK」という手順を踏みます。

COM参照

この図の例の場合、MSXML2 という COM ライブラリを参照します。 これで、例えば以下のように、MSXML2 中のクラス(この例ではDOMDocument60クラス)を使えます。

using MSXML2;
using System;

namespace NativeInterop
{
    class ComImportSample
    {
        static void Main()
        {
            var doc = new DOMDocument60();

            if (doc.load("Sample.xml"))
            {
                var s = doc.documentElement;

                foreach (IXMLDOMElement item in s.getElementsByTagName("Item"))
                {
                    var name = item.getAttribute("Name");
                    var value = item.getAttribute("Value");

                    Console.WriteLine($"{name} = {value}");
                }
            }
        }
    }
}

見ての通り、C#からCOMオブジェクトは、普通にC#のクラスっぽく見えます。 造りが古臭いせいで面倒になりがちですが、そこまで違和感なく使えます。

ここで、このコードに対して与えるデータ(Sample.xml)として以下のようなものを用意したとすると、

<?xml version="1.0" encoding="utf-8" ?>
<Sample>
    <Item Name="a" Value="1"/>
    <Item Name="b" Value="2"/>
    <Item Name="c" Value="3"/>
    <Item Name="d" Value="4"/>
</Sample>

以下の結果が得られます。

a = 1
b = 2
c = 3
d = 4

RCW と CCW

前節の「COM参照」をすると、コンパイラーが以下のようなクラスを生成します。

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MSXML2
{
    [CompilerGenerated, CoClass(typeof(object)), Guid("2933BF96-7B36-11D2-B20E-00C04F983E60"), TypeIdentifier]
    [ComImport]
    public interface DOMDocument60 : IXMLDOMDocument3, XMLDOMDocumentEvents_Event
    {
    }
}
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MSXML2
{
    [CompilerGenerated, Guid("2933BF86-7B36-11D2-B20E-00C04F983E60"), TypeIdentifier]
    [ComImport]
    public interface IXMLDOMElement : IXMLDOMNode
    {
        void _VtblGap1_37();
        [DispId(99)]
        [MethodImpl(MethodImplOptions.InternalCall)]
        [return: MarshalAs(UnmanagedType.Struct)]
        object getAttribute([MarshalAs(UnmanagedType.BStr)] [In] string name);
        void _VtblGap2_5();
        [DispId(105)]
        [MethodImpl(MethodImplOptions.InternalCall)]
        [return: MarshalAs(UnmanagedType.Interface)]
        IXMLDOMNodeList getElementsByTagName([MarshalAs(UnmanagedType.BStr)] [In] string tagName);
    }
}

要は、C-Style 関数の時にDllImport属性を使ったように、 COM オブジェクトに対しては ComImport という属性を使います。

このような、C# から COM オブジェクトを呼び出すためのラッパー クラスをRCW (Runtime Callable Wrapper)と呼びます。

逆に、詳細はここでは省略しますが、C# で書いたクラスを COM 側から使う手段もあって、そちらはCCW (COM Callable Wrapper)と呼ばれます。

No PIA

Ver. 4.0

.NET Framework 3.5以前では、RCW を介してCOM呼び出しに少し問題がありました。

先ほど例示したように、COM参照すると RCW と呼ばれるラッパークラスが生成されます。 プログラム中で使っている型・メソッドだけが残されていて、不要なメンバーはありません。

ここで問題になるのは、複数のライブラリ中にそれぞれ別個に RCW が生成された場合です。 .NET では、「アセンブリ+名前」の組み合わせで型の所在を検索するので、同名・同機能であっても、別のライブラリ中に定義されていたら別の型扱いになります。

そこで、複数のライブラリからCOM参照したい場合、.NET Framework 3.5以前では、PIA (Primary Interop Assembly: プライマリ相互運用アセンブリ)といって、RCW 専用のCOMオブジェクト中の全メンバーを参照したライブラリを作って使う必要がありました(参考: PIA に関するドキュメント)。

PIAは、まじめに作ると結構馬鹿でかいファイルになってしまいます。 それが嫌で、.NET Framework 4からは、アセンブリやメンバー定義が違っていても、RCW の GUIDが同じなら同じ型とみなして扱うという特殊処理が入りました(この処理は No PIA と呼ばれています)。 この処理によって、PIAなしでも複数のライブラリからCOMオブジェクトの参照ができるようになりました。

dynamic と COM 呼び出し

Ver. 4.0

C# 4.0の dynamic (参考: 「動的型付け変数」)を使えば、「COM 参照」すらなしで COM オブジェクトを呼び出せます。

例えば、先ほどのコードは以下のように書き換えることもできます。

using System;

namespace NativeInterop
{
    class ComLateBindingSample
    {
        public static void Main()
        {
            var t = Type.GetTypeFromProgID("MSXML2.DOMDocument");
            dynamic doc = Activator.CreateInstance(t);

            if (doc.load("Sample.xml"))
            {
                var s = doc.documentElement;

                foreach (var item in s.getElementsByTagName("Item"))
                {
                    var name = item.getAttribute("Name");
                    var value = item.getAttribute("Value");

                    Console.WriteLine($"{name} = {value}");
                }
            }
        }
    }
}

インスタンス doc を作るところが new から CreateInstance に代わって、変数の型が dynamic になっただけで、そこから先のコードはほぼ同じです。 これで、「COM 参照」は必要なくなり、RCW は実行時に動的に作られます。

この機能は、RCW が不要になる分、プログラムのサイズが縮むというメリットがあります。 一方、開発時には、どのクラスにどういうメンバーがあるといった情報が得られる、コード補完が効かなくなるというデメリットがあります。 なので、開発時には「COM参照」を使って開発して、 最後にdynamicに置き換えて、「COM参照」を消してからコンパイルするという手順を踏むとよいかもしれません。

更新履歴

ブログ