Плагины на основе COM интерфейсов |
Страница 1 из 3
|
Роман Лут
В период с 1997 по 2001 работал в компании GSC Game World над 3D-экшеном Venom: Codename Outbreak в качестве программиста графического движка и дизайнера уровней. С 2002 работает в компании Deep Shadows ведущим программистом графического движка. Принимал непосредственное участие в работе над экшен-ролевым проектом Xenus: Boiling Point.
Связанные темы: Программирование с использованием абстрактных интерфейсов, экспорт классов из DLL, межъязыковое взаимодействие, система плагинов.
В этой статье я расскажу, как использовать COM интерфейсы для обеспечения бинарной совместимости между модулями, написанными на разных языках программирования.
Взаимодействие программ, написанных на разных языках программирования
Несмотря на то, что Андрей Плахов в своей лекции о языках программирования на КРИ 2006 даже не упомянул о Delphi и C++ Builder[12], мы активно используем эти продукты для создания редакторов, утилит и плагинов.
Причина проста: продукты Borland позволяют очень быстро и легко писать GUI-приложения, и для них существует огромное количество полезных компонентов.
К сожалению, простота написания GUI плагина, скажем, для редактирования системы частиц, заканчивается, когда становится необходимо связать его с кодом движка, который безусловно написан на Visual C++.
Ни Delphi, ни C++ Builder не являются совместимыми с Visual C++ по формату obj и lib файлов, поэтому единственным способом связывания остается экспорт функций из DLL.
В принципе, это работает, но есть масса неудобных моментов. Объектно-ориентированное программирование превращается в "пародию на объекты".
Формат DLL позволяет экспортировать исключительно функции. В Visual C++ существует расширение, которое позволяет экспортировать классы и переменные, но опять же, VC++ здесь совместима только сама с собой.
Поэтому приходится писать и экспортировать:
Пример:
В DLL на VC++ реализован класс TSphere. Вот так выглядит его экспорт-импорт в Delphi:
=================== VC ++ =================
class TSphere
private:
T3DVECTOR center;
float radius;
public:
...
T3DVECTOR& GetCenter() const;
float GetRadius() const;
...
};
void* __cdecl TSphere _Create()
{
return new TSphere();
}
void __cdecl TSphere _GetCenter(void* pthis, float* x, float* y, float*z)
{
T3DVECTOR c = ( (TSphere*)pthis)->GetCenter();
*x = c.x;
*y = c.y;
*z =c.z;
}
float __cdecl TSphere _GetRadius(void* pthis)
{
return ( (TSphere*)pthis)->GetRadius();
}
TSphere _Destroy(void* pthis)
{
delete ( (TSphere*)pthis);
}
=================== Delphi =================
function TSphere _Create(): pointer; cdecl; external 'mydll.dll';
procedure TSphere _GetRadius(pthis: pointer; var x,y,z: single); cdecl; external 'mydll.dll';
function TSphere _GetRadius(pthis: pointer): single; cdecl; external 'mydll.dll';
procedure TSphere _Delete(pthis: pointer); cdecl; external 'mydll.dll';
var
p: pointer;
p:= TSphere _Create();
radius:= TSphere_GetRadius(p);
TSphere_Destroy(p);
Очевидно, что так работать совсем неудобно. При изменении или добавлении метода класса, необходимо исправлять прокси-функцию, ее отражение в проекте на другом языке, и заново пересобирать оба проекта. Кроме того, в случае экспорта Delphi->VC++ приходится описывать получение адреса через GetProcAddress(), так как VC++ не позволяет просто написать "функция находится в такой-то DLL", как это можно в Delphi. DLL приходится загружать динамически, с помощью функции LoadLibrary().
Несмотря на недостатки, этот способ широко используется при портировании библиотек. На эту тему есть множество статей [13] [14] [15] [16] [17] [18].
Фундаментальной проблемой является то, что один и тот же класс, откомпилированный разными компиляторами, или даже одним и тем же компилятором, но с разными настройками, не совместим в бинарном виде.
Нельзя передавать указатель на экземпляр класса из модуля на VC++ в модуль на C++Builder, в котором пытаться вызывать методы этого класса. Даже если используется один и тот же .h-файл с описанием класса.
К счастью, практически все компиляторы поддерживают Component Object Model (COM).
Оформляя классы, как COM-объекты, но не используя все "тяжелые" возможности COM, можно добиться, чтобы классы были бинарно-совместимыми между разными языками программирования и компиляторами.
Component object model (COM)
Архитектура COM - достаточно обширная тема, поэтому я просто укажу ссылки [1] [2] [6] [9] [11].
Принцип работы архитектуры COM в двух словах можно объяснить следующим образом.
Код классов располагается в библиотеках (DLL), которые регистрируются в специальном разделе системного реестра. Каждый класс реализует один или несколько известных COM-интерфейсов. DLL экспортирует функцию для создания экземпляра указанного класса, которая возвращает указатель на базовый интерфейс IUnknown (все классы обязаны реализовывать этот интерфейс).
Каждому интерфейсу (описанию интерфейса) сопоставляется уникальный идентификатор (Global unique identifier - GUID).
Для создания экземпляра класса пользователь вызывает функцию CoCreateInstance(GUID) из библиотеки COM. Именно она занимается просмотром записей в реестре, загрузкой DLL и вызовом функции создания экземпляра.
Пользователь работает с объектом через указатель на интерфейс.
Для контроля существования объекта используется подсчет ссылок. После использования экземпляра класса, пользователь обязан вызвать IUnknown->Release() для уничтожения объекта.
Архитектура COM предполагает независимость от компиляторов, и поэтому бинарный формат COM-интерфейсов строго регламентирован.
COM-интерфейс можно воспринимать как базовый абстрактный класс - без конструктора, деструктора и полей данных. На самом деле, именно так он и описывается в C++.
Указатель на COM объект можно воспринимать как указатель на экземпляр класса, который наследован от базового абстрактного класса.
В бинарном виде указатель на COM объект представляет собой указатель на экземпляр объекта, в первых четырех байтах которого содержится указатель на таблицу виртуальных функций (vtable).
Плагины на основе COM
Идея использовать COM-подобные интерфейсы является расширением идеи использовать абстрактные интерфейсы в дизайне движка[4]. Преимущества такого подхода описаны в упомянутой статье: разделение интерфейса и реализации, инкапсуляция, низкая связанность и, как результат, понятная архитектура и простота сопровождения.
Игровой "движок" представляет собой набор различных менеджеров (объектов, текстур, моделей, уровней и т.д.). Создав COM-интерфейс для всех этих объектов, можно обеспечить широкие возможности для написания плагинов.
Система плагинов включает в себя:
Удобство использования COM-интерфейсов состоит в том, что указатель на интерфейс можно свободно передавать между модулями, написанными на разных языках программирования. Достаточно экспортировать из DLL функцию:
void GetInterface(void** pInteface, DWORD interfaceID, DWORD interfaceVersion);и другие модули смогут получать указатели на интерфейсы к менеджерам в этой DLL, и смогут работать с ними как с классами.
Какие это дает преимущества перед методом, описанным в начале статьи:
Приложение-пример
К статье прилагается приложение, реализующее предложенную систему[20.1].
Основной модуль приложения представляет собой диалог рис.4. Модуль реализован на Borland C++ Builder 6.0.
Основной модуль реализует следующие интерфейсы:
Класс, реализующий интерфейс IDLVManager, находится в DLVManager.dll. Модуль реализован на VC++ 7.0.
Плагины - модули DLV - реализуют интерфейсы IFractalMaker и IFractal (реализованы на VC++ 7.0, Borland C++ Builder, Borland Delphi 6.0, C++ .net, C# .net).
При инициализации системы плагинов с помощью метода DLVManager->Init(), все модули регистрируют в фабрике IFractalFactory набор классов, реализующих интерфейс IFractalMaker.
Интерфейс IFractalMaker предназначен для получения описания и создания экземпляров классов с интерфейсом IFractal.
Интерфейс IFractal предназначен для рисования выбранной геометрической фигуры на ICanvas.
Правила описания COM-интерфейсов
Каждый язык программирования имеет свой синтаксис описания COM-интерфейсов. Кроме него, нужно знать также следующие правила:
Пример.
| C++ | virtual HRESULT __stdcall DrawPixel(DWORD x, DWORD y, DWORD RGB) = 0; |
| Delphi | function DrawPixel(x, y, RGB : DWORD): HRESULT; stdcall; |
| C++.net | virtual void DrawPixel(unsigned int x, unsigned int y, unsigned int RGB) = 0; |
| C# | void DrawPixel(uint x, uint y, uint RGB); |
| C++ | virtual HRESULT __stdcall GetDesc(OUT const char** desc) const = 0; |
| Delphi | function GetDesc(var desc: pchar): HRESULT; stdcall; |
| C++.net | virtual void GetDesc(OUT const char** desc) = 0; |
| C# | void GetDesc(out IntPtr desc); |
Пример.
| C++ | virtual HRESULT __stdcall Make(OUT IFractal** instance) = 0; |
| Delphi | function Make(var instance: pointer): HRESULT; stdcall; |
| C++.net | virtual void Make(OUT IntPtr* instance) = 0; |
| C# | void Make(out IntPtr instance); |
IFractal* fractal; factory->Make(ComboBox1->Items->Strings[ComboBox1->ItemIndex].c_str(), &fractal); fractal->Draw(CanvasWrapper); fractal->Release();
virtual HRESULT __stdcall GetVersion(OUT DWORD* version)
{
*version = IFractalMaker::VERSION;
return S_OK;
}
Copyright © 2013 ООО "ДТФ.РУ". Все права защищены.
Воспроизведение материалов или их частей в любом виде и форме без письменного согласия запрещено.
Замечания и предложения отправляйте через форму обратной связи.