Разработка COM сервера: Python в системном программировании

Раздел: Системное программирование -> COM-технологии и взаимодействие с Windows

Создание COM сервера на Python

COM (Component Object Model) технология Microsoft, позволяющая компонентам взаимодействовать независимо от языка программирования. Python может выступать в роли сервера COM, предоставляя свои объекты клиентам (VBScript, C++, .NET, Excel). В статье рассматривается несколько подходов с акцентом на практическую реализацию.

Основное решение: библиотека pywin32 (модуль win32com.server). Это наиболее стабильный и документированный способ. Сервер регистрируется в реестре Windows и может быть вызван из любого COM клиента.

Как создать простой COM сервер с одним методом?

Определяется класс Python с атрибутами _public_methods_ (список имен методов, доступных клиенту), _reg_progid_ (программный идентификатор), _reg_clsid_ (GUID). После этого объект регистрируется функцией win32com.server.register.UseCommandLine или вручную с помощью win32com.server.register.RegisterServer.

import pythoncom
import win32com.server.register

class SimpleCalc:
    _public_methods_ = ['Add', 'Multiply']
    _reg_progid_ = 'PythonDemo.SimpleCalc'
    _reg_clsid_ = '{B8F2B3A0-5C9E-11EC-9A0A-0800200C9A66}'  # сгенерировать новый GUID

    def Add(self, a, b):
        return a + b

    def Multiply(self, a, b):
        return a * b

if __name__ == '__main__':
    win32com.server.register.UseCommandLine(SimpleCalc)

Com сервер на python (com сервер на python)

После запуска скрипта с аргументом --register COM сервер регистрируется. Клиент может создать экземпляр через CreateObject("PythonDemo.SimpleCalc").

Типичные проблемы: регистрация требует прав администратора. Ошибка "access denied" при попытке записи в реестр. Решение: запускать скрипт от имени администратора. Еще одна ошибка "CoInitialize has not been called" – вызывается pythoncom.CoInitialize() в главном потоке.

Некорректный GUID – если дважды используется один и тот же CLSID, возникает конфликт. Решение: генерировать новый GUID для каждого сервера (например, с помощью uuid.uuid4()).

Как зарегистрировать сервер в качестве COM DLL (in-process) и EXE (out-of-process)?

По умолчанию pywin32 регистрирует сервер как DLL (in-process). Чтобы создать out-of-process (EXE), требуется дополнительная обёртка. Класс наследуется от win32com.server.util.NewObject или используется win32com.server.dispatcher.Dispatcher. Ниже пример для EXE.

import sys
import pythoncom
import win32com.server.register

class CalcExe:
    _public_methods_ = ['Add']
    _reg_progid_ = 'PythonDemo.CalcExe'
    _reg_clsid_ = '{C1D2E3F4-5A6B-7C8D-9E0F-1A2B3C4D5E6F}'

    def Add(self, x, y):
        return x + y

if __name__ == '__main__':
    import win32com.server.dispatcher
    win32com.server.register.RegisterServer(CalcExe)
    pythoncom.CoInitialize()
    win32com.server.dispatcher.Run()

После регистрации запустите скрипт как сервер (он будет висеть в процессах). Клиент подключается через GetObject или CreateObject.

Ошибка "Class not registered" – сервер не зарегистрирован или указан неверный CLSID. Решение: проверить реестр под HKEY_CLASSES_ROOT\CLSID.

При запуске EXE сервера могут возникнуть проблемы с потоковой моделью (STA, MTA). По умолчанию используется pythoncom.COINIT_APARTMENTTHREADED, что требует для каждого потока отдельной инициализации.

Вариант: использование comtypes для создания COM сервера

Библиотека comtypes ориентирована на работу с существующими COM интерфейсами, но также позволяет создавать серверы. Требуется больше ручной работы с указателями и интерфейсами. Подходит для продвинутых разработчиков.

from comtypes.server import comtypes
import comtypes

class MyServer:
    @comtypes.server.interface.IUnknown
    def QueryInterface(self, riid, ppvObject):
        pass
    # ...

Однако полноценный сервер с IDispatch проще реализовать через pywin32. comtypes чаще используется для создания COM клиентов или для низкоуровневой работы.

Сложность отладки, недостаток примеров – основная проблема.

Вариант: ручная регистрация сервера с помощью regedit и ctypes

Иногда требуется встроить COM сервер в приложение без внешних библиотек (кроме ctypes). Технически возможно, но объём кода велик. Не рекомендуется для реальных проектов, но может быть познавательным.

import ctypes
import uuid

# Определение класса и методов вручную, регистрация в реестре
# Кроме ctypes потребуется манипуляции с GUID и таблицами vtbl

Подход используется при встраивании Python в C++ приложения.

В каждом варианте важно учитывать потоковую модель, права доступа и корректную обработку ссылок (AddRef/Release). Для большинства сценариев автоматизация через pywin32 является оптимальной.

Расширенные примеры и сценарии

Пример 1: COM сервер с событиями (ConnectionPoints)

События позволяют серверу уведомлять клиента (например, при изменении данных). Реализация требует создания объекта источника событий.

Пример
import pythoncom
import win32com.server.util
from win32com.server import connect

class EventSource:
    _public_methods_ = ['FireEvent']
    _reg_progid_ = 'PythonDemo.EventServer'
    _reg_clsid_ = '{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}'
    _reg_desc_ = 'Python Event Server'
    _connect_to_ = None

    def __init__(self):
        self.__event_set = set()

    def FireEvent(self, value):
        # уведомление всех подключённых клиентов
        for sink in self.__event_set:
            sink.OnChange(value)

    def Advise(self, sink):
        self.__event_set.add(sink)
        return 0

    def Unadvise(self, sink):
        self.__event_set.discard(sink)

if __name__ == '__main__':
    win32com.server.register.UseCommandLine(EventSource)
Результат: сервер зарегистрирован. Клиент на VBScript может подписаться на событие OnChange.

Пример 2: COM сервер с двойным интерфейсом (Dual Interface)

Поддержка IDispatch и пользовательского интерфейса одновременно. Требуется описание метода в _public_methods_ и указание _typelib_guid_.

Пример
class DualCalc:
    _public_methods_ = ['Add', 'Subtract']
    _reg_progid_ = 'PythonDemo.DualCalc'
    _reg_clsid_ = '{12345678-1234-1234-1234-123456789ABC}'
    _typelib_guid_ = '{87654321-4321-4321-4321-CBA987654321}'
    _reg_typelib_name_ = 'DualCalcLib'

    def Add(self, a, b):
        return a + b

    def Subtract(self, a, b):
        return a - b

После регистрации создается библиотека типов, позволяющая раннее связывание.

Пример 3: Многопоточный COM сервер (MTA)

По умолчанию Python COM сервер работает в STA (Single-Threaded Apartment). Чтобы обеспечить многопоточность, используется pythoncom.COINIT_MULTITHREADED и специальный диспетчер.

Пример
import pythoncom
import win32com.server.dispatcher

class MultiThreadServer:
    _public_methods_ = ['LongOp']
    _reg_progid_ = 'PythonDemo.MultiThread'
    _reg_clsid_ = '{DEADBEEF-0000-0000-0000-000000000001}'

    def LongOp(self, n):
        import time
        time.sleep(n)
        return n

if __name__ == '__main__':
    pythoncom.CoInitialize(pythoncom.COINIT_MULTITHREADED)
    win32com.server.dispatcher.Run()

При этом вызовы могут приходить из разных потоков, но сам Python GIL сохраняется.

Пример 4: Использование makepy для создания .py файла из библиотеки типов

Не для сервера, а для клиента. Однако может пригодиться при создании сервера с известным интерфейсом.

Пример
# В командной строке:
python -m win32com.client.makepy "PythonDemo.DualCalc"
Создаётся файл с именем библиотеки, содержащий классы для раннего связывания.

Пример 5: Отладка COM сервера с использованием print и лог-файла

Поскольку сервер может быть запущен в отдельном процессе, вывод на консоль не виден. Рекомендуется использовать логирование.

Пример
import logging
logging.basicConfig(filename='com_server.log', level=logging.DEBUG)

class DebugServer:
    _public_methods_ = ['Echo']
    def Echo(self, msg):
        logging.debug(f'Echo called with {msg}')
        return msg
При каждом вызове в лог пишется сообщение.

Данные примеры покрывают основные сценарии использования COM сервера на Python: от простых методов до событий и многопоточности.

COM сервер на Python - comments

En
Com сервер на python (python)