Roman Lut

  1. Взаємодія програм, написаних на різних мовах програмування
  2. Component object model (COM)
  3. Модулі на основі COM
  4. Додаток-приклад
  5. Правила опису COM-інтерфейсів
  6. VC ++ та Borland С ++ Builder
  7. Borland Delphi
  8. Managed C ++
  9. C #
  10. Висновок
  11. ПОСИЛАННЯ

Стаття опублікована на сайті dtf.ru

Пов'язані теми: Програмування з використанням абстрактних інтерфейсів, експорт класів з DLL, міжмовна взаємодія, система плагінів.

У цій статті я розповім, як використовувати COM інтерфейси для забезпечення бінарної сумісності між модулями, написаними на різних мовах програмування.

Взаємодія програм, написаних на різних мовах програмування

Незважаючи на те, що Андрій Плахов в своїй лекції про мови програмування на КРІ 2006 навіть не згадав про Delphi і C ++ Builder [12], ми активно використовуємо ці продукти для створення редакторів, утиліт і плагінів.

Причина проста: продукти Borland дозволяють дуже швидко і легко писати GUI-додатки, і для них існує величезна кількість корисних компонентів.

На жаль, простота написання GUI плагіна, скажімо, для редагування системи частинок, закінчується, коли стає необхідно пов'язати його з кодом движка, який безумовно написаний на Visual C ++.

Ні Delphi, ні C ++ Builder не є сумісними з Visual C ++ за форматом obj і lib файлів, тому єдиним способом зв'язування залишається експорт функцій з DLL.

Малюнок 1 Малюнок 1. Експорт класу як набору функцій.

В принципі, це працює, але є маса незручних моментів. Об'єктно-орієнтоване програмування перетворюється в "пародію на об'єкти".

Формат DLL дозволяє експортувати виключно функції. В Visual C ++ існує розширення, яке дозволяє експортувати класи та змінні, але знову ж таки, VC ++ тут ​​сумісна тільки сама з собою.

Тому доводиться писати і експортувати:

а) функцію-конструктор, яка конструює екземпляр класу (по new) і повертає покажчик на цей екземпляр у вигляді void *;
б) повний набір проксі-функцій, які дублюють методи класу, які приймають покажчик на екземпляр у вигляді void * і викликають на екземплярі класу відповідний метод;
в) функцію-деструктор, яка приймає покажчик на екземпляр у вигляді void * і знищує об'єкт.

приклад:

У 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 = cx; * Y = cy; * Z = cz; } 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).

Малюнок 2. Бінарний формат COM об'єкта.


Модулі на основі COM

Ідея використовувати COM-подібні інтерфейси є розширенням ідеї використовувати абстрактні інтерфейси в дизайні движка [4]. Переваги такого підходу описані в згаданій статті: поділ інтерфейсу і реалізації, інкапсуляція, низька зв'язаність і, як результат, зрозуміла архітектура і простота супроводу.

Ігровий "движок" являє собою набір різних менеджерів (об'єктів, текстур, моделей, рівнів і т.д.). Створивши COM-інтерфейс для всіх цих об'єктів, можна забезпечити широкі можливості для написання плагінів.

Система плагінів включає в себе:

  • менеджер плагінів - dlvmnager.dll. Менеждер займається завантаженням плагінів і диспетчеризацией виклику DLVManager.GetInterface () в усі модулі DLV (аналог CoCreateInstance ());
  • плагіни - модулі dll, перейменовані в dlv. DLV модуль експортує три функції: DLV_Init (), DLV_GetInterface () і DLV_Close ().
  • кожному опису інтерфейсу зіставляється унікальний індентіфікатор (DWORD) і версія (DWORD) (аналог GUID);
  • для розширення функціональності програми, плагін або налаштовує callbacks / listeners в DLV_Init (), або створює об'єкти / фабрики об'єктів з відомими Id в DLV_GetInterface ().

кожному опису інтерфейсу зіставляється унікальний індентіфікатор (DWORD) і версія (DWORD) (аналог GUID);   для розширення функціональності програми, плагін або налаштовує callbacks / listeners в DLV_Init (), або створює об'єкти / фабрики об'єктів з відомими Id в DLV_GetInterface ()

Малюнок 3. Архітектура системи плагінів.

Зручність використання COM-інтерфейсів полягає в тому, що покажчик на інтерфейс можна вільно передавати між модулями, написаними на різних мовах програмування. Досить експортувати з DLL функцію:

void GetInterface (void ** pInteface, DWORD interfaceID, DWORD interfaceVersion);

і інші модулі зможуть отримувати покажчики на інтерфейси до менеджерів в цій DLL, і зможуть працювати з ними як з класами. Які це дає переваги перед методом, описаним на початку статті:

  • З DLL необхідно експортувати всього одну функцію (GetInterface ()), незалежно від кількості інтерфейсів, реалізованих в DLL. Це єдина функція, для якої потрібно отримувати адреса, використовуючи GetProcAddress ().
  • Для використання класу в програмі на іншій мові досить описати його COM-інтерфейс.
  • При додаванні нового методу потрібно всього лише додати його в опис інтерфейсу (на всіх мовах). Порівняйте це з необхідністю описувати і експортувати proxy-функцій, а також отримувати її адресу по GetProcAddress ().

Додаток-приклад

До статті додається додаток, що реалізує запропоновану систему [20.1].

Основний модуль програми є діалог рис.4. Модуль реалізований на Borland C ++ Builder 6.0.

Модуль реалізований на Borland C ++ Builder 6

Малюнок 4. Основне вікно програми-прикладу.

Основний модуль реалізує наступні інтерфейси:

  • IFractalFactory - фабрика геометричних фігур;
  • ICanvas - область малювання;
  • IFractalMaker і IFractal (див. Нижче).

Клас, який реалізує інтерфейс 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-інтерфейсів. Крім нього, потрібно знати також наступні правила:

  • в якості параметрів можна передавати тільки прості типи (int, byte, ...), покажчики на прості типи (int *, byte *, char *, ...), покажчики на структури (T3DVECTOR * V, ...), покажчики на масиви (int * , ....), покажчики на інтерфейси (IFractal *). При описі структури необхідно подбати, щоб вирівнювання членів структури було зазначено явно (#pragma pack ());
    Приклад.
    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);
  • забороняється передавати покажчики на класи або спеціальні типи даних (наприклад, String в Delphi). Якщо необхідно передавати покажчик на клас, то потрібно описати інтерфейс, який реалізує цей клас, і передавати покажчик на інтерфейс;
    Приклад.
    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);
  • об'єкти в різних модулях використовують різні менеджери пам'яті. Якщо метод повертає покажчик на структуру, виділену в купі, то повинен існувати метод для звільнення цього блоку пам'яті;
    Приклад. Для звільнення об'єкта використовується метод Release ().
    IFractal * fractal; factory-> Make (ComboBox1-> Items-> Strings [ComboBox1-> ItemIndex] .c_str (), & fractal); fractal-> Draw (CanvasWrapper); fractal-> Release ();
  • за стандартом, все методи COM інтерфейсу (крім AddRef () і Release ()) повинні повертати HRESULT. Допускається повертати інші типи, але інтерфейс не буде сумісний з C ++. Net і C # .net. Не можна повертати структури за значенням, так як в цьому випадку різні компілятори створюють несумісний код.
    Приклад.
    virtual HRESULT __stdcall GetVersion (OUT DWORD * version) {* version = IFractalMaker :: VERSION; return S_OK; }
  • всі методи повинні використовувати метод виклику __stdcall;
  • як я вже говорив раніше, інтерфейси не містять полів даних, конструкторів, деструкторів, а всі методи оголошені віртуальними абстрактними.
  • інтерфейси не успадковуються, але один об'єкт може реалізовувати декілька інтерфейсів (тобто об'єкт може бути успадкований від декількох інтерфейсів).

VC ++ та Borland С ++ Builder

Опис інтерфейсу на мові C ++ повністю збігається з описом абстрактного класу (стандартні макроси DECLARE_INTERFACE () з Platform SDK описують інтерфейси як структури).

Приклад опису COM платформ на C ++:

// ================================================ =========== // ICanvas // ================================== ========================= DECLARE_INTERFACE_ (ICanvas, IUnknown) {IUNKNOWN_METHODS_PURE (0x83893202,0x00010000) virtual HRESULT __stdcall GetWidth (OUT DWORD * width) const = 0; virtual HRESULT __stdcall GetHeight (OUT DWORD * height) const = 0; virtual HRESULT __stdcall DrawPixel (DWORD x, DWORD y, DWORD RGB) = 0; virtual HRESULT __stdcall DrawLine (DWORD x1, DWORD y1, DWORD x2, DWORD y2, DWORD RGB) = 0; };

Архітектура COM вимагає, щоб всі інтерфейси були успадковані від IUnknown. На практиці це означає, що першими трьома методами в описі інтерфейсу повинні бути QueryInterface (), AddRef (), RemoveRef (). Вони оголошуються макросом IUNKNOWN_METHODS_PURE.

// Definition of IUnknown methods #define IUNKNOWN_METHODS_PURE (InterfaceId, InterfaceVersion) \ virtual HRESULT __stdcall QueryInterface (REFIID riid, void ** ppv) = 0; \ Virtual ULONG __stdcall AddRef () = 0; \ Virtual ULONG __stdcall Release () = 0; \ Typedef enum \ {\ ID = ## InterfaceId, \ VERSION = ## InterfaceVersion, \ FORCE_DWORD = 0xffffffff \} desc; \


Ось так виглядає реалізація інтерфейсу в класі TCanvasWrapper, який представляє собою область малювання:

// ========================================= // class TCanvasWrapper // = ======================================== class TCanvasWrapper: public ICanvas {private: DWORD width ; DWORD height; TCanvas * canvas; DWORD SwapRB (DWORD RGB); public: // ================== begin COM interface =================== IUNKNOWN_METHODS_IMPLEMENTATION_REFERENCE () virtual HRESULT __stdcall GetWidth (OUT DWORD * width) const; virtual HRESULT __stdcall GetHeight (OUT DWORD * height) const; virtual HRESULT __stdcall DrawPixel (DWORD x, DWORD y, DWORD RGB); virtual HRESULT __stdcall DrawLine (DWORD x1, DWORD y1, DWORD x2, DWORD y2, DWORD RGB); // ================== end COM interface =================== TCanvasWrapper (TCanvas * canvas, DWORD width , DWORD height); };

Оскільки реалізація методів інтерфейсу IUnknown у всіх об'єктів буде однакова, досить визначити два макроси IUNKNOWN_METHODS_IMPLEMENTATION_REFERENCE (без підрахунку посилань) і IUNKNOWN_METHODS_IMPLEMENTATION_INSTANCE (з підрахунком посилань), або створити проміжні класи TUnknown, що реалізують ці методи.

Тут слід згадати невеликий нюанс: необхідно, щоб метод QueryInterface () повертав правильні покажчики на інтерфейс IUnknown, а також покажчик на реалізований інтерфейс, так як це необхідно для підтримки плагінів на .net мовах (див. Нижче).

// Implementation of IUnknown methods for singletons - no reference counting #define IUNKNOWN_METHODS_IMPLEMENTATION_REFERENCE () \ virtual HRESULT __stdcall QueryInterface (REFIID riid, void ** ppv) \ {\ if (riid == IID_IUnknown) \ {\ * ppv = this; \ Return S_OK; \} \ Else \ if (riid.Data1 == (unsigned long) ID && riid.Data2 == 0 && riid.Data3 == 0 && \ riid.Data4 [0] == 0 && riid.Data4 [1] = = 0 && \ riid.Data4 [2] == 0 && riid.Data4 [3] == 0 && \ riid.Data4 [4] == 0 && riid.Data4 [5] == 0 && \ riid.Data4 [ 6] == 0 && riid.Data4 [7] == 0) \ {\ * ppv = this; \ Return S_OK; \} \ Else \ {\ * ppv = NULL; \ Return E_NOINTERFACE; \} \}; \ Virtual ULONG __stdcall AddRef () {return 1;}; \ Virtual ULONG __stdcall Release () {return 1;};

Borland Delphi

Для опису COM інтерфейсів в Object Pascal використовується ключове слово interface:

const IID_ICANVAS = $ 83893202; ICANVAS_VERSION = $ 00010000; // ================================================ =========== // ICanvas // ================================== ========================= type ICanvas = interface (IUnknown) function GetWidth (var width: DWORD): HRESULT; stdcall; function GetHeight (var height: DWORD): HRESULT; stdcall; function DrawPixel (x, y, RGB: DWORD): HRESULT; stdcall; function DrawLine (x1, y1, x2, y2, RGB: DWORD): HRESULT; stdcall; end;


При цьому методи інтерфейсу IUnknown оголошуються неявно, і їх потрібно реалізувати в спадкоємця:

// ================================================ ===== // class IUNKNOWN_InstanceBase // ======================================= ============== type IUNKNOWN_InstanceBase = class (TObject, IUnknown) private refCount: DWORD; protected function DoQueryInterface (const IID: TGUID; out Obj): HResult; virtual; abstract; public function QueryInterface (const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; constructor Create (); end;


Покажчики на COM-інтерфейси в Object Pascal є "розумними" покажчиками. Щоб правильно їх використовувати, необхідно знати наступні правила:

  1. Присвоєння адреси вказівником на інтерфейс автоматично викликає AddRef () на копійованому інтерфейсі, і Release () на звільняються:
    iptr1: ISomeInteface; iptr2: ISomeInterface; iptr1: = iptr2; // викликає Release () на iptr1, викликає AddRef () на iptr2


    Це ж правило діє при присвоєнні вказівниками на інтерфейси, переданим в функцію по var.

  2. При створенні змінної-покажчика на інтерфейс, їй автоматично привласнюється значення nil.
  3. Присвоєння покажчику на інтерфейс значення nil викликається Release () на звільняються інтерфейсі (якщо він не-nil):
    iptr1: ISomeInteface; iptr1: = nil; // викликає Release () на iptr1
  4. Якщо покажчик на інтерфейс виходить з області видимості, завершальний код перевірять значення покажчика, і якщо він не дорівнює nil, викликає Release ():
    procedure myFunc (); var iptr: ISomeInterface; begin iptr: = ... ... end; // тут неявно викликається Release ()
  5. При присвоєнні структур, членами яких є покажчики на інтерфейси, на кожного не-nil інтерфейсі викликаються Release () / AddRef (). При виході із зони видимості структури, членами якої є покажчики на інтерфейси, на кожного не-nil інтерфейсі викликається Release ().
  6. При передачі покажчика на інтерфейс як параметр у функцію за значенням, на інтерфейсі викликається AddRef (), при передачі по посиланню - немає:
    procedure MyFunc1 (ptr: ISomeInterface); // викликати AddRef () на вході в функцію, Release () на виході procedure MyFunc2 (var ptr: ISomeInterface); // не викликає AddRef () / Release () procedure MyFunc3 (const ptr: ISomeInterface); // не викликає AddRef () / Release ()

Якщо функція повертає покажчик на інтерфейс, то AddRef () викликається один раз:

function GetInterface (): ISomeInterface; begin result: = TSomenterfaceImplementator.Create (); // викликає AddRef (), refount = 2 // передбачається, що TSomenterfaceImplementator.Create () створює об'єкт з початковим значенням лічильника посилань, рівним 1 end; var iptr: ISomeInterface; begin iptr: = GetInterface (); // AddRef () не викликається, refcount = 2 end;

Це поведінка більш очевидно, якщо згадати, що при поверненні з функції структури (або покажчика на інтерфейс), в функцію передається адресу, за якою потрібно записати вихідну значення. Самої змінної result насправді не існує:

procedure GetInterface (var result: ISomeInterface); // еквівалентно функції

Очевидно, що зазначені правила досить складні. Можна значно спростити поведінку компілятора, і позбутися від "закулісної магії", якщо працювати з покажчиками на інтерфейси як зі звичайними покажчиками, і приводити їх до типу покажчика на інтерфейс тільки безпосередньо при виклику методу:

Var iptr: pointer; Begin iptr: = GetInterfacePointer (); // не викликає AddRef (); (Ф-ція повертає Pointer) ISomeInterface (iptr) .SomeMethod (); // викликати метод інтерфейсу iptr: = nil; // не викликає Release ();

Можна позбутися від зайвих викликів AddRef () і Release (), приводячи тип покажчика на інтерфейс до звичайного покажчика:

Var iptr: ISomeInterface; begin pointer (iptr): = GetInterfacePointer (); // не викликає AddRef (); (Ф-ція повертає Pointer) ... iptr.SomeMethod (); ... // не забуваємо обнулити покажчик, інакше при виході з функції буде неявно викликаний Release () pointer (iptr): = nil; // не викликає Release (); end;

Приклад 2:

Var Iptr1: ISomeInterface; iptr2: ISomeInterface; iptr1: = iptr2; // викликає iptr1.Release () і iptr2.AddRef (); pointer (iptr1): = pointer (iptr2); // не викликає нічого


Використання звичайних покажчиків особисто мені бачиться більш простим. Єдине, що хочу зауважити: якщо не використовується підрахунок посилань, то при видаленні об'єктів, що реалізують інтерфейс, потрібно дотримуватися обережності - необхідно явно обнулити всі покажчики на інтерфейс, щоб компілятор викликав Release () на знищеному об'єкті.
приклад:

var iptr: ISomeInterface; i: integer; begin for i: = 0 to Manager.ObjectsCount () - 1 do begin pointer (iptr): = Manager.GetObject (i); if iptr.Selected () = true then begin Manager.DeleteObject (i); // обов'язково обнулити покажчик, інакше при виході з функції буде викликаний Release () на знищеному об'єкті. pointer (iptr): = nil; break; end; end; end;


При цьому явне оголошення змінної-покажчика на інтерфейс в даному випадку є необхідним. Що неправильно в наступному прикладі?

var i: integer; begin for i: = 0 to Manager.ObjectsCount () - 1 do if ISomeInterface (Manager.GetObject (i)). Selected () = true then begin Manager.DeleteObject (i); break; end; end;


Якщо Ви здогадалися, що компілятор _может_ створити тимчасову змінну типу ISomeInterface, яку постарається знищити при виході з процедури (тобто після видалення об'єкта) - знімаю перед Вами капелюха, далі можна не читати.

Managed C ++

Взагалі-то, при розробці під платформу .net, проблема міжмовної взаємодії відсутній в принципі. Однак, якщо деякі модулі додатки написані native мовами, можна скористатися COM інтерфейсами.

Для того, щоб дати можливість native клієнтам викликати managed інтерфейс, необхідно оголосити його зі спеціальними атрибутами:

// ================================================ ========== // ICanvas // =================================== ======================= [InterfaceTypeAttribute (ComInterfaceType :: InterfaceIsIUnknown), GuidAttribute ( "83893202-0000-0000-0000-000000000000")] public interface class ICanvas {public: virtual void GetWidth (OUT unsigned int * width) = 0; virtual void GetHeight (OUT unsigned int * height) = 0; virtual void DrawPixel (unsigned int x, unsigned int y, unsigned int RGB) = 0; virtual void DrawLine (unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2, unsigned int RGB) = 0; };

При описі інтерфейсу на інших мовах потрібно враховувати, що всі методи повинні повертати HRESULT; в .net це мається на увазі неявно.

Реалізація інтерфейсу на managed C ++ компілюється в керований код, тому неможливо безпосередньо викликати методи інтерфейсу з native коду. Для цього компілятор створює так званий Callable COM Wrapper - спеціальний клас в native коді, який виробляє конвертацію типів і виклик managed функцій.

Малюнок 5Малюнок 5. Callable COM Wrapper (CCW).

Отримати CCW можна за допомогою наступної функції: // ======================================== ======= // IntPtr GetCCW () // =================================== ============ // return ptr to COM callable wrapper for object implementing interface interfaceType // used to pass pointers to interfaces out of .net framework static IntPtr GetCCW (Object ^ obj, Type ^ interfaceType) {GuidAttribute ^ ga = (GuidAttribute ^) Attribute :: GetCustomAttribute (interfaceType, GuidAttribute :: typeid); String ^ SIID = ga-> Value; Guid guid (SIID); IntPtr unknownIntPtr = Marshal :: GetIUnknownForObject (obj); // calls AddRef () IntPtr CCW; Marshal :: QueryInterface (unknownIntPtr, guid, CCW); // we should not add refs when asking for CCW; // app is responsible for lifetime of object int ii = Marshal :: Release (unknownIntPtr); System :: Diagnostics :: Debug :: WriteLine ( "refcount after GetCCW () =" + ii); return CCW; }


Для того, щоб managed код міг викликати методи native COM інтерфейсів, необхідно отримати покажчик на спеціальний об'єкт (Runtime Callable Wrapper, RCW), який буде виробляти конвертацію типів і виклик native коду:

ICanvas ^ Canvas = (ICanvas ^) Marshal :: GetTypedObjectForIUnknown (pCanvas, ICanvas :: typeid);

Малюнок 6Малюнок 6. Runtime Callable Wrapper (RCW).

Для того, щоб код міг правильно отримати покажчик на такий об'єкт, необхідно, щоб реалізація методу QueryInterface () коректно повертала покажчики на інтерфейси IUnknown і запитуваний інтерфейс - см. Реалізацію методу QueryInterface () - макрос IUNKNOWN_METHODS_ IMPLEMENTATION_REFERENCE (), наведений вище.

Оскільки DLL містить .net код, менеджер плагінів не може безпосередньо викликати функції DLV_Init (), DLV_Close () і DLV_GetInterface (). Описавши їх як extern "C" __ declspec (dllexport), ми змушуємо компілятор створити native-код для виклику .net функцій з native коду:

extern "C" __declspec (dllexport) void DLV_Init () {...}

C #

З усіх згаданих мов, C # найбільш складно використовувати для написання плагінів. Взагалі кажучи, відсутність можливості експортувати native-функціонально з DLL робить це неможливим. Виходить, що для того, щоб написати плагін на C #, доведеться ще написати і Managed C ++ проект, який експортує функції DLV_Init (), DLV_Close (), DLV_GetInterface () і викличе відповідні методи з збірки на C #.

Саме так все і працює, коли ви створюєте COM-сервери на .net мовах: як COM-сервера реєструється не збірка, а спеціальна DLL, що викликає методи з збірки.

Однак, порившись в інтернеті, я все-таки знайшов спосіб експортувати native функції з C # .net збірки [19.1]. Негативною стороною описаного способу є відсутність можливості запускати плагін під отладчиком після такого перетворення збірки.

Опис інтерфейсу на C # слід тим же правилам, що і Managed C ++:

// ================================================ ========== // ICanvas // =================================== ======================= [InterfaceTypeAttribute (ComInterfaceType.InterfaceIsIUnknown), Guid ( "83893202-0000-0000-0000-000000000000")] public interface ICanvas { void GetWidth (out uint width); void GetHeight (out uint height); void DrawPixel (uint x, uint y, uint RGB); void DrawLine (uint x1, uint y1, uint x2, uint y2, uint RGB); }; public static class iCanvas {public static uint ID = 0x83893202; public static uint VERSION = 0x00010000; };

Ті ж правила діють і при отриманні CCW / RCW.

Крім цього, потрібно знати ще кілька правил:

  1. У .net використовується збирач сміття. Він може переміщати об'єкти в пам'яті. Тому в native код можна передавати тільки ті покажчики, які вказують на об'єкти, спеціально створені в непереміщуваними пам'яті: private IntPtr name; name = System.Runtime.InteropServices.Marshal.StringToHGlobalAnsi ( "Diamond, implemented in C # plugin"); public void GetDesc (out IntPtr desc) {desc = name; }
  2. Складальник сміття знищує об'єкт тільки в момент складання сміття. Це означає, що об'єкт може "жити" ще дуже довго після моменту, коли лічильник посилань досяг 0. Якщо об'єкт використовує якісь ресурси, то необхідно передбачити метод, який викликає їх звільнення, так як деструктор об'єкта не буде викликаний в момент звільнення об'єкта по Release ()
  3. При закритті плагіна необхідно викликати методи: GC.Collect (); GC.WaitForPendingFinalizers ();

    щоб коректно відпрацювали деструктори всіх об'єктів (тому проблема, яка згадується до коментарів до статті [19.2] насправді не існує).

У книзі [21] дуже докладно описані різні приклади взаємодії managed і native коду.

Висновок

Описану систему можна розширити і для інших мов: VB, VB.net, J # і ін. На жаль, я занадто погано знаю ці мови, щоб написати приклад.

До статті додається додаток-приклад [20.2]. Описані в ньому прийоми роботи наочно розкривають способи реалізації.

Приблизно така ж система використовується в движку Vital Engine 3.0. У прикладі до даної статті, система плагінів трохи спрощена, щоб зосередити увагу на основних принципах, а також розширена для підтримки .NET мов.

ПОСИЛАННЯ

[1] 3 кита COM. Кіт перший: реєстр

[2] 3 кита COM. Кіт другий: dll

[3] Adding Plug-ins To Your Application

[4] "Programming with abstract interfaces"
Book: "Game Programming Gems 2"

[5] "Exporting C ++ classes from DLLs"
Book: "Game Programming Gems 2"

[6] COM Interface Basics

[7] Abstract class versus Interface

[8] C ++ і Java: спільне використання

[9] COM in plain C

[10] How to automate exporting .NET function to unmanaged

[11] Архів статей "Що таке" технологія COM "

[12] Андрій Плахов.Параллельное вимір, або за гранню C ++

[13] Виклик Delphi DLL з MS Visual C ++

[14] Using C ++ objects in Delphi

[15] Utilizing Delphi Codes in VC Without Using a DLL

[16] Using C DLLs with Delphi

[17] Step by Step: Calling C ++ DLLs from VC ++ and VB - Part 2

[18] Створення в середовищі Borland C ++ Builder dll, сумісної з Visual C ++

[19.1] [19.2] Unmanaged code can wrap managed methods

[20.1] [20.2] Додаток - приклад

[21] Bruce Bukovics. .NET 2.0 Interoperability recipes.
ISBN-13: 978-1-59059-669-2, ISBN-10: 1-59059-669-2

Що неправильно в наступному прикладі?