Что такое COM — почему дизайн Windows COM до сих пор красив

· Обновлено: · · COM, ActiveX, Разработка Windows

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619623)

Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.

Го Комура (2026). Что такое COM — почему дизайн Windows COM до сих пор красив. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619623 https://comcomponent.com/ru/blog/2026/01/25/001-why-com-is-beautiful/

DOI (последняя версия)
10.5281/zenodo.21619623
DOI (эта версия)
10.5281/zenodo.21619624

Что такое COM?

COM (Component Object Model) — это бинарный контракт, по которому компоненты общаются друг с другом в Windows. Механизм, в котором компоненты общаются через интерфейсы как строгие контракты, поверх различий языка и компилятора. В основе лежит идея «писать код к контракту, а не к реализации».

Карта знаний этой статьи

COM — это двоичный контракт, по которому компоненты в Windows обмениваются друг с другом. IUnknown, который наследуют все интерфейсы, даёт проверку возможностей через QueryInterface и управление подсчётом ссылок через AddRef/Release. Уникальная идентификация компонента и интерфейса GUID-ами CLSID и IID избегает столкновений имён; проект, который добавляет новые интерфейсы, не меняя существующие, и проверяет их через QueryInterface, обеспечивает сосуществование версий. In-proc (DLL-сервер), загружаемый в тот же процесс, что и вызывающая сторона, требует совпадения разрядности и увлекает вызывающего при падении партнёра; Out-of-proc (LocalServer), работающий в отдельном процессе, свободен от этого ограничения, но требует готовности к ошибкам разрыва вроде RPC_E_DISCONNECTED, когда партнёр исчез. Выражение успеха и неудачи через возвращаемое значение HRESULT вместе с Proxy/Stub и предварительным определением контракта в IDL делают весь этот механизм независимым от границ языка и процесса.

Карта знаний: почему устройство COM изящноСхема, которая показывает, как COM на основе подсчёта ссылок через IUnknown и QueryInterface и идентификации по CLSID и IID реализует сильные стороны — двоичную совместимость, разделение интерфейсов, сосуществование версий и повторное использование через границу процесса — и как это связано с вариантами размещения In-proc и Out-of-proc, HRESULT и взаимодействием с .NET.используетиспользуетиспользуетиспользуетиспользуетиспользуетреализуеттребуетреализуеттребуетиспользуетиспользуеттребуетможет вызватьпредотвращаетможет вызватьиспользуетиспользуетиспользуетиспользуетиспользуетиспользуеттребуетCOM (Component Object Model)IUnknownQueryInterfaceподсчёт ссылок COM (AddRef/Release)CLSID (Class ID)IID (идентификатор интерфейса)HRESULTбинарная совместимость COMверсионирование интерфейсов COMIn-proc COM (DLL-сервер)COM LocalServer (внепроцессный COM-сервер)требование совпадения разрядностипадение хоста вместе с In-proc crashошибка разрыва out-of-proc COMProxy/StubIDL (Interface Definition Language)COM Interop в .NETProgID (Programmatic Identifier)ActiveX

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 23, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

Три ключевых элемента COM

В этой главе — составные части, из которых собран механизм COM. «Четыре сильные стороны COM» ниже — свойства, которые получаются, когда эти части складывают вместе; это не повтор одного и того же дважды. Соответствие сведём в таблицу в конце главы про сильные стороны.

1. Проектирование вокруг интерфейса

В COM «контракт раньше реализации». Объектом можно пользоваться, не зная внутренней реализации: достаточно знать опубликованные интерфейсы.

2. Идентификация по GUID (CLSID / IID)

Каждому компоненту и интерфейсу выдаётся глобально уникальный идентификатор (GUID), поэтому конфликта имён просто не возникает.

3. IUnknown

Базовый интерфейс, от которого наследуют все интерфейсы COM. Он даёт три операции.

Метод Роль
QueryInterface Спрашивает, есть ли у объекта другой интерфейс
AddRef Увеличивает счётчик ссылок
Release Уменьшает счётчик ссылок (при нуле объект уничтожает себя)

Когда эти три элемента складываются, между вызывающей стороной и реализацией остаётся только «интерфейс, опознанный по GUID, с фиксированным порядком методов». Ни язык, ни компилятор в этом контракте не фигурируют.

