David Zemens

How to Build a COM-Visible Executable with Python

Recently I had to figure out how to make a COM-visible executable from python. I’d worked on something similar about 5 years ago in python2, which was a bit easier of a process since py2exe creates a DLL, but that library is no longer maintained and it doesn’t work for python3.

Below is a very simple example class. The easy part was exposing the class to COM locally which can be done via command line: python controller.py --register .

import pythoncom, sys, win32com.server.register, logging

class Controller(object):
    _reg_progid_ = "TestController.Application"
    _reg_desc_ = "Python Test COM Server"
    # generate your own UID via uuid.uuid4() or pythoncom.CreateGuid()
    _reg_clsid_ = '{9e243d9c-96c7-4c7c-987d-893f1f55c064}'  
    _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
    _public_methods_ = ['hello']

    def __init__(self):
        pass

    def hello(self, name):
        return f'Hello, {name}'

if __name__ == '__main__':
    if '--register' in sys.argv or '--unregister' in sys.argv:
        win32com.server.register.UseCommandLine(Controller)

Once you register the class, you can test it out in python:

from comtypes.client import CreateObject
c = CreateObject('TestController.Application')
c.hello('David')

Or from Visual Basic:

Dim c as Object
Set c = CreateObject("TestController.Application")
MsgBox c.hello('David')

But the tricky part was figuring out how to make this distributable. I used python’s pyinstaller to create an EXE from the python code, but ran into a number of problems & errors along the way. I needed a way to serve it, so I modified the if __name__ == '__main__' guard:

if __name__ == '__main__':
    print(__name__)
    print(sys.argv)
    try:
        if '--register' in sys.argv or '--unregister' in sys.argv:
            win32com.server.register.UseCommandLine(Controller)
        else:
            win32com.server.localserver.serve([ Controller._reg_clsid_ ])
    except:
        logging.exception('Oops!')
        input()

Somewhere along the way I realized that I could pass command-line arguments to the EXE.

controller.exe --register

This revelation was not something that I found anywhere in my researching — there are plenty of examples of making com-visible classes in python (all pretty much the same as I had initially) but none of them went so far as to make an executable. When I figured this out, it actually felt like magic! Except the magic didn’t work yet, the class could be registered but not served.

This comment on a somewhat-related StackOverflow question was helpful in identifying some missing class attributes that seemed to be preventing the class from being served.

Those using Python 3.7 (and newest version of PyWin32), you need to add variables: _reg_verprogid_ and _reg_class_spec_. The first one is about the same as _reg_progid_ (for example Python.TestServer.2), the second should be <filename>.<classname> (like a Python module)

So I added those to the Controller class:

    _reg_verprogid_ = "TestController.Application.1"
    _reg_class_spec_ = "controller.Controller"

But we’re still not quite there, because if you try to run the EXE you’ll get a pythoncom error:

Traceback (most recent call last):
  File "site-packages\win32com\server\policy.py", line 136, in CreateInstance
  File "site-packages\win32com\server\policy.py", line 194, in CreateInstance
  File "site-packages\win32com\server\policy.py", line 728, in call_func
  File "site-packages\win32com\server\policy.py", line 717, in resolve_func
  File "site-packages\win32com\server\policy.py", line 736, in _import_module
ModuleNotFoundError: No module named 'controller'
pythoncom error: Unexpected gateway error
Traceback (most recent call last):
   File "site-packages\win32com\server\policy.py", line 136, in CreateInstance
   File "site-packages\win32com\server\policy.py", line 194, in CreateInstance
   File "site-packages\win32com\server\policy.py", line 728, in call_func
   File "site-packages\win32com\server\policy.py", line 717, in resolve_func
   File "site-packages\win32com\server\policy.py", line 736, in _import_module
 ModuleNotFoundError: No module named 'controller'
 pythoncom error: CPyFactory::CreateInstance failed to create instance. (80004005)

I had to make the _reg_class_spec_ = __name__ + '.Controller' because using the module name was resulting in an that nasty ModuleNotFoundError. (Untested, but I suppose that could be circumvented by putting the Controller class in a separate .py file.)

Once that’s all taken care of, I rebuilt the EXE from pyinstaller, registered it via controller.exe --register, and finally had a COM-visible EXE.

The final python code for the controller.py file looks like:

import pythoncom, sys, win32com.server.register, win32com.server.localserver, logging

class Controller(object):
    _reg_progid_ = "TestController.Application"
    _reg_verprogid_ = "TestController.Application.1"
    _reg_class_spec_ = __name__ + ".Controller"
    _reg_desc_ = "Python Test COM Server"
    # generate your own UID via uuid.uuid4() or pythoncom.CreateGuid()
    _reg_clsid_ = '{9e243d9c-96c7-4c7c-987d-893f1f55c064}'  
    _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
    _public_methods_ = ['hello']

    def __init__(self):
        pass

    def hello(self, name):
        return f'Hello, {name}'

if __name__ == '__main__':
    print(__name__)
    print(sys.argv)
    try:
        if '--register' in sys.argv or '--unregister' in sys.argv:
            win32com.server.register.UseCommandLine(Controller)
        else:
            win32com.server.localserver.serve([ Controller._reg_clsid_ ])
    except:
        logging.exception('Oops!')
        input()

Leave a Reply

Your email address will not be published. Required fields are marked *