Implementing a COM server with 'ctypes' "overview":index.html :: "tutorial":tutorial.html :: "reference":reference.html :: "faq":faq.html ( Work in progress: "COM":com.html :: 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 '_' 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":http://starship.python.net/crew/theller/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":http://www.sysinternals.com/ 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.