Бинарный контракт COMВызывающая сторона на любом языке и реализация на любом языке сходятся в контракте, зафиксированном в бинарном виде. IID (GUID) однозначно задаёт контракт, порядок методов начинается с трёх методов IUnknown, у каждого метода свои типы аргументов и возврата и соглашение о вызовах.Реализация — язык любойВызывающая сторона — язык любойКомпонент, написанный на C++Компонент, написанный на C#Приложение на C++Приложение на C#VBA / Python и др.Контракт (то, что зафиксировано в бинарном виде)· IID (GUID) однозначно задаёт, какой это контракт· порядок методов начинается с трёх методов IUnknown· типы аргументов и возврата и соглашение о вызовах для каждого метода

Рис. 1: Бинарный контракт COM. Ни язык вызывающей стороны, ни язык реализации в контракт не входят, поэтому любую сторону можно заменить без пересборки другой.

«Писать код к контракту» на примере

На словах это хуже видно, поэтому свернём к минимуму кода. Вызывающая сторона пользуется компонентом ICalcService, который только складывает числа, вообще не зная реализации.

Сначала C++ (сырой COM). Определение ICalcService здесь — сам контракт. Написана реализация на C++ или на C# — в коде вызывающей стороны этого нигде нет.

#include <objbase.h>

// Определение контракта. Обычно такой заголовок генерируется из IDL.
// Наследует IUnknown, поэтому QueryInterface / AddRef / Release есть всегда.
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001"))
ICalcService : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
};

// Расширенный контракт, добавленный позже. GUID другой.
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
    virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};

// CLSID компонента-реализации. Обычно определяется в заголовке, сгенерированном из IDL.
static const CLSID CLSID_CalcService =
    { 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };

// Вызывающая сторона
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) { return hr; }

ICalcService* calc = nullptr;
hr = CoCreateInstance(CLSID_CalcService, nullptr, CLSCTX_ALL,
                      __uuidof(ICalcService), reinterpret_cast<void**>(&calc));
// Вернувшийся указатель уже с AddRef (счётчик ссылок равен 1)

if (SUCCEEDED(hr))
{
    int sum = 0;
    hr = calc->Add(1, 2, &sum);          // Можно вызвать, не зная реализации

    // «Есть ли ещё и расширенный контракт?» — спрашиваем в runtime = сосуществование версий
    ICalcServiceEx* calcEx = nullptr;
    if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
                                       reinterpret_cast<void**>(&calcEx))))
    {
        // Сюда попадаем, только если это новая версия компонента
        calcEx->Release();               // Кто получил указатель, тот и отдаёт
    }
    // Если интерфейса нет, возвращается E_NOINTERFACE — старая версия продолжает работать

    calc->Release();                     // Счётчик ссылок становится 0, объект уничтожается
}
CoUninitialize();

Три места, на которые стоит смотреть.

  • AddRef почти никогда не пишут руками. И CoCreateInstance, и QueryInterface уже сделали AddRef на возвращаемый указатель. Правило такое: кто получил указатель, тот вызывает Release. Явный AddRef нужен только когда тот же указатель начинают держать ещё в одном месте.
  • Отказ QueryInterface — не авария. «Этого контракта нет» (E_NOINTERFACE) — штатный ответ. Именно он позволяет добавлять новые возможности, не ломая старый компонент.
  • Все возвраты — HRESULT. Успех и неудача передаются кодом возврата, а не исключением: так договариваются, чтобы пересечь границу языка. Как бросать исключения, у каждого языка своё; целое как код возврата умеет разобрать кто угодно.
Правило счётчика ссылок и ReleaseCoCreateInstance и QueryInterface возвращают указатель, на который уже сделан AddRef, поэтому получившая сторона в конце вызывает Release, и когда счётчик ссылок становится 0, объект уничтожается.Явный AddRefCoCreateInstance или QueryInterfaceВозвращается указатель с уже сделанным AddRefПолучившая сторона используетПолучившая сторона вызывает ReleaseПри нуле счётчика объект уничтожаетсяТот же указатель начинают держать ещё в одном месте

