Портирование COM на Linux

Мне нравится технология 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


Спасибо.

Средняя зарплата в IT

111 111 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 6 788 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 38

    +1

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

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

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

          0

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

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

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

              0

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


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

            +4

            Для критиков "зачем это на 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# как родные.

              0

              Так Xamarin.Mac же...

                +2

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

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

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

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

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

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

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

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

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


                  Что позволяет делать обвязки для любых языков программирования, а не только C++.
                    +1
                    Так сложно было читать статью с таким большим количеством грамматических ошибок…
                      0
                      Ничего себе. В 2018-ом то. У меня сложилось впечатление, что COM и DCOM и на windows уже скорее мертв, чем жив; а вы его на Linux.
                        0

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

                          0
                          Ну а как же стабильный ABI?
                            0
                            Сделать все функции в обязательном порядке __cdecl или __stdcall (правда тут переменное число аргументов никак не получится, так что для vararg только __cdecl) и полностью запретить __fastcall, вот и получится стабильный ABI.
                            0
                            Вообще-то это чисто сишная технология, отсюда и универсальный ABI. Ее более удобно обернули в C++. Что естественно.
                          0
                          string scope = cmdline(«scope», "");

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

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

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

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

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

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

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

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

                                    0
                                    Да ну? Обоснуйте. Каким образом, увеличение ссылок для возвращаемого интерфейса IB может помещать мне сделать release для IA?
                                      0
                                      Давайте я ещё раз медленно и печально проясню ситуацию. Есть объект A с интерфейсом IA. Внутри него находится объект B c интерфейсом IB. Объект B написан независимо и ничего про объект А не знает. При удалении объекта А, он удаляет и объект B, причем не взирая на его счетчик ссылок.

                                      Получаем IA, от него получаем IB, делаем release на IA, Память, занята объектом B, возвращается в кучу. Через 15 минут использования интерфейса IB (объекта B) он зависает.

                                      Причем вы можете проверять своего клиента на десятке серверов. Все будет отлично работать, ибо у них интерфейсы IA и IB относятся к одному объекту А (или сделано делегирование вместо агрегатирования). А потом нарветесь на сервер с такой схемой — и кранты. Причем замены этому серверу нет, для данного устойства он один.

                                      Это вот и есть чудный мир OPC и COM.
                                        +1
                                        При удалении объекта А, он удаляет и объект B, причем не взирая на его счетчик ссылок.

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

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

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

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

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

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

                                                Внутренние объекты экселя начинают возвращать коды ошибок, если мне не изменяет память. Ссылки на них продолжают быть валидными, иначе всё начнёт сегфолтиться.


                                                На тему "досрочного" протухания дочерних объектов хорошо написано здесь

                                0
                                Зачем божечки? Есть ведь CORBA. Я 15 лет назад использовал pythonhosted.org/Pyro4 Вообще работало из коробки…
                                  0
                                  Да любая библиотека, которую вы можете подключить к своей программе, может дать вам утечки памяти, краши и т.д. Это общая проблема использования чужого кода. Совершенно непонятно, при чем тут COM?

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое