Rambler's Top100

Твой мир ПРОграммирования-Delphi, Pascal, C++

Объявление

Если это Ваш первый визит на форум, то советуем зарегистрироваться. Зарегистрированные пользователи могут задавать вопросы и просматривать темы со скрытым текстом. Чтобы зарегистрироваться, нажмите Регистрация.

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Твой мир ПРОграммирования-Delphi, Pascal, C++ » Создание игр » Плагины на основе COM интерфейсов


Плагины на основе COM интерфейсов

Сообщений 1 страница 24 из 24

1

Часть1:

Код:
Роман Лут
В период с 1997 по 2001 работал в компании GSC Game World над 3D-экшеном Venom: Codename Outbreak в качестве программиста графического движка и дизайнера уровней. С 2002 работает в компании Deep Shadows ведущим программистом графического движка. Принимал непосредственное участие в работе над экшен-ролевым проектом Xenus: Boiling Point. 
Связанные темы: Программирование с использованием абстрактных интерфейсов, экспорт классов из DLL, межъязыковое взаимодействие, система плагинов. 
В этой статье я расскажу, как использовать COM интерфейсы для обеспечения бинарной совместимости между модулями, написанными на разных языках программирования.

Взаимодействие программ, написанных на разных языках программирования

Код:
Несмотря на то, что Андрей Плахов в своей лекции о языках программирования на КРИ 2006 даже не упомянул о Delphi и C++ Builder, мы активно используем эти продукты для создания редакторов, утилит и плагинов. 
Причина проста: продукты Borland позволяют очень быстро и легко писать GUI-приложения, и для них существует огромное количество полезных компонентов. 
К сожалению, простота написания GUI плагина, скажем, для редактирования системы частиц, заканчивается, когда становится необходимо связать его с кодом движка, который безусловно написан на Visual C++. 
Ни Delphi, ни C++ Builder не являются совместимыми с Visual C++ по формату obj и lib файлов, поэтому единственным способом связывания остается экспорт функций из DLL.

0

2

Рисунок 1. Экспорт класса как набор функций.

0

3

0

4

=================== Delphi  =================

0

5

Component object model (COM)

0

6

Рисунок 2. Бинарный формат COM объекта.

0

7

Плагины на основе COM

Продолжение следует...

0

8

Код:
Удобство использования COM-интерфейсов состоит в том, что указатель на интерфейс можно свободно передавать между модулями, написанными на разных языках программирования. Достаточно экспортировать из DLL функцию: 
void GetInterface(void** pInteface, DWORD interfaceID, DWORD interfaceVersion);
и другие модули смогут получать указатели на интерфейсы к менеджерам в этой DLL, и смогут работать с ними как с классами. 
Какие это дает преимущества перед методом, описанным в начале статьи: 
1. Из DLL необходимо экспортировать всего одну функцию (GetInterface()), независимо от количества интерфейсов, реализованных в DLL. Это единственна функция, для которой нужно получать адрес, используя GetProcAddress(). 
2. Для использования класса в программе на другом языке достаточно описать его COM-интерфейс. 
3. При добавлении нового метода нужно всего лишь добавить его в описание интерфейса (на всех языках). Сравните это с необходимостью описывать и экспортировать proxy-функций, а также получать ее адрес по GetProcAddress().

0

9

Правила описания COM-интерфейсов

0

10

VC++ и Borland С++ Builder

0

11

Код:
//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;};

0

12

Borland Delphi

0

13

Код:
Это же правило действует при присваивании указателям на интерфейсы, переданным в функцию по var. 
5.	При создании переменной-указателя на интерфейс, ей автоматически присваивается значение nil. 
6.	Присваивание указателю на интерфейс значения nil вызывается Release() на освобождаемом интерфейсе(если он не-nil): 
7.	iptr1: ISomeInteface;
iptr1:=nil; //вызывает Release() на iptr1
8.	Если указатель на интерфейс выходит из области видимости, завершающий код проверят значение указателя, и если он не равен nil, вызывает Release(): 
9.	procedure myFunc();
10.	var
11.	 iptr: ISomeInterface;
12.	begin
13.	 iptr:=…
14.	 …
end;  //здесь неявно вызывается Release()
15.	При присваивании структур, членами которых являются указатели на интерфейсы, на каждом не-nil интерфейсе вызываются Release()/AddRef(). При выходе из зоны видимости структуры, членами которой являются указатели на интерфейсы, на каждом не-nil интерфейсе вызывается Release(). 
16.	При передаче указателя на интерфейс в качестве параметра в функцию по значению, на интерфейсе вызывается AddRef(), при передаче по ссылке - нет: 
17.	procedure MyFunc1(ptr :ISomeInterface);  //Вызвает AddRef() на входе в функцию,
 Release() на выходе
18.	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;

Продолжение следует...

0

14

Код:
Это поведение более очевидно, если вспомнить, что при возврате из функции структуры (или указателя на интерфейс), в функцию передается адрес, по которому нужно записать выходное значение. Самой переменной 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;

0

15

Код:
Пример 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;

0

16

Код:
При этом явное объявление переменной-указателя на интерфейс в данном случае является необходимым. Что неправильно в следующем примере? 
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, которую постарается уничтожить при выходе из процедуры (то есть после удаления объекта) - снимаю перед Вами шляпу, дальше можно не читать.

0

17

Managed C++

0

18

Рисунок 5. Callable COM Wrapper (CCW).

0

19

0

20

Рисунок 6. Runtime Callable Wrapper (RCW).

0

21

Код:
Для того, чтобы код мог правильно получить указатель на такой объект, необходимо, чтобы реализация метода 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() 
{
…
}

0

22

C#

0

23

Код:
public static class iCanvas
 {
  public static uint ID = 0x83893202;
  public static uint VERSION = 0x00010000;
 };
Те же правила действуют и при получении CCW/RCW. 
Кроме этого, нужно знать еще несколько правил: 
1.	В .net используется сборщик мусора. Он может перемещать объекты в памяти. Поэтому в native код можно передавать только те указатели, которые указывают на объекты, специально созданные в неперемещаемой памяти: 
2.	private IntPtr name;
3.	
4.	name = System.Runtime.InteropServices.Marshal.StringToHGlobalAnsi("Diamond, implemented
 in C# plugin");
5.	
6.	public void GetDesc(out IntPtr desc)
7.	{
8.	 desc = name;
}
9.	Сборщик мусора уничтожает объект только в момент сборки мусора. Это значит, что объект может "жить" еще очень долго после момента, когда счетчик ссылок достиг 0. Если объект использует какие-то ресурсы, то необходимо предусмотреть метод, который вызывает их освобождение, так как деструктор объекта не будет вызван в момент освобождения объекта по Release(). 
10.	При закрытии плагина необходимо вызвать методы:
GC.Collect();
GC.WaitForPendingFinalizers();

чтобы корректно отработали деструкторы всех объектов (поэтому проблема, которая упоминается к комментариям к статье, на самом деле не существует). 
В книге очень подробно описаны различные примеры взаимодействия managed и native кода.

0

24

Заключение
Описанную систему можно расширить и для других языков: VB, VB.net, J# и др. К сожалению, я слишком плохо знаю эти языки, чтобы написать пример.
Примерно такая же система используется в движке Vital Engine 3.0.

0


Вы здесь » Твой мир ПРОграммирования-Delphi, Pascal, C++ » Создание игр » Плагины на основе COM интерфейсов