Рис. 2: Кто получил указатель, тот вызывает Release. Явный AddRef — только когда мест хранения становится больше.

Тот же контракт с C# выглядит так. Одинаковый GUID означает один и тот же контракт, поэтому реализация на C++ не мешает.

using System;
using System.Runtime.InteropServices;

// Пишем тот же GUID, что и на стороне C++. Это объявление «тот же контракт».
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
    int Add(int a, int b);
}

// Вызывающая сторона
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
         ?? throw new InvalidOperationException("COM-сервер не зарегистрирован.");
object server = Activator.CreateInstance(t)!;
try
{
    var calc = (ICalcService)server;   // Это приведение соответствует QueryInterface
    int sum = calc.Add(1, 2);
    Console.WriteLine(sum);            // 3
}
finally
{
    Marshal.ReleaseComObject(server);  // Соответствует Release
}

На стороне C# у Add возвращаемый тип int, потому что COM Interop в .NET делает фиксированное преобразование: последний аргумент [out, retval] становится возвращаемым значением, а неуспешный HRESULT превращается в исключение. AddRef/Release с вызывающей стороны тоже пропадают из виду — не потому что их нет, а потому что тонкая обёртка .NET (RCW) вызывает их за вас. Один и тот же контракт можно писать так, как естественно для языка — вот как бинарный контракт выглядит на практике.

Как запись на C# соответствует COMВ C# приведение к интерфейсу соответствует QueryInterface, Marshal.ReleaseComObject — Release, неуспешный HRESULT превращается в исключение, а AddRef и Release за вызывающую сторону вызывает RCW.Код на C#Приведение к типуReleaseComObjectНеуспешный HRESULTСоответствует QueryInterfaceСоответствует ReleaseПреобразуется в исключениеRCW ведёт счётчик ссылок

Рис. 3: Даже при том же контракте на стороне C# запись становится естественной для языка; преобразование берут на себя RCW и слой взаимодействия.

Четыре сильные стороны COM

1. Бинарная совместимость

Собранный однажды компонент можно использовать повторно независимо от языка программирования и runtime. Вызвать из C# или Python COM-компонент, написанный на C++, — обычная практика.

2. Разделение интерфейса и реализации

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

3. Сосуществование версий

Базовый приём добавить возможности, сохранив обратную совместимость, — добавлять новые интерфейсы. Новую функциональность дают, не меняя старые интерфейсы.

Если скомбинировать старую и новую вызывающую сторону со старым и новым компонентом, из четырёх сочетаний три работают как есть, а оставшееся просто отвечает «этого нет».

Сосуществование версий через QueryInterfaceСтарая и новая вызывающая сторона в сочетании со старой и новой версией компонента. Три сочетания работают как есть, четвёртое отвечает E_NOINTERFACE и продолжает работу со старой функциональностью.QueryInterface(IID_ICalcService)S_OKQueryInterface(IID_ICalcService)S_OK — после обновления не ломаетсяQueryInterface(IID_ICalcServiceEx)S_OK — доступны новые возможностиQueryInterface(IID_ICalcServiceEx)E_NOINTERFACE — продолжаем со старой функциональностьюСтарая вызывающая стороназнает только ICalcServiceНовая вызывающая сторонаспрашивает ICalcServiceExСтарая версия компонентареализует только ICalcServiceНовая версия компонентареализует оба

Рис. 4: Если существующие интерфейсы не трогать и добавить новый IID, работают любые сочетания старого и нового. Пунктир — штатный ответ «этого контракта нет».

4. Повторное использование через границы процесса

У COM-компонента два места размещения. In-proc (DLL-сервер) загружается как DLL в тот же процесс, что и вызывающая сторона. Out-of-proc (EXE-сервер, LocalServer) запускается как EXE в отдельном процессе. Снаружи вызывающий код выглядит одинаково.

Два места размещения компонентаОт одного и того же снаружи вызывающего кода можно прийти и к In-proc, который загружается как DLL в тот же процесс, и к Out-of-proc, который запускается как EXE в отдельном процессе.Код вызывающей стороны (снаружи одинаковый)In-proc (DLL-сервер)Out-of-proc (EXE-сервер)Загружается как DLL в тот же процессЗапускается как EXE в отдельном процессе

Рис. 5: Мест размещения два — In-proc и Out-of-proc, но снаружи вызывающий код не меняется.

  In-proc (DLL-сервер) Out-of-proc (EXE-сервер)
Где выполняется В том же процессе, что и вызывающая сторона В отдельном процессе
Как на самом деле идёт вызов Прямой вызов через указатель на функцию Аргументы перепаковываются (маршалинг) и уходят через межпроцессное взаимодействие
Скорость Быстро Медленнее из-за IPC
Если вторая сторона упала Вызывающая сторона падает вместе с ней Вызывающая сторона остаётся жива (вызов возвращается как неудача)
Разрядность (32/64-bit) Без совпадения не загрузится Может отличаться

Через Out-of-proc COM (EXE-сервер) можно безопасно вызывать функциональность другого процесса. Последняя строка — «разрядность может отличаться» — на практике бывает важна. Конкретную сборку, где 32-bit приложение вызывает 64-bit DLL, разбираем в статье «Пример COM-моста: вызов 64-битной DLL из 32-битного приложения».

Но отдельный процесс значит и то, что вторая сторона может исчезнуть в любой момент. Если процесс-сервер падает или завершается, вызывающая сторона получает неудачу вроде следующей. Это не «сломалось», а «второй стороны уже нет» — ошибки, характерные именно для Out-of-proc.

Код ошибки Смысл
RPC_E_DISCONNECTED Объект, к которому обратились, отсоединён от клиента (объекта на той стороне уже нет)
RPC_S_SERVER_UNAVAILABLE До сервера (процесса) на той стороне достучаться нельзя

В In-proc «упал он — упал и я», и думать об этом не нужно. В Out-of-proc в проектирование входят восстановление: перезапуск и повторное подключение. Точнее думать так: взамен на безопасность появляется дополнительная работа.

Что происходит, когда процесс на той стороне исчезаетВ Out-of-proc, если процесс-сервер падает или завершается, вызов возвращается как неудача; вызывающая сторона остаётся жива, поэтому в проект нужно закладывать восстановление — перезапуск и повторное подключение.Крах или завершение процесса-сервераВызов возвращается как неудачаВызывающая сторона остаётся живаК восстановлению: перезапуск и повторное подключениеНе «сломалось», а «второй стороны уже нет»

Рис. 6: В Out-of-proc падение той стороны не утягивает вызывающую, но взамен в проект входит восстановление.

Как три элемента связаны с четырьмя сильными сторонами

Как сказано в начале, «три элемента» — составные части, «четыре сильные стороны» — результат. Если выписать, какой элемент какую сторону держит, роли того, что казалось перекрытием, становятся ясны.

Сильная сторона Какие элементы в основном работают
1. Бинарная совместимость Проектирование вокруг интерфейса + IUnknown (порядок вызовов зафиксирован на бинарном уровне)
2. Разделение интерфейса и реализации Проектирование вокруг интерфейса (наружу выходит только контракт)
3. Сосуществование версий GUID + QueryInterface (другой контракт можно запросить в runtime)
4. Повторное использование через границы процесса Проектирование вокруг интерфейса (от контракта можно отвязать даже место реализации)

COM до сих пор в строю

Его часто считают «старой технологией», но COM — механизм, который до сих пор используется в самой основе Windows.

Где встречается COM

  • Расширения Проводника (контекстное меню, предпросмотр)
  • Автоматизация Office (внешнее управление Excel и Word)
  • Взаимодействие с .NET (COM Interop)
  • Существующие системы, включая ActiveX
  • DirectX, Windows Shell API и множество других Windows API

Даже если кажется, что «меня это не касается», пока вы разрабатываете под Windows, COM где-нибудь встретится.

Итог

В центре дизайна COM — независимость от языка, процесса и реализации. Нейтральные к языку интерфейсы, однозначная идентификация и версионирование через GUID, счётчик ссылок через IUnknown и механизм, который прозрачно закрывает межпроцессное взаимодействие.

Центр дизайна COMНейтральное к языку устройство интерфейсов, однозначная идентификация и версионирование через GUID, счётчик ссылок IUnknown и прозрачное межпроцессное взаимодействие — четыре механизма, которые держат независимость от языка, процесса и реализации.Нейтральное к языку устройствоНезависимость от языка, процесса и реализацииОднозначная идентификация через GUIDСчётчик ссылок IUnknownПрозрачное межпроцессное взаимодействие

Рис. 7: Четыре механизма вместе держат центр COM — независимость от языка, процесса и реализации.

Слово «красиво» субъективно, но основания для такой оценки конкретны. Если рядом поставить задачу, которую COM брался решить, и сам способ решения, получается так.

Ограничение тогда (и сейчас) Ответ COM
У C++ нет стандартного бинарного соглашения (ABI), разные компиляторы уже не дают повторно использовать код. Не совпадают ни манглинг имён, ни раскладка объекта в памяти Соглашением сделали только «порядок в таблице виртуальных функций». Сузили до этой одной точки — и язык с компилятором перестали иметь значение
Замена библиотеки заставляет пересобирать всё, что ею пользуется Пока контракт (интерфейс) не меняется, пересборка не нужна. Можно подменить бинарник как есть
Хочется добавить возможности, не ломая существующую вызывающую сторону Интерфейсы добавляют, не меняя, и в runtime спрашивают через QueryInterface, «есть ли он»
Конфликт имён (под одним именем класса живут разные вещи) Однозначная идентификация по GUID. Согласовывать имена стало незачем
Вызов функций другого процесса или другой машины полностью меняет то, как пишется вызов Между сторонами ставят Proxy/Stub и сохраняют форму кода вызывающей стороны

То есть дизайн COM начался не с вопроса «как сделать красиво». Нынешняя форма — результат того, что по одному снимали конкретные препятствия, которые мешали повторному использованию. Проектов, где соответствие ограничений и решений прослеживается так прямо, немного.

И этот способ решения без изменений остался в современной компонентной разработке.

COM Современный аналог
Контракт сначала фиксируют в IDL и из него генерируют код обеих сторон Клиент и сервер генерируют из схемы OpenAPI или Protocol Buffers
Существующие интерфейсы не меняют, добавляют новые В Protocol Buffers поля добавляют, не переиспользуя номера; API версионируют
Язык реализации не важен (бинарный контракт) Язык реализации не важен (контракт сообщений через сеть)
Proxy/Stub прячет границу процесса Клиентский stub RPC прячет границу сети

Различается только тип границы (процессы на одной машине или сеть) и форма контракта (бинарный или текст/схема). Если понять COM, в проектировании интерфейсов между микросервисами вопросы «почему контракт фиксируют заранее» и «почему нельзя удалять существующие поля» укладываются не как мода, а как необходимость.

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое COM?
COM (Component Object Model) — бинарный контракт, по которому компоненты общаются друг с другом в Windows. Это механизм, в котором компоненты общаются через интерфейсы как строгие контракты, поверх различий языка и компилятора. В основе лежит идея «писать код к контракту, а не к реализации».
Что такое IUnknown?
Базовый интерфейс, от которого наследуют все интерфейсы COM. Он даёт три операции: QueryInterface спрашивает, есть ли у объекта другой интерфейс; AddRef увеличивает счётчик ссылок; Release уменьшает счётчик и, когда тот становится нулём, объект уничтожает себя. Временем жизни объектов COM управляет именно этот счётчик ссылок.
COM ещё используют?
Да. COM — механизм, который до сих пор используется в самой основе Windows. Он появляется в расширениях Проводника (контекстное меню и предпросмотр), в автоматизации Office (Excel и Word), во взаимодействии с .NET (COM Interop), в существующих системах, включая ActiveX, в DirectX, Windows Shell API и во многих других местах. Пока вы разрабатываете под Windows, COM где-нибудь встретится.
В чём сильные стороны COM?
Их четыре. Бинарная совместимость: собранный однажды компонент можно использовать повторно независимо от языка и runtime. Разделение интерфейса и реализации: реализация скрыта, наружу выходит только контракт. Сосуществование версий: новые интерфейсы добавляют, не ломая обратную совместимость. И безопасный вызов функциональности другого процесса через Out-of-proc COM (EXE-сервер).

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог