C#からのCOMの利用 - COMサーバ

いちごパック > COM/ActiveXの解説 > C#からのCOMの利用 - COMサーバ
C#では、DLLタイプのCOMサーバを実装できます。 COMサーバは次の手順で実装できます。
  • IUnknownまたはIDispatchから継承し、必要なメソッドを追加したインターフェース(interface)を定義する。
  • 定義したインターフェース(interface)を継承したクラス(class)を実装する。
  • COMサーバは、.NET付属のregasmを利用して、DLLサーバとしてシステムに登録できます。
    COMの基本インターフェースである IUnknownとIDispatchはシステムに組み込まれており、 C#ではinterfaceやclassの属性として指定します。 そのため、これらのインターフェースは実装する必要がありません。 実装することもできません。

    interfaceの宣言

    C#では、interfaceを使うと、純粋仮想クラスを定義できます。 COMのインターフェースを定義したい場合は、 次のようなCOMに必要な属性を指定してinterfaceを定義します。
    属性内容
    ComVisible外部からインターフェースを参照可能とする場合はtrue。
    InterfaceTypeインターフェースの種類(IUnknown、IDispatch、Dual)を指定する。
    GuidinterfaceのIIDを指定する。
    例えば、Write()というメソッドを持ち、デュアルインターフェースを備えた ICSichigo1というインターフェースは次のように定義できます。
    [ComVisible(true)]
    [Guid("2FBCAFB9-4AEB-4295-87C4-4725BE75BBE3")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface ICSichigo1
    {
        void Write(string logstr);
    }
    
    これだけで、C++言語でidlファイルを使った定義と同じように、 タイプライブラリ出力可能なしたインターフェースになります。

    classの実装

    インターフェースを定義した後は、定義したインターフェースを備えたクラスを実装します。 クラスの場合もインターフェースと同様に、COM特有の属性を指定します。
    属性内容
    ComVisibleタイプライブラリ(.tlb)等でクラスを公開する場合はtrue。
    InterfaceType通常はNoneを指定する。それ以外の値を用いると、継承に加えて実装するインターフェースの種類を指定する形になる。
    GuidクラスのCLSIDを指定する。
    ProgIdクラスのProgIdを指定する。ProgIdを指定しておくと、このクラスをCLSIDだけでなくProgIdでも参照できるようになる。
    ComDefaultInterfaceインターフェース指定をしなかった場合に、ここで指定したインターフェースをデフォルトとする。
    属性指定する点以外は通常のクラスとほとんど同じです。 実装もそれほど難しくありません。
    [ComVisible(true)]
    [Guid("27BCBDA1-F045-4322-BCB4-10D746E081F4")]
    [ProgId("csichigo1.LogWriter")]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(ICSichigo1))]
    public class CSichigo1 : ICSichigo1
    {
        public void Write(string logstr) {
            File.AppendAllText(@"c:/tmp/csichigo1.txt", logstr);
        }
    }
    

    アセンブリ属性の追加

    COMサーバとして公開する.NETのアセンブリでは、ComVisibleにtrueを指定し、 自分のアセンブリのために生成したGUIDを指定します。 アセンブリ属性はSystem.Reflectionに収められており、COMに関連する項目は次の通りです。
    属性内容
    assembly: ComVisibleタイプライブラリ(.tlb)を生成する場合はtrueを指定します。
    assembly: Guid自分で生成したタイプライブラリのGUIDを指定します。他のGUIDをコピーせず、guidgen等で生成したものを指定します。
    Visual Studioの統合環境では、 これらの情報はAssemblyInfo.csというファイルに収められ、 他のソースコードとリンクされるようになっています。 統合環境を使っている場合、これらの情報はプロジェクトのプロパティで編集できます。
    コマンドラインでビルドしている場合は、 他の属性と同じように、ソースファイルに直接追加して問題ありません。 例えば、次のようにアセンブリ属性を指定できます。
    [assembly: AssemblyTitle("csichigo1")]
    [assembly: AssemblyProduct("csichigo1")]
    [assembly: AssemblyDescription("csichigo1")]
    [assembly: AssemblyConfiguration("")]
    [assembly: AssemblyCompany("Ichigopack")]
    [assembly: AssemblyCopyright("Copyright (C) 2018 The Ichigopack Project")]
    [assembly: AssemblyVersion("1.0.0.0")]
    [assembly: AssemblyFileVersion("1.0.0.0")]
    
    [assembly: ComVisible(true)] [assembly: Guid("0F6A07A0-A000-403E-AF5C-8AAF45CC0C07")]

    ビルドとシステムへの登録

    コマンドラインからビルドする場合は、次のようになります。 regasmはCOMサーバをシステムに登録するコマンドです。 regasmの実行には管理者権限が必要になります。
    prompt> csc /target:library csichigo1.cs
    prompt> regasm /tlb csichigo1.dll
    
    regasmに/tlbを指定するとタイプライブラリを出力します。
    32bit環境では32bit、64bit環境では64bitのCOMサーバになります。 DLLサーバは32bitと64bitが混在しているとCoCreateInstance()で失敗しますので、 実行環境に留意してご登録ください。

    クライアント

    C#で実装されたCOMサーバはデュアルインターフェースを備えますので、 IDispatchを介さずC++から呼び出せます。
    Visual C++では、tlbを#import指定するとヘッダの代わりに使えます。
    #import "csichigo1.tlb" no_namespace no_implementation \
        no_smart_pointers raw_interfaces_only named_guids
    
    この#import行により、csichigo1.tlbに対応するヘッダが csichigo1.tlhというファイル名で生成されます。 なお、Write()引数のstringはCOMのBSTR型になります。
    生成されるファイルcsichigo1.tlhは以下のようなヘッダファイルです。
    struct ICSichigo1 : IDispatch
    {
        virtual HRESULT __stdcall
            Write(BSTR logstr) = 0;
    };
    struct CSichigo1;
    
    extern "C" const GUID __declspec(selectany) LIBID_CSichigo1 = {0x0f6a07a0,0xa000,0x403e,{0xaf,0x5c,0x8a,0xaf,0x45,0xcc,0x0c,0x07}}; extern "C" const GUID __declspec(selectany) IID_ICSichigo1 = {0x2fbcafb9,0x4aeb,0x4295,{0x87,0xc4,0x47,0x25,0xbe,0x75,0xbb,0xe3}}; extern "C" const GUID __declspec(selectany) CLSID_CSichigo1 = {0x27bcbda1,0xf045,0x4322,{0xbc,0xb4,0x10,0xd7,0x46,0xe0,0x81,0xf4}};
    #importにより、C++のコードで、 このヘッダファイルを#includeしてビルドすることに相当します。
    したがって、C++からは次のコードで直接呼び出せることがわかります。 説明のため、エラーチェックは省略しています。
        ICSichigo1* pObj = NULL;
        HRESULT hr = CoCreateInstance(
             CLSID_CSichigo1, NULL,
             CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER |
             CLSCTX_LOCAL_SERVER,
             IID_ICSichigo1, (LPVOID*)&pObj);
        BSTR bstr = SysAllocString(L"ichigopack c# sample 1 from c++\n");
        pObj->Write(bstr);
        SysFreeString(bstr);
    
    CSichigo1はデュアルインターフェースを備えているため、 C++以外の言語からでも呼び出せます。 例えば、Windows Scriptからは次のように呼び出せます。
    <job id="ichigocom1">
      <script language="JScript">
        var obj = new ActiveXObject("csichigo1.LogWriter")
        obj.Write("c# ichigopack COM sample 1\n");
      </script>
    </job>
    
    もちろん、C++であってもIDispatchを介したアクセスも可能です。 IDispatchの利用はC++からは手間がかかりますが、例えば次のように呼び出せます。 説明のため、エラーチェックは省略しています。
        IDispatch* pObj = NULL;
        HRESULT hr = CoCreateInstance(
             CLSID_CSichigo1, NULL,
             CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER |
             CLSCTX_LOCAL_SERVER,
             IID_IDispatch, (LPVOID*)&pObj);
        LCID lcid = GetSystemDefaultLCID();
        LPOLESTR szFuncNames[1] = {L"Write"};
        DISPID dispFunc[1] = {0};
        VARIANT v, vret;
    
    VariantInit(&v); VariantInit(&vret); hr = pObj->GetIDsOfNames(IID_NULL, szFuncNames, 1, lcid, dispFunc); v.bstrVal = SysAllocString( L"ichigopack c# sample 1 from c++ (IDispatch)\n");
    DISPPARAMS dp; EXCEPINFO ei; UINT uArgErr = 0; dp.rgvarg = &v; dp.rgdispidNamedArgs = NULL; dp.cArgs = 1; dp.cNamedArgs = 0; hr = pObj->Invoke(dispFunc[0], IID_NULL, lcid, DISPATCH_METHOD, &dp, &vret, &ei, &uArgErr);
    VariantClear(&v); VariantClear(&vret);

    ソースコード

    最後に、csichigo1.csの完全なソースコードを載せておきます。
    //
    // Copyright (c) 2018 The Ichigopack Project (http://ichigopack.net/).
    //
    using System;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.IO;
    
    [assembly: AssemblyTitle("csichigo1")]
    [assembly: AssemblyProduct("csichigo1")]
    [assembly: AssemblyDescription("csichigo1")]
    [assembly: AssemblyConfiguration("")]
    [assembly: AssemblyCompany("Ichigopack")]
    [assembly: AssemblyCopyright("Copyright (C) 2018 The Ichigopack Project")]
    [assembly: AssemblyVersion("1.0.0.0")]
    [assembly: AssemblyFileVersion("1.0.0.0")]
    
    [assembly: ComVisible(true)]
    [assembly: Guid("0F6A07A0-A000-403E-AF5C-8AAF45CC0C07")]
    
    namespace csichigo1
    {
        [ComVisible(true)]
        [Guid("2FBCAFB9-4AEB-4295-87C4-4725BE75BBE3")]
        [InterfaceType(ComInterfaceType.InterfaceIsDual)]
        public interface ICSichigo1
        {
            void Write(string logstr);
        }
    
        [ComVisible(true)]
        [Guid("27BCBDA1-F045-4322-BCB4-10D746E081F4")]
        [ProgId("csichigo1.LogWriter")]
        [ClassInterface(ClassInterfaceType.None)] // ICSichigo1 only
        [ComDefaultInterface(typeof(ICSichigo1))]
        public class CSichigo1 : ICSichigo1
        {
            public void Write(string logstr) {
                File.AppendAllText(@"c:/tmp/csichigo1.txt", logstr);
            }
        }
    }
    

    解説一覧

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