COM-объект (1)
В общем понимании, объект класса, реализующий как минимум один COM-интерфейс. Реализация объекта в основном скрывается в динамически подключаемой библиотеке, называемой COM-сервер(2), для использования публикуются и распространяются интерфейсы.
COM-интерфейс, абстрактный класс содержащий только чисто виртуальные функции. Выделяется особый интерфейс IUnknown, любой COM-объект обязан реализовывать данный интерфейс.
Каждый COM-интерфейс должен содержать некий свой идентификатор. В COM он определяется структурой GUID и вот тут столкнемся с первым недостатком COM. GUID непонятен и не читаем ну и все остальное описанное на Wiki. Нам он то же нужен, но в более читаемом и понятном виде (назовем его uiid).
IUnknown и uiid
#define define_uiid(name) inline static const std::string& guid() { const static std::string idn(dom_guid_pre_name #name); return idn; }
namespace Dom {
using uiid = std::string;
using clsuid= std::string;
struct IUnknown
{
virtual long AddRef() = 0;
virtual long Release() = 0;
virtual bool QueryInterface(const uiid&, void **ppv) = 0;
define_uiid(Unknown)
};
}
Помимо идентификатора интерфейса, выделяется и идентификатор класса (clsuid), необходимый для создания объекта. В нашем случае, т.к. это более менее читаемый идентификатор, который может определять суть, можно пока забыть о их публикации (возможно это не хорошо).
Резюме
COM-объект, содержит единственный идентификатор класса. Реализует как минимум один COM-интерфейс — IUnknown (любой COM-интерфейс имеет уникальный идентификатор интерфейса). Разные реализации COM-объекта могут иметь один и тот же идентификатор класса (пример: release и debug версия).
COM-сервер (2)
Динамически подключаемой библиотека (для Linux это Shared object — so) реализующая как минимум один COM-объект. Сервер должен экспортировать определенный набор функций:
extern "C" bool DllCreateInstance(const uiid& iid, void** ppv)
Создает объект класса по clsuid, увеличивает количество ссылок на so, каждый раз при успешном создании объекта. Вызов IUnknown::AddRef, так же должен увеличивать счетчик ссылок на so, а IUnknown::Release должен уменьшать.extern "C" bool DllCanUnloadNow()
Если количество ссылок на SO равно 0, то можно выгружать библиотеку.
extern "C" bool DllRegisterServer(IUnknown* unknown)
Регистрирует в “реестре” все clsuid сервера. Вызывается единожды при инсталляции COM-сервера.
extern "C" bool DllUnRegisterServer(IUnknown* unknown)
Удаляет из “реестра” записи о зарегистрированных clsuid сервера. Вызывается единожды при деинсталляции COM-сервера.
Пример SimpleHello, объявляем интерфейс IHello:
struct IHello : public virtual Dom::IUnknown {
virtual void Print() = 0;
define_uiid(Hello)
};
Реализация интерфейса:
/* COM-объект */
class SimpleHello : public Dom::Implement<SimpleHello, IHello> {
public:
SimpleHello() { printf("%s\n", __PRETTY_FUNCTION__); }
~SimpleHello() { printf("%s\n", __PRETTY_FUNCTION__); }
virtual void Print() {
printf("Hello from %s\n",__PRETTY_FUNCTION__);
}
define_clsuid(SimpleHello)
};
/* COM-сервер */
namespace Dom {
DOM_SERVER_EXPORT_BEGIN
EXPORT_CLASS(SimpleHello)
DOM_SERVER_EXPORT_END
DOM_SERVER_INSTALL(IUnknown* unknown) {
Interface<IRegistryServer> registry;
if (unknown->QueryInterface(IRegistryServer::guid(), registry)) {
// Дополнительные действия при инсталляции сервера
}
return true;
}
DOM_SERVER_UNINSTALL(IUnknown* unknown) {
Interface<IRegistryServer> registry;
if (unknown->QueryInterface(IRegistryServer::guid(), registry)) {
// Дополнительные действия прии деинсталляции сервера
}
return true;
}
}
Набор макросов скрывает реализации функций, предоставляя более структурированное объявление и логику.
Dom::Implement<SimpleHello, IHello> — скрывает реализацию методов интерфейса IUnknown, добавляет “сахарок”, при объявлении интерфейсов реализуемых объектом (С++11 и variadic templates):
template <typename T, typename ... IFACES>
struct Implement : virtual public IUnknown, virtual public IFACES… {
...
};
Интерфейс IRegistryServer — определяет набор методов работы с “реестром” COM-серверов.
“Реестр” COM-серверов (3)
Важность реестра можно недооценить, но он является наверное главным столпом COM. Microsoft пишет в системный реестр, создает сложную структуру описания интерфейсов и их атрибутов (idl), я пошел немного по другому пути.
В реализации реестр базируется на файловой системе.
Какие плюшки? Понятность, простота, возможность восстановления, особая плюшка при регистрации сервера можно задать некого рода namespace (директорию относительно базового реестра в которой будет регистрироваться объекты сервера), тем самым можно реализовать целостность и версионность приложений использующих технологию.
Из недостатков, возможные проблемы с безопасностью, подмена реализаций объектов.
Как использовать, пример приложения (4)
Для того чтобы заставить все работать потребуется еще небольшая “библиотечка” и небольшая “программка”.
“Библиотечка” — ни что иное как обертка реализующая и собирающая все в единое целое, работу с реестром, загрузку\выгрузку SO, создание объектов.
Она единственная должна быть указана при сборке приложения. Все остальное, “хочется верить”, она сделает сама.
“Программка” — regsrv — собственно это аналог программы Microsoft RegSrv32, выполняющей те же действия (+ возможность указания namespace, + возможность получения списка зарегистрированных clsuid и COM-серверов).
sample
#include "../include/dom.h"
#include "../../skel/ihello.h"
int main()
{
Dom::Interface<Dom::IUnknown> unkwn;
Dom::Interface<IHello> hello;
if (Dom::CreateInstance(Dom::clsid("SimpleHello"), unkwn)) {
unkwn->QueryInterface(IHello::guid(), hello);
hello->Print();
}
else {
printf("[WARNING] Class `SimpleHello` not register.\nFirst execute command\n\tregsrv <fullpath>/libskel.so\n... and try again.");
}
return 0;
}
Dom (5)
Dom (Dynamic Object Model), моя реализация для Linux.
git clone
Спасибо.
Комментарии (37)
eirnym
27.10.2018 11:08+3Он совместим с MS COM? Если нет, то есть D-Bus, как правильно сказали, если да, то рекомендую еще посмотреть в исходники wine и reactos.
5oclock
28.10.2018 07:56D-bus не слишком жирно, для внутрипроцессного взаимодействия?
Вместо того, чтобы виртуальные функции дёргать...eirnym
28.10.2018 18:19При большем объёме соместимого кода с Windows,
жить будет веселее, и ошибок в Windows будут находить большенекоторое ПО, которое живёт только на Windows из-за COM/DCOM,спокойнолегче может быть портировано на Linux.
Например, можно будет редактировать в Microsoft Excel for Windows,
не вставая со стулаимея все возможности Linux
kekekeks
27.10.2018 12:47+1Для критиков "зачем это на Linux": COM — это универсальное ABI для кросс-языкового объектно-ориентированного взаимодействия. На текущий момент встроенные средства Linux и OSX такое ABI определяют только для обычных функций в стиле C.
Мы успешно применяем COM для взаимодействия кода на C# с объектно-ориентированным кодом на C++. За счёт встроенного рефкаунтинга время жизни объектов прозрачно регулируется, ABI даёт переносимость между платформами.
Шарповую обёртку генерим средствами SharpGenTools.
В частности посредством COM сделан новый бэкэнд под OSX для AvaloniaUI, других вменяемых способов сделать слой интеграции с ObjC просто нет. А тут clang-овский ARC обеспечивает интеграцию рефкаунтинга C++ и ObjC, а COM обеспечивает интеграцию C++ и C#.
Соорудил базовый заголовок с описанием IUnknown, реализацию ComPtr и ComObject и поехали. Дальше определяем набор интерфейсов и можно прозрачно их использовать из кода на C# как родные.
AnarchyMob
27.10.2018 14:00Так Xamarin.Mac же...
kekekeks
27.10.2018 14:58+1Гвоздями приколочен к патченому моно, в которое добавили костыли для GC. Не работает ни на неткоре, ни на стоковом моно.
Antervis
28.10.2018 12:48настолько универсальное, что нормально работать с ним можно лишь в студии, в которой для COM-объектов есть несколько расширений компилятора
kekekeks
28.10.2018 23:24Ну вот у меня проблем с реализацией на связке clang/XCode не возникло
Antervis
29.10.2018 11:54Из того, с чем сталкивался я при попытке скрестить c# утилиту с mingw:
1. midl из комплекта VS умеет генерировать либо COM-интерфейс для студии (со студийными расширениями), либо не обернутый для остальных компиляторов. Во втором режиме не поддерживает часть функционала, midl выдаст ошибку при его наличии в idl файле. Решение: править idl руками
2. сгенерированный заголовочник не компилится в mingw из-за очередного использования нестандартных расширений msvc типа forward enum declaration (нетипизированного). Решение: править руками
3. т.к. в «универсальном» варианте полностью отсутствует всяческая обвязка, необходимо самому дописывать всю обработку ошибок COM. «Родной» вариант прокидывает их в виде исключений.
В итоге то, что делается прагмой #import в студии, вне студии выливается в несколько дней курения документации плюс несколько дней написания оберток.kekekeks
29.10.2018 12:10- Используем SharpGenTools, на вход подаём сразу плюсовые заголовки, всё работает.
- Не используем midl, см. выше
- Все обёртки нагенерил SharpGenTools.
Пример рабочего проекта см. по ссылкам выше.
Antervis
29.10.2018 13:26но мне нужно было дергать c# из плюсов, а не наоборот
kekekeks
30.10.2018 10:28Тогда проблем выше вообще в принципе нет. Тем же самым SharpGenTools генерите по заголовкам трамплины (в терминологии — Shadow), которые нативному коду выдают нормальный C++-совместимый vtable. С точки зрения плюсового кода это выглядит как обычный указатель на интерфейс.
staticmain
29.10.2018 09:01Для критиков «зачем это на Linux»: COM — это универсальное ABI для кросс-языкового объектно-ориентированного взаимодействия. На текущий момент встроенные средства Linux и OSX такое ABI определяют только для обычных функций в стиле C.
Что позволяет делать обвязки для любых языков программирования, а не только C++.
DrMefistO
27.10.2018 17:55+1Так сложно было читать статью с таким большим количеством грамматических ошибок…
boblenin
27.10.2018 19:50Ничего себе. В 2018-ом то. У меня сложилось впечатление, что COM и DCOM и на windows уже скорее мертв, чем жив; а вы его на Linux.
kekekeks
27.10.2018 20:51Базовая часть COM — это по сути обычные интерфейсы из C++. Просто в корне иерархии наследования оных должен быть IUnknown с реализацией счётчика ссылок и каста к другим интерфейсам. На этом собственно всё.
Ryppka
28.10.2018 19:31Вообще-то это чисто сишная технология, отсюда и универсальный ABI. Ее более удобно обернули в C++. Что естественно.
voidptr0
27.10.2018 19:54string scope = cmdline(«scope», "");
Ну, наверное, для примеров достаточно, а так — стоило бы убедиться, откуда и куда мы будем пытаться работать.
Jef239
28.10.2018 04:18COM — довольно дурная технология. Её проблема — в «корпоративной» стабильности. Один объект, неверно подсчитывающий свои ссылки — и всё, приплыли. В лучшем случае — утечка памяти, в худшем — ссылки на уже удаленный объект.
В итоге вы можете полностью отладить свой код, но работать он будет лишь с известными вам COM-серверами. Одна ошибка в чужом коде — и всё, ваш код трещит по швам.
Не стоит выкапывать эту стюардессу без нужды. Лучше сделать что-то аналогичное на умных указателях.
Одно исключение — это OPC (прежде всего OPC DA). Оно завязано на DCOM. Но при реализации — нарветесь на проблемы с «корпоративной» стабильностью.kekekeks
28.10.2018 23:26С такой аргументацией нельзя использовать вообще никакие гномовские библиотеки. Они же все на GLib, а там g_object_ref и g_object_unref.
Jef239
28.10.2018 23:40Почему? Подсчет ссылок можно сделать по-разному. Можно иметь автоматические средства проверки. Можно иметь хорошие правила. Можно иметь макрос и темплейты, автоматизирующие подсчет.
В COM- ничего этого нет. Типичная ситуцаия в COM. Есть объект А, у него интрефейс IA. Через этот интерфейс получаем интерфейсы IB и IC. Они могут относится к объекту А, а могут — ко вложенным объектам B и С. Это зависит от реализации. В результате, уничтожая ссылку на IA, мы можем получить неработоспобность B и С, потому что А им нужен, а он уничтожился (точнее при уничтожении А уничтожает B и С). А можем — не получить. И все это зависит от конкретной реализации, которая бывает какая угодно.kekekeks
29.10.2018 00:14-1Описываемая вами ситуация произойти не может, т. к. QueryInterface увеличивает счётчик ссылок возвращаемого объекта.
Jef239
29.10.2018 00:19Да ну? Обоснуйте. Каким образом, увеличение ссылок для возвращаемого интерфейса IB может помещать мне сделать release для IA?
Jef239
29.10.2018 10:58Давайте я ещё раз медленно и печально проясню ситуацию. Есть объект A с интерфейсом IA. Внутри него находится объект B c интерфейсом IB. Объект B написан независимо и ничего про объект А не знает. При удалении объекта А, он удаляет и объект B, причем не взирая на его счетчик ссылок.
Получаем IA, от него получаем IB, делаем release на IA, Память, занята объектом B, возвращается в кучу. Через 15 минут использования интерфейса IB (объекта B) он зависает.
Причем вы можете проверять своего клиента на десятке серверов. Все будет отлично работать, ибо у них интерфейсы IA и IB относятся к одному объекту А (или сделано делегирование вместо агрегатирования). А потом нарветесь на сервер с такой схемой — и кранты. Причем замены этому серверу нет, для данного устойства он один.
Это вот и есть чудный мир OPC и COM.kekekeks
29.10.2018 12:07+1При удалении объекта А, он удаляет и объект B, причем не взирая на его счетчик ссылок.
Это прямое нарушение принципов работы с COM. Все COM-объекты должны быть в куче и все ссылки на COM-объект должны быть через умные указатели либо соответствующие механизмы клиентского языка. Описываемая вами явная ошибка программиста в коде компонента может случиться при использовании любой системы работы с учётом ссылок.
Jef239
29.10.2018 13:56Увы, в учебниках по COM встречается и агрегирование и делегирование. И это не «ошибка», а довольно распространенная практика реализации. Вплоть до примеров из книг от microsift press.
В другой системе использование ссылок может быть глубоко под капотом. Или возможен явный запрет агрегирования или присутствовать хороший чекер.kekekeks
30.10.2018 10:29Агрегирование и делегирование подразумевают раздельный подсчёт ссылок. Удаление объекта с живыми ссылками — это всегда явный косяк того, кто писал реализацию и к COM как таковому отношения не имеет.
Jef239
30.10.2018 13:08Вот вы уже и запутались. Excel — это большой объект. Но внутри него сидит куча других объектов. Запускаем Excel руками, потом подключаемся к его вложенным объектам по COM/DCOM. Затем закрываем. Что будет при раздельном подсчете ссылок? Excel закроется и похоронит все вложенные объекты.
Вся фишка в том, что объемлющий объект должен иметь общий подсчет ссылок со вложенными. А не раздельный. Но, поскольку вложенные объекты могут быть независимыми (inProc COM изDLL ), это не всегда возможно.
dmxrand
28.10.2018 12:21Зачем божечки? Есть ведь CORBA. Я 15 лет назад использовал pythonhosted.org/Pyro4 Вообще работало из коробки…
green-caterpillar
28.10.2018 12:41Да любая библиотека, которую вы можете подключить к своей программе, может дать вам утечки памяти, краши и т.д. Это общая проблема использования чужого кода. Совершенно непонятно, при чем тут COM?
saipr
Начало положено!