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.
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
.
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?
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.
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.
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 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
?
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.