Мне нравится технология COM. Но речь пойдет не о технологии, восхвалении или недостатках COM, а опыте переноса и реализации на Linux. Велосипед? Целесообразность? Давайте не будем на этом заострять внимание.


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)


  1. saipr
    27.10.2018 10:38
    +1

    Начало положено!


  1. pfemidi
    27.10.2018 10:43
    +3

    Изобретаем D-Bus? Так он давно уже изобретён.


  1. eirnym
    27.10.2018 11:08
    +3

    Он совместим с MS COM? Если нет, то есть D-Bus, как правильно сказали, если да, то рекомендую еще посмотреть в исходники wine и reactos.


    1. 5oclock
      28.10.2018 07:56

      D-bus не слишком жирно, для внутрипроцессного взаимодействия?
      Вместо того, чтобы виртуальные функции дёргать...


      1. RPG18
        28.10.2018 10:42

        Это вы системный webview не использовали, который подтикает на windows. Лучше все в отдельных процессах.


        1. 5oclock
          28.10.2018 11:22

          Отдельные процессы — само по себе довольно жирно для многих задач :)


      1. eirnym
        28.10.2018 18:19

        При большем объёме соместимого кода с Windows, жить будет веселее, и ошибок в Windows будут находить больше некоторое ПО, которое живёт только на Windows из-за COM/DCOM, спокойно легче может быть портировано на Linux.


        Например, можно будет редактировать в Microsoft Excel for Windows, не вставая со стула имея все возможности Linux


  1. 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# как родные.


    1. AnarchyMob
      27.10.2018 14:00

      Так Xamarin.Mac же...


      1. kekekeks
        27.10.2018 14:58
        +1

        Гвоздями приколочен к патченому моно, в которое добавили костыли для GC. Не работает ни на неткоре, ни на стоковом моно.


    1. Antervis
      28.10.2018 12:48

      настолько универсальное, что нормально работать с ним можно лишь в студии, в которой для COM-объектов есть несколько расширений компилятора


      1. kekekeks
        28.10.2018 23:24

        Ну вот у меня проблем с реализацией на связке clang/XCode не возникло


        1. Antervis
          29.10.2018 11:54

          Из того, с чем сталкивался я при попытке скрестить c# утилиту с mingw:
          1. midl из комплекта VS умеет генерировать либо COM-интерфейс для студии (со студийными расширениями), либо не обернутый для остальных компиляторов. Во втором режиме не поддерживает часть функционала, midl выдаст ошибку при его наличии в idl файле. Решение: править idl руками
          2. сгенерированный заголовочник не компилится в mingw из-за очередного использования нестандартных расширений msvc типа forward enum declaration (нетипизированного). Решение: править руками
          3. т.к. в «универсальном» варианте полностью отсутствует всяческая обвязка, необходимо самому дописывать всю обработку ошибок COM. «Родной» вариант прокидывает их в виде исключений.

          В итоге то, что делается прагмой #import в студии, вне студии выливается в несколько дней курения документации плюс несколько дней написания оберток.


          1. kekekeks
            29.10.2018 12:10

            1. Используем SharpGenTools, на вход подаём сразу плюсовые заголовки, всё работает.
            2. Не используем midl, см. выше
            3. Все обёртки нагенерил SharpGenTools.

            Пример рабочего проекта см. по ссылкам выше.


            1. Antervis
              29.10.2018 13:26

              но мне нужно было дергать c# из плюсов, а не наоборот


              1. kekekeks
                30.10.2018 10:28

                Тогда проблем выше вообще в принципе нет. Тем же самым SharpGenTools генерите по заголовкам трамплины (в терминологии — Shadow), которые нативному коду выдают нормальный C++-совместимый vtable. С точки зрения плюсового кода это выглядит как обычный указатель на интерфейс.


    1. staticmain
      29.10.2018 09:01

      Для критиков «зачем это на Linux»: COM — это универсальное ABI для кросс-языкового объектно-ориентированного взаимодействия. На текущий момент встроенные средства Linux и OSX такое ABI определяют только для обычных функций в стиле C.


      Что позволяет делать обвязки для любых языков программирования, а не только C++.



  1. DrMefistO
    27.10.2018 17:55
    +1

    Так сложно было читать статью с таким большим количеством грамматических ошибок…


  1. boblenin
    27.10.2018 19:50

    Ничего себе. В 2018-ом то. У меня сложилось впечатление, что COM и DCOM и на windows уже скорее мертв, чем жив; а вы его на Linux.


    1. kekekeks
      27.10.2018 20:51

      Базовая часть COM — это по сути обычные интерфейсы из C++. Просто в корне иерархии наследования оных должен быть IUnknown с реализацией счётчика ссылок и каста к другим интерфейсам. На этом собственно всё.


      1. boblenin
        27.10.2018 22:06

        Ну а как же стабильный ABI?


        1. pfemidi
          27.10.2018 23:03

          Сделать все функции в обязательном порядке __cdecl или __stdcall (правда тут переменное число аргументов никак не получится, так что для vararg только __cdecl) и полностью запретить __fastcall, вот и получится стабильный ABI.


      1. Ryppka
        28.10.2018 19:31

        Вообще-то это чисто сишная технология, отсюда и универсальный ABI. Ее более удобно обернули в C++. Что естественно.


  1. voidptr0
    27.10.2018 19:54

    string scope = cmdline(«scope», "");

    Ну, наверное, для примеров достаточно, а так — стоило бы убедиться, откуда и куда мы будем пытаться работать.


  1. Jef239
    28.10.2018 04:18

    COM — довольно дурная технология. Её проблема — в «корпоративной» стабильности. Один объект, неверно подсчитывающий свои ссылки — и всё, приплыли. В лучшем случае — утечка памяти, в худшем — ссылки на уже удаленный объект.

    В итоге вы можете полностью отладить свой код, но работать он будет лишь с известными вам COM-серверами. Одна ошибка в чужом коде — и всё, ваш код трещит по швам.

    Не стоит выкапывать эту стюардессу без нужды. Лучше сделать что-то аналогичное на умных указателях.

    Одно исключение — это OPC (прежде всего OPC DA). Оно завязано на DCOM. Но при реализации — нарветесь на проблемы с «корпоративной» стабильностью.


    1. kekekeks
      28.10.2018 23:26

      С такой аргументацией нельзя использовать вообще никакие гномовские библиотеки. Они же все на GLib, а там g_object_ref и g_object_unref.


      1. Jef239
        28.10.2018 23:40

        Почему? Подсчет ссылок можно сделать по-разному. Можно иметь автоматические средства проверки. Можно иметь хорошие правила. Можно иметь макрос и темплейты, автоматизирующие подсчет.

        В COM- ничего этого нет. Типичная ситуцаия в COM. Есть объект А, у него интрефейс IA. Через этот интерфейс получаем интерфейсы IB и IC. Они могут относится к объекту А, а могут — ко вложенным объектам B и С. Это зависит от реализации. В результате, уничтожая ссылку на IA, мы можем получить неработоспобность B и С, потому что А им нужен, а он уничтожился (точнее при уничтожении А уничтожает B и С). А можем — не получить. И все это зависит от конкретной реализации, которая бывает какая угодно.


        1. kekekeks
          29.10.2018 00:14
          -1

          Описываемая вами ситуация произойти не может, т. к. QueryInterface увеличивает счётчик ссылок возвращаемого объекта.


          1. Jef239
            29.10.2018 00:19

            Да ну? Обоснуйте. Каким образом, увеличение ссылок для возвращаемого интерфейса IB может помещать мне сделать release для IA?


          1. 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.


            1. kekekeks
              29.10.2018 12:07
              +1

              При удалении объекта А, он удаляет и объект B, причем не взирая на его счетчик ссылок.

              Это прямое нарушение принципов работы с COM. Все COM-объекты должны быть в куче и все ссылки на COM-объект должны быть через умные указатели либо соответствующие механизмы клиентского языка. Описываемая вами явная ошибка программиста в коде компонента может случиться при использовании любой системы работы с учётом ссылок.


              1. Jef239
                29.10.2018 13:56

                Увы, в учебниках по COM встречается и агрегирование и делегирование. И это не «ошибка», а довольно распространенная практика реализации. Вплоть до примеров из книг от microsift press.

                В другой системе использование ссылок может быть глубоко под капотом. Или возможен явный запрет агрегирования или присутствовать хороший чекер.


                1. kekekeks
                  30.10.2018 10:29

                  Агрегирование и делегирование подразумевают раздельный подсчёт ссылок. Удаление объекта с живыми ссылками — это всегда явный косяк того, кто писал реализацию и к COM как таковому отношения не имеет.


                  1. Jef239
                    30.10.2018 13:08

                    Вот вы уже и запутались. Excel — это большой объект. Но внутри него сидит куча других объектов. Запускаем Excel руками, потом подключаемся к его вложенным объектам по COM/DCOM. Затем закрываем. Что будет при раздельном подсчете ссылок? Excel закроется и похоронит все вложенные объекты.

                    Вся фишка в том, что объемлющий объект должен иметь общий подсчет ссылок со вложенными. А не раздельный. Но, поскольку вложенные объекты могут быть независимыми (inProc COM изDLL ), это не всегда возможно.


  1. dmxrand
    28.10.2018 12:21

    Зачем божечки? Есть ведь CORBA. Я 15 лет назад использовал pythonhosted.org/Pyro4 Вообще работало из коробки…


  1. green-caterpillar
    28.10.2018 12:41

    Да любая библиотека, которую вы можете подключить к своей программе, может дать вам утечки памяти, краши и т.д. Это общая проблема использования чужого кода. Совершенно непонятно, при чем тут COM?