COM サーバを ctypes で実装する

注:このドキュメントは Thomas Heller 氏の ctypes モジュールドキュメント を翻訳したものです.オリジナルのドキュメントは ctypes のページ にあります.

概要 :: チュートリアル :: リファレンス :: faq

( 作業中: COM :: COM サンプル )

注意: 作業中です

このウォークスルーでは,単純な exe 形式のサーバ内の COM オブジェクトを 実装する方法を,すでに型ライブラリを作成しているものとして説明します.

この記事の全てのコードは ctypes 配布物の ctypes\com\samples\server サブディレクトリで入手することができます.

Note: 現在, ctypes.com ではローカルサーバだけを実装することが できますが,これは将来変更される予定です.

型ライブラリを書く

この例は idl ファイルから始まります.これはバイナリ形式の型 ライブラリにコンパイルされるもので,自分でコンパイルするには MIDL コンパイラが必要です.

以下に idl ファイル sum.idl を示します:

        /* A TypeLibrary, compiled to sum.tlb */

        [
            uuid(90810cb9-d427-48b6-81ff-92d4a2098b45),
            version(1.0),
            helpstring("Sum 1.0 Type Library")
        ]
        library SumLib
        {
            importlib("stdole2.tlb");
            /* a dual interface, derived from IDispatch */
            [
                    object,
                    dual,
                    uuid(6edc65bf-0cb7-4b0d-9e43-11c655e51ae9),
                    helpstring("IDualSum Interface"),
                    pointer_default(unique)
            ]
            interface IDualSum: IDispatch {
                    [id(100)] HRESULT Add(double a, double b, [out, retval] double *result);
            };

            [
                    uuid(2e0504a1-1a23-443f-939d-869a6c731521),
                    helpstring("CSum Class")
            ]
            /* a coclass, implementing this interface */
            coclass CSum
            {
                    [default] interface IDualSum;
            }
        };

この型ライブラリは CSum と言う名前の COM オブジェクトを記述して います. CSumIDualSum という名の dual インタフェースを実装 しています.このインタフェースはメソッド Add 一つだけを持ちます. このメソッドは二つの浮動小数点パラメタを受理し,その結果を三つ目の 引数で返します.この三つ目の引数は double へのポインタでなければ なりません.

メソッドの戻り値は HRESULT で,オートメーションインタフェースでは 典型的なものです. IDispatch から導出されたインタフェースで必要と されている,このメソッドの dispid は 100 です.

ISum インタフェースは dual インタフェースで,動的ディスパッチ と直接的な vtable 呼び出しの両方を行うことができます.

midl コンパイラを midl sum.idl /tlb sum.tlb として動作させると, 型ライブラリ sum.tlb が生成されます.

Python COM インタフェース記述を作成する

次のステップでは, IDualSum インタフェースへの Python ラッパ を生成します.これは手作業で行うこともできますが,幸運なことに, ctypes\com\tools ディレクトリには readtlb.py ユーティリティ ツールがあります.

readtlb.py は型ライブラリのファイル名をコマンドライン上で指定して 走らせ,現状では Python ソースコードを標準出力に書き出します.従って ファイルに出力をリダイレクトしなければなりません. 生成されたコードが接尾辞 _gen.py を使うという慣習から,コマンドライン は以下のようになります:

      python ctypes\com\tools\readtlb.py sum.tlb > sum_gen.py

この操作は sum_gen.py ファイルを生成します.以下にその内容を 簡単のために不要な部分を削って示します:

        from ctypes import *
        from ctypes.com import IUnknown, GUID, STDMETHOD, HRESULT
        from ctypes.com.automation import IDispatch, BSTR, VARIANT

        class COMObject:
            # later this class will be used to create COM objects.
            pass

        ##############################################################################

        # The Type Library
        class SumLib:
            'Sum 1.0 Type Library'
            guid = GUID('{90810CB9-D427-48B6-81FF-92D4A2098B45}')
            version = (1, 0)
            flags = 0x8
            path = 'C:\\sf\\ctypes_head\\win32\\com\\samples\\server\\sum.tlb'

        ##############################################################################

        class IDualSum(IDispatch):
            """IDualSum Interface"""
            _iid_ = GUID('{6EDC65BF-0CB7-4B0D-9E43-11C655E51AE9}')

        IDualSum._methods_ = IDispatch._methods_ + [
            (STDMETHOD(HRESULT, "Add", c_double, c_double, POINTER(c_double))),
        ]

        ##############################################################################

        class CSum(COMObject):
            """CSum Class"""
            _reg_clsid_ = '{2E0504A1-1A23-443F-939D-869A6C731521}'
            _com_interfaces_ = [IDualSum]

型ライブラリ自体のクラス SumLib ,インタフェースの IDualSum , コクラス CSum が生成されているのが分かります.

通常, SumLibCSum といったクラスがインスタンス化して使われる ことはなく,単にいくつかの属性を運ぶためのオブジェクトに過ぎません. インタフェース IDualSum は CSum COM オブジェクトが使われる際に インスタンス化されます.

XXX _methods_ について記述する.

XXX dispids の記述はどこに?

COM オブジェクトを実装する

インタフェースへのラッパを手に入れたので,今度はこのインタフェースを 実装している COM オブジェクトを書く番です.

我々のオブジェクトは dual な COM インタフェースを実装しているので, ctypes.com.automation モジュールで提供されている DualObjImpl 基底クラスを使います:

      from sum_gen import IDualSum, CSum, SumLib

      class SumObject(DualObjImpl):
          _com_interfaces_ = [IDualSum]
          _typelib_ = SumLib
          _reg_progid_ = "ctypes.SumObject"
          _reg_desc_ = "Sum Object"
          _reg_clsid_ = CSum._reg_clsid_

          ...

XXX なぜ CSum を mixin クラスとして使わないのか?

_com_interfaces_ 属性は我々のオブジェクトが実装している COM インタフェースからなる配列で,最初のものがデフォルトのインタフェース となります.全てのインタフェースは ctypes.com.IUnknown のサブクラス でなければなりません.他のインタフェースをリストに付け加えることも できますが,今回は行いません.

_typelib_DualObjImpl から導出するクラスで必要な属性です. この属性は sum_gen.SumLib の属性を持っているオブジェクトで なければなりません. readtlb.py は生成されたモジュール内に 型ライブラリファイルの絶対パス名を書き込むので,ファイルをどこか 別の場所に移動する場合にはこのパス名を変更しなくてはなりません. 型ライブラリへのパスは型ライブラリの登録に使われ, guidversion , および flags 属性を使って型ライブラリを実行時にレジストリから 読み出して,dual インタフェースの IDispatch 部分を実装します.

_reg_progid_ および _reg_desc_ は文字列の属性で,COM オブジェクト の名前を与えます.後者の方はオプションです.

_reg_clsid_ 属性は文字列で guid が入っており,上のコードでは 型ライブラリから値が取り出されます.

現時点では Add メソッドの実装がありません.このメソッド無しには COM オブジェクトは何も行うことができないので,以下のコードを追加します:

          def IDualSum_Add(self, this, a, b, presult):
              presult[0] = a + b
              return 0

メソッドの名前は雛型 <interface_name>_<method_name> に合致 しなければなりません.ここで interface_name はこのメソッドが属している インタフェースの名前で, method_name はメソッドのラッパモジュール内での 名前と同じものです.現状では method_name だけの Add を使うことも できますが,おそらくユーザ定義のインタフェースではこれはお勧めできません. しかし,基底インタフェースである IUnknownIDispatch で使うことは できます.ただし異論はあるかもしれませんね (YMMV).

パラメタはどうなっているのでしょうか? self はコメントする必要は ないでしょう. ab ,および presult はインタフェースメソッドで 使われるパラメタで, a および b は Python の float 型です.これらは ctypes によって自動的に c_double に変換されます. presultc_double への ポインタ です.ここでは二つの数字を加算して,その 結果を presult に記憶します.式 "presult[0] = a + b" が加算結果 を presult が指す場所に記憶することを思い出してください.同様の C のコードは以下のようになるでしょう:

      /* double a, b, *presult */
      *presult = a + b;

これは以下のように書くこともできます:

      /* double a, b, *presult */
      presult[0] = a + b;

this パラメタは整数で, COM の this ポインタを表します.この値は 全ての ctypes COM メソッド実装に渡されます.この値は時に便利なことが ありますが,ほとんどの場合無視されるだけです.

main プログラム

COM オブジェクトの実装は完成しましたが, main プログラムがありません. Mark Hammond の win32com.server と非常によく似ていますが, UseCommandLine メソッドが全ての作業を行います:

      if __name__ == '__main__':
          from ctypes.com.server import UseCommandLine
          UseCommandLine(SumObject)

UseCommandLine はコマンドラインスイッチ /regserver/unregserver ,および /embedding に反応します.後者は COM によって起動された際に exe サーバによって自動的に与えられます. 全てのスイッチは大小文字の区別があり, "/" の代わりに "-" で はじめることができます.

/regserver および /unregserver は COM オブジェクト と型ライブラリ を Windows レジストリに登録したり登録解除したります.

COM オブジェクトを試す

準備はととのいました.最初のステップとして,コマンドラインから COM オブジェクトを登録します:

      python sum.py -regserver

うまく行ったなら,以下の内容が出力されるはずです:

      LocalServer32 C:\Python22\python.exe C:\sf\ctypes_head\win32\com\samples\server\sum.py
      Registered Typelib C:\sf\ctypes_head\win32\com\samples\server\sum.tlb
      Registered COM class __main__.SumObject

登録解除も同様にうまくいくはずです:

      python sum.py -unregserver

以下の内容が出力されます:

      deleted LocalServer32
      deleted ProgID

しかしこれからこのオブジェクトを使いたいので,再度登録してくださいね.

oleview プログラムを持っているなら,Object Classes -> All Objects の下にレジストリエントリ "Sum Object" を, Type Libraries の下に 型ライブラリ "Sum 1.0 Type Library (Ver 1.0)" を, Interfaces の 下にインタフェース "IDualSum" を見つけることができるはずです.

oleview を使うことで,COM オブジェクトのインスタンスを生成する こともできます.メニューエントリ Object -> CoCreateInstanceFlags -> CLSCTX_LOCAL_SERVER がチェックされていることを確かめ, "Sum Object" エントリ をダブルクリックしてください.

何も表示されない dos 窓がポップアップし (python.exe は pythonw.exe と違ってローカルサーバとして登録されているため), oleview が オブジェクトに全ての既知のインタフェースをリクエストするまで すこしかかった後,実装されているインタフェースが Sum Object の下に 表示されます. IUnknownIDispatch ,および IDualSum は 期待通りの結果ですが,他のインタフェースも表示されます.これらは COM 自体によって実装されているもので,おそらく内部的な使用のための ものです: 私は Windows XP を使っていますが, IClientSecurityIMarshalIMultiQI ,および IProxyManager があります.

Sum Object コンテキストメニューからインスタンスを開放すると COM オブジェクトは再び破壊され,dos 窓は閉じます.

COM オブジェクトを使う

Sum オブジェクトを win32com で利用すると,動的ディスパッチで動作 します.また makepy を起動した後でも使うことができます (私の知る限り, このプログラムはディスパッチインタフェースを呼ぶはずですが, そうではなくて vtable メソッドを直接呼ぶのかな?):

      from win32com.client import Dispatch
      d = Dispatch("ctypes.SumObject")
      print d.Add(3.14, 3.14)

ctypes からのオブジェクト利用は, sum_gen ラッパモジュールが 利用可能なためさほど難しくありません. vtable のカスタムインタフェースを 直接呼び出すことを思い出すだけです.そこで,結果を受け取るための c_double を生成して,そのポインタを渡します:

      from sum_gen import CSum
      from ctypes.com import CreateInstance
      from ctypes import c_double, byref

      sum = CreateInstance(CSum)

      result = c_double()
      sum.Add(3.14, 3.14, byref(result))
      print result

this パラメタは使われず,クライアント側のコードで要求されることも ないので注意してください.

CreateInstance 関数は COM オブジェクトを生成し, デフォルトの COM インタフェースへのポインタを取得します.COM インタフェースを 2番目のパラメタとして渡して,他のインタフェースポインタを取得することも できます.

XXX CreateInstance をもっと詳細に?

COM オブジェクトを開放する

py2exe は Python スクリプトを exe ファイルに変換することができます.これには python22.dll_ctypes.pyd_sre.pyd ,および _winreg.pyd だけが必要です.生成される exe ファイル内に型ライブラリをリソースとして 埋め込むためには --typelib sum.tlb コマンドラインオプションを 付けて py2exe を起動しなければなりません.

exe ファイルを --windows フラグ付きでビルドして,DOS ウィンドウ を消すことができますが,トレースバックを見るためには sys.stdout および sys.stderr をどこか別の場所にリダイレクト する必要があります.

出力をログファイルにリダイレクトすることができます.また以下の コード断片を使って Windows の OutputDebugString 関数を呼び出す こともできます.この出力は例えば,デバッガに出力されるか, Sysinternals DebugView ユーティリティ に表示されます:

      from ctypes import windll
      class Output:
          def write(self, text):
              windll.kernel32.OutputDebugStringA(text)
      import sys
      sys.stdout = sys.stderr = Output()

このドキュメントを書いている時点では,まだリリースされていない py2exe のバージョン,少なくとも 0.3.4 以降が必要です.



Page updated: Fri Nov 28 15:50:54 2003