Implementing a COM server with ctypes

overview :: tutorial :: reference :: faq

( Work in progress: COM :: COM sample )

Warning: work in progress

This walkthrough describes how to implement a simple COM object in an exe-server, assuming that you already have or will create a type library.

All the code for this article is available in the ctypes\com\samples\server subdirectory in the ctypes distribution.

Note: Currently only local servers can be implemented with ctypes.com, but this will change in the future.

Writing the type library

This example starts with an idl file which will be compiled to a binary type library, you need the MIDL compiler to compile it yourself.

Here is the idl file 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;
            }
        };

This type library describes a COM object named CSum which implements a dual interface named IDualSum. This interface has only one method named Add. The method accepts two floating point parameters and returns it's result in the third, which must be a pointer to a double.

The method's result type is a HRESULT, which is typical for automation interfaces. The dispid of this method, which is required for interfaces derived from IDispatch, is 100.

The ISum interface is a dual interface which allows access through dynamic dispatch as well as through direct vtable calls.

Running the midl compiler midl sum.idl /tlb sum.tlb produces the type library sum.tlb.

Creating the Python COM interface description

The next step is to create a Python wrapper for the IDualSum interface. This can be done manually, but fortunately there's also a tool for it, the readtlb.py utility in the ctypes\com\tools directory.

readtlb.py is run with the filename of a type library on the command line, and currently writes Python source code to standard output. So you should run it with the output redirected to a file. Per convention the generated code uses a _gen.py suffix, so the command line should be:

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

This creates the sum_gen.py file, and here is it's contents with unused parts removed for clarity:

        from ctypes import *
        from ctypes.com import IUnknown, GUID, STDMETHOD, HRESULT
        from ctypes.com.automation import IDispatch, BSTR, VARIANT, dispinterface, \
                                          DISPMETHOD, DISPPARAMS, EXCEPINFO

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

        # 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:
            """CSum Class"""
            _reg_clsid_ = '{2E0504A1-1A23-443F-939D-869A6C731521}'
            _com_interfaces_ = [IDualSum]

We see that classes have been created for the type library itself SumLib, the interface IDualSum, and the coclass CSum.

Typically, the SumLib and CSum classes will never by instantiated, they are simply objects carrying some attributes, the interface IDualSum will be instantiated when the CSum COM object is used.

XXX Explain _methods_

XXX Where are the dispids?

Implementing the COM object

Now that we have the interface wrapper, it's time to write the COM object implementing this interface.

Since our object implements a dual COM interface, we use the DualObjImpl baseclass provided by the ctypes.com.automation module:

      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 Why not use CSum as a mixin class?

The _com_interfaces_ attribute is a sequence of COM interfaces our object implements, the first one being the default interface. All interfaces must be subclasses of ctypes.com.IUnknown. We could add other interfaces to this list as well, but we don't at the moment.

The _typelib_ attribute is required for classes deriving from DualObjImpl. It must be an object having the attributes of the sum_gen.SumLib class. Note that readtlb.py writes an absolute pathname for the type library file into the generated module, this must be changed if the file is moved somewhere else. The type library path is used for registration of the type library, the guid, version, and flags attributes are used to load the type library at runtime via the registry to implement the IDispatch part of the dual interface.

_reg_progid_ and _reg_desc_ are string attributes providing names for the COM object, the latter is optional.

The _reg_clsid_ attribute is a string containing a guid, in the code above it is taken from the type library.

So far the implementation of the Add method is missing, without this our COM object won't be able to do anything, so we add this code:

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

The name of the method must match the template <interface_name>_<method_name> where interface_name is the name of the interface this method belongs to, and method_name is the name of the method as it is in the wrapper module. Currently it is possible to also use only the method_name Add, but this is probably not recommended for user defined interfaces. It is, however, used in base interfaces like IUnknown or IDispatch. YMMV.

What about the parameters? self does not require any comment. a, b, and presult are the parameters used in our interface method, a and b are Python floats, automatically converted from c_double by ctypes, and presult is a pointer to a c_double. We add the two numbers together and store the result in presult, remember that the expression '"presult[0] = a + b"' stores the sum in the location pointed at by presult. Similar C code would be:

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

and it could also be written in this way:

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

The this parameter is an integer representing the COM this pointer, it it passed to all ctypes COM method implementations. Sometimes it can be useful, but most of the time it should simply be ignored.

The main program

Our COM object is complete, only the main program missing. Very similar to Mark Hammond's win32com.server, there's a UseCommandLine method doing all the work for us:

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

UseCommandLine responds to the command line switches /regserver, /unregserver, and /embedding, the latter will be automatically provided for an exe server if it is started by COM. All switches are case insensitive and can also start with "-" instead of "/".

/regserver and /unregserver will register or unregister our COM object and the type library in the Windows registry.

Testing the COM object

We're ready to go. The first step is to register the COM object with this command line:

      python sum.py -regserver

If all goes well, this should be printed:

      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

Unregistering should work as well:

      python sum.py -unregserver

prints this:

      deleted LocalServer32
      deleted ProgID

but we want to use it, so we should register it again.

If you have the oleview program, you should be able to find the registry entries "Sum Object" under Object Classes -> All Objects, the type library under Type Libraries as "Sum 1.0 Type Library (Ver 1.0)", and the interface under Interfaces as "IDualSum".

Using oleview you can also create an instance of the COM object. Make sure that the menu entry Object -> CoCreateInstanceFlags -> CLSCTX_LOCAL_SERVER is checked, and double click on the "Sum Object" entry.

An empty dos box pops up (since python.exe is registered as local server, not pythonw.exe), and after a short moment in which oleview tries to request all known interfaces from the object you should see the implemented interfaces displayed as Sum Object. IUnknown, IDispatch, and IDualSum are what we expected, but there are other interfaces as well, which are implemented by COM itself, probably for internal use: IClientSecurity, IMarshal, IMultiQI, and IProxyManager is what I see on Windows XP.

Releasing the instance from the Sum Object context menu destroys the COM object again, and the dos box closes.

Using the COM object

Using the Sum object with win32com works for dynamic dispatch, and also after running makepy (IIUC, this call the dispatch interface, or does it call the vtable methods directly?):

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

Using the object with ctypes is not much more complicated, since we have the sum_gen wrapper module available, and this makes life easy. We just have to remember that we call the vtable custom interface directly, so we have to create a c_double to receive the result and pass a pointer to it:

      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

Note that the this parameter is never used or required in client code.

The CreateInstance function creates the COM object and retrieves a pointer to the default COM interface, you could also pass a COM interface as the second parameter to retrive another interface pointer.

XXX More on CreateInstance ?

Deploying the COM object

py2exe is able to convert the Python script into an exe-file, which only needs python22.dll, _ctypes.pyd, _sre.pyd, and _winreg.pyd. It must be run with the --typelib sum.tlb command line option to embed the type library as resource into the created exe file.

You can build the exe-file with the --windows flag to get rid of the DOS box, but you should redirect sys.stdout and sys.stderr somewhere else to see tracebacks.

You can redirect output to a log file, or you can use this snippet to call Windows OutputDebugString function, the output of which can be displayed by debuggers or Sysinternals DebugView utility, for example:

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

You need a recent version of py2exe, version 0.4.1 or later should work.


SourceForge.net Logo
Page updated: Sat Mar 19 20:45:25 2005