Windows Programming in Python: Creating COM Servers

In an earlier article I discussed accessing COM components from within Python programs. However, I left a question dangling, namely, can COM servers be created in Python, and can they be accessed by applications created in other languages or platforms such as Visual Basic? The answer is an emphatic yes.

In this discussion I will be tackling the whys and wherefores of creating COM servers in Python. In the first section the focus will be on COM servers. In the second section and third sections, I will detail the necessary steps for creating a COM server in Python. The fourth section will provide a real world example of developing a COM server and calling it from Visual Basic. That’s the outline for this discussion.

More About COM

In the world of COM there is a clear line of separation between accessing COM components and implementing COM objects. While accessing COM objects, method calls are made on them, provided externally using the interfaces exposed by the COM object. When implementing COM objects, which are also known as COM servers, the object has to publish or expose its interfaces so that external clients can use them.

This difference is evident from the way the interfaces and methods implemented are utilized either to access a COM component or expose the functionalities of a COM component. The interface under consideration is IDispatch. Programs that use an IDispatch object must call the GetIDsOfNames() and Invoke() methods to perform method calls or property reference, whereas the objects that wish expose their functionalities via IDispatch must implement the GetIDsOfNames() and Invoke() methods, providing the logic for translating between names and IDs, and so on. In the world of PythonCOM, the former is called client-side COM and the latter is server-side COM.

The next topic I will cover is the types of server-side COM objects. There are three types of server-side COM objects:

  1. In-Proc Server
  2. Local Server
  3. Remote Server

The difference in the types is based on where the COM server is executing. The COM server could be executing in the process space of the client, in its own separate process space, or on a different system. Here are the details.

In-Proc Server

When the COM server is loaded into the process space of the client, it is known as an In-Proc server. In other words, "A Component Object Model (COM) object that is executing in the same process space as the user of that object is called an In Process object (InProc for short)." An In-Proc server is implemented as a DLL in other languages.

Local Server

When the COM server is loaded and executed in its own process space, it is called a Local Server. By definition, "When the user code is executing in the process space of Application A, and the COM object is executing in the process space of Application B, interface method invocations by A on interfaces implemented on B clearly must cross process boundaries, then the COM objects in Application B are cross-process objects, or Local Servers." A Local Server is implemented in the form of an EXE.

Remote Server

When the COM server is implemented as an EXE but executed on a remote machine, it is known as a Remote Server. Remote Servers come under the heading of Distributed COM or DCOM.

The important aspect of these differences is that they are mutually inclusive. That means a COM component can be registered as a combination of any of the above three types. Now that the types of COM servers have been introduced, let’s see how to develop COM servers in Python step-by-step.

{mospagebreak title=Developing COM Servers Step by Step}

Developing a COM server in Python is easier than in any other language. If the process is broken down into steps there are primarily two steps, which are developing the class and annotating the class with attributes. Furthermore, in the first step you must decide on which model the class must be based. 

Developing the class

The philosophy of COM is that a fixed interface must be built and the class must be modeled around it. In essence, once the interface has been defined, it must not be changed. To work with this philosophy there are three design patterns that can be used. They are:

  • COM base class, pure Python subclass
  • Pure Python base class, COM subclass
  • COM interface, Python delegate

The COM interface, Python delegate pattern is the most commonly used. The reason for this is the ability to develop the class first.

In the pattern of a COM base class, pure Python subclass, a base class has to be defined and exposed as a COM server. The methods in the base class perform no tasks. The base class works as a COM interface. Then a sub class, which implements the base class, has to be developed; this class provides the actual services. In other words a class that implements the base class performs the tasks. This pattern is most appropriate when designing a class whose main function is to be used from COM and not from Python.

Now let’s look at a pure Python base class, COM subclass pattern. It is in a way the opposite of the previous pattern. Here, the existing COM class is inherited from a Python base class. The COM class can be differentiated by the the attributes used to annotate the class, which will be discussed shortly.

Finally, let’s examine the COM interface, Python delegate pattern. In this case, the COM interface is defined. The class that defines the COM interface has variables internal to it that point to the pure Python counterpart. The Python counterpart is known as the delegate. The methods of class that represent the COM interface translate their arguments, return values as needed, and forward them to the delegate.

Let’s take the third pattern as an example. For the sake of the example, suppose there is a BookSet class that implements the methods of the COM interface. Now if the third pattern is applied to define a COM interface, it would look like this:

class COMBookSet:
  _reg_clsid_ = ‘{38CB8241-D698-11D2-B806-0060974AB8A9}’
  _reg_progid_ = ‘Doubletalk.BookServer’
  _public_methods_ = ['double']

  def __init__(self):
    self.__BookSet = doubletalk.bookset.BookSet()

  def double(self, arg):
    # trivial test function to check it’s alive
    return arg * 2

The __BookSet variable points to the pure Python class BookSet. This is how it delegates the task to the pure Python class. You will observe that there are certain attributes such as _reg_clsid_. The explanation of these attributes is coming up next.

{mospagebreak title=Annotating the Class with Attributes}

Every Python class representing a COM interface must expose itself as a COM object. To achieve this, the PythonCOM framework requires certain attributes to be associated with the Python class that needs to be exposed as a COM object. There are three main attributes, which are:

  • _public_methods_
  • _reg_progid_
  • _reg_clsid_

All of the above attributes are required in exposing a Python class as a COM object – from simplest to the most complex COM objects.

The _public_methods_ attribute takes a list of all those methods that need to be exposed via the COM. The _reg_progid_ attribute is used to assign the ProgID for the new object, that is, the name that the users of this object must use to create the object. It is the human readable name of the object. Finally, the _reg_clsid_ attribute sets the unique CLSID for the object. These IDs must not be copied. Instead new ones should be created using pythoncom.CreateGuid().

Let’s take a look at the same example of the COM server:

class COMBookSet:
  _reg_clsid_ = ‘{38CB8241-D698-11D2-B806-0060974AB8A9}’
  _reg_progid_ = ‘Doubletalk.BookServer’
  _public_methods_ = ['double']

  def __init__(self):
    self.__BookSet = doubletalk.bookset.BookSet()

  def double(self, arg):
    # trivial test function to check it’s alive
    return arg * 2

     

The _reg_clsid_ contains a 32 bit class id for the object. The _reg_progid_ contains the human readable name using which the object can be called. Here the _public_methods_ contains the list of all the methods exposed via COM. Here there is only one — double. That completes this section. In the next section, I will be creating a real world example and call it from Visual Basic.

{mospagebreak title=Creating COM Servers in the Real World}

In the previous sections I introduced the basic requirements for creating a COM server in Python. Now let’s put the concepts into practice. The example will provide a simple functionality — splitting a given string. There are two parts of the application:

  1. PyCOMServer.py – The COM server implemented in Python
  2. SampleClent.vb – The client implemented in VB

Let’s start with the server. First let’s define the Python class contained in PyCOMServer.py – PythonUtilities

class PythonUtilities:
  def SplitString(self, val, item=None):
    import string
    if item != None: item = str(item)
    return string.split(str(val), item)

This class defines a single method, SplitString, that takes two arguments: item, which is the string to be split; and the value contained in val on the basis of which string has to be split.

The next step is to embed the attributes so that the class can expose its functionalities through the COM:

class PythonUtilities:
  _public_methods_ = [ 'SplitString' ]
  _reg_progid_ = "PythonServer.Utilities"
  # NEVER copy the following ID
  # Use "print pythoncom.CreateGuid()" to make a new one.
  _reg_clsid_ = "{41E24E95-D45A-11D2-852C-204C4F4F5020}"

  def SplitString(self, val, item=None):
    import string
    if item != None: item = str(item)
    return string.split(str(val), item)

Since there is only one method that needs to be exposed, the list for _public_methods_ contains only the SplitString method. Next, the name by which it can be called is given via _reg_progid_ which is PythonServer.Utilities. Finally the class id generated using pythoncom.CreateGuid().

Next is the main code that is required to register and run the COM server.

class PythonUtilities:
  _public_methods_ = [ 'SplitString' ]
  _reg_progid_ = "PythonDemos.Utilities"
  # NEVER copy the following ID
  # Use "print pythoncom.CreateGuid()" to make a new one.
  _reg_clsid_ = "{41E24E95-D45A-11D2-852C-204C4F4F5020}"

  def SplitString(self, val, item=None):
    import string
    if item != None: item = str(item)
    return string.split(str(val), item)

# Add code so that when this script is run by
# Python.exe, it self-registers.
if __name__==’__main__’:
  print "Registering COM server…"
  import win32com.server.register
  win32com.server.register.UseCommandLine(PythonUtilities)

Next comes the client. For developing the client start the Macro editor either in MS Word or MS Excel. Enter the name for the macro. The implementation of the macro is as follows:

Set PythonUtils = CreateObject("PythonDemos.Utilities")
response = PythonUtils.SplitString("Hello from VB")
for each Item in response
  MsgBox Item
Next

That brings us to the end of this discussion. The application developed doesn’t apply the design patterns, as certain advanced aspects of COM programming have to be covered yet. That’s the agenda for the next part: advanced aspects of COM servers. Till then…

Google+ Comments

Google+ Comments