C#からのCOMの利用 - COMクライアント

いちごパック > COM/ActiveXの解説 > C#からのCOMの利用 - COMクライアント
C#にはCOMクライアントをサポートするための機能が用意されています。
  • CoCreateInstance()を内部的に呼び出す機能
  • キャストをQueryInterface()呼び出しに変換する機能
  • IDispatchを介したメソッド呼び出しのダイナミックバインディング機能

  • クラスの作成

    C#では、CoCreateInstance()によるコクラス(IUnknownを継承したCOMクラス)をnewで作成できます。 次のようにComImportとCLSIDを与えた空のクラスを書くと、コクラスが定義されます。
    [ComImport]
    [Guid("425C76F4-E126-456C-837D-1EE96DB89126")]
    class CIchigo2
    {
    }
    
    ComImport属性のクラスがnewされると、C#は内部的にCoCreateInstance()を呼び出します。 そのままではIUnknown相当であり、クラスメソッドの呼び出しができません。 作成したコクラスは、使いたいインターフェースにキャストして使います。
    Win32 APIを利用してCOMインターフェースへの参照を直接得ることもできます。 この場合、HRESULTを戻り値とするC++の関数にPreserveSig=falseを与えると、C#がHRESULTを例外に変換してくれます。
    DllImport("ole32.dll", PreserveSig = false)
    public static extern void CreateBindCtx(
        uint reserved, out IBindCtx ppbc);
    

    キャストの方法

    C#は、キャストをQueryInterface()呼び出しに変換する機能を備えています。 インターフェースは次のいずれかの方法で得られます。
  • ダイナミックバインディング(IDispatch)を使う
  • System.Runtime.InteropServices.ComTypesの定義を利用する
  • コマンドラインツールtlbimpを使ってタイプライブラリを.NETのDLLに変換し、それをusingで読み込む
  • インターフェースをC#コードとして手動で用意する
  • 利用が簡単な順にダイナミックバインディング、ComTypes、tlbimp、手動となります。
    コクラスを使いたい場合は、 いずれかの方法を使ってインターフェースの定義を追加し、 コクラスをnewして、利用するインターフェースにキャストして使うことになります。

    ダイナミックバインディングを使う方法

    この場合は、変数型としてdynamicを指定するだけです。
    IDispatchであるがわかっているインターフェースであれば、 次の例のように、dynamic型の変数に保存すればメソッドを呼び出せます。 メソッド引数の型については、IDispatchが自動的に解決してくれます。
    
    using System.Runtime.InteropServices;
    
    
    [ComImport]
    [Guid("316E547D-CBB4-495F-A839-DC4414842C8F")]
    class CIchigo3
    {
    }
    
    class Ichigo3Call {
        public static void Main(string[] args) {
            dynamic inst = new CIchigo3();
            inst.Write("ichigopack COM sample 3 (c#)\n");
        }
    }
    
    
    この方法は簡単ですが、スクリプト言語からアクセスする場合と同様、 IDispatchを介するため、少し遅くなります。

    System.Runtime.InteropServices.ComTypesを使う方法

    System.Runtime.InteropServices.ComTypesはシステムのCOMインターフェースの一部を与えます。
    using System.Runtime.InteropServices.ComTypes;
    class Program {
        [DllImport("ole32.dll", PreserveSig = false)]
        public static extern void CreateBindCtx(
            uint reserved, out IBindCtx ppbc);
        static void Main(string[] args) {
            IBindCtx ctx;
            CreateBindCtx(0, out ctx);
            ...
        }
    }
    

    tlbimpを使う方法

    コマンドラインツールtlbimpを使うと、 例として、次のichigo4のidlファイルを考えます。
    //
    // Copyright (c) 2016 The Ichigopack Project (http://ichigopack.net/).
    //
    
    import "unknwn.idl";
    import "oaidl.idl";
    
    // IIchigo4 interface
    [
        object,
        dual,
        uuid(964FC28B-E237-4C33-A5E6-5C384021145A),
        pointer_default(unique)
    ]
    interface IIchigo4 : IDispatch
    {
        HRESULT Write( BSTR bstrWrite );
    };
    
    // Type library
    [
        version(1.0),
        uuid(74e6d628-b3b0-4ea2-89b3-c85be7bb9e5a)
    ]
    library Ichigo4TypeLib
    {
        importlib("stdole32.tlb"); // import IDispatch
        interface IIchigo4;
        [ uuid(b899e97f-e7f5-47c3-875d-807be4b5bcf2) ]
        coclass Ichigo4
        {
            interface IIchigo4;
        };
    };
    
    このidlから生成したタイプライブラリichigo4com.tlbをC#から呼び出したい場合は、 まず次のコマンドを実行します。
    c:\> tlbimp ichigo4com.tlb
    
    このコマンドを実行すると、ライブラリ名称Ichigo4TypeLibに対応した、 Ichigo4TypeLib.dllという名前のdllが生成されます。
    このdllには、Ichigo4TypeLibという名前空間内に、 IIchigo4インターフェースとIchigo4コクラスが収められています。 したがって、このライブラリとインターフェースは次のコードで呼び出せます。
    using System.Runtime.InteropServices;
    // tlbimp ichigocom4_coclass_if.tlb
    using Ichigo4TypeLib;
    
    class Ichigo4Call {
        public static void Main(string[] args) {
            Ichigo4 inst = new Ichigo4();
            inst.Write("ichigopack COM sample 4 (c#)\n");
        }
    }
    
    コマンドラインからビルドする場合は、次のようにします。
    c:\>csc ichigo4call.cs /r:Ichigo4TypeLib.dll 
    
    tlbファイルの代わりにtlbファイルを含むdllファイルがある場合でも、 同じ方法でC#から呼び出せます。
    ここではC++で書かれたCOMサーバを例としましたが、 tlbファイルを提供するCOMサーバは、 どんな言語で書かれていても問題ありません。

    手動でinterfaceを用意する方法

    手動で用意する場合は、 インターフェースがIUnknownとIDispatchのどちらから継承されたクラスであるかを、 属性として与えます。
    例えばシステムのクラスであるISequentialStreamを、 自分で与えるには次の例のようにします。
    using System;
    using System.Runtime.InteropServices;
    
    [ComImport]
    [Guid("425C76F4-E126-456C-837D-1EE96DB89126")]
    class CIchigo2
    {
    }
    
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("0c733a30-2a1c-11ce-ade5-00aa0044773d")]
    interface ISequentialStream
    {
        void Read([Out,
            MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)]
            byte[] pv, uint cb, out uint pcbRead);
        void Write([In,
            MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)]
            byte[] pv, uint cb, out uint pcbWritten);
    }
    
    class Ichigo1Call {
        public static void Main(string[] args) {
            ISequentialStream inst = (ISequentialStream)new CIchigo2();
            uint cbWritten;
            string unistr = "ichigopack COM sample 2 (c#)\n";
            byte[] data = System.Text.Encoding.ASCII.GetBytes(unistr);
            inst.Write(data, (uint)data.Length, out cbWritten);
        }
    }
    
    C#の引数をNative(COM)の引数に変換する方法として、 何も指定しなければデフォルトの方法が使われますが、 デフォルトの方法で配列などを扱うと正しく動かないでしょう。
    引数がデフォルトの振る舞いで都合が悪い場合は、 MarshalAsという属性を追加します。 MarshalAsはC#の引数をNativeの引数に変換する方法を指定します。
    引数を配列として扱いたい場合は、属性としてUnmanagedType.LPArrayを与えます。 このときSizeParamIndex=N(Nは0から始まる引数番号)を与えると、配列のサイズを与えることができます。
    UnmanagedTypeには多くのタイプが存在します。 以下にその一部を示します。
    タイプ内容
    I11バイトsigned整数
    I22バイトsigned整数
    I44バイトsigned整数
    I88バイトsigned整数
    U11バイトunsigned整数
    U22バイトunsigned整数
    U44バイトunsigned整数
    U88バイトunsigned整数
    R44バイトfloat
    R88バイトfloat
    BStrCOM文字列
    IUnknownCOMのIUnknown型
    IDispatchCOMのIDispatch型
    LPArray配列。配列のサイズはSizeConst(固定長の要素数)、あるいはSizeParamIndex(変数から要素数を取得)により求められる。
    LPStrANSI文字列
    LPWStrUnicode文字列
    これらの機能はC#からアンマネージドコードを呼び出す時に使われるものです。 COM以外でも使えます。

    解説一覧

  • COM/ActiveXの解説ページ 目次
  • 機能とセットアップ
  • COMクライアント
  • COMサーバ