DxDev.narod.ru

Основы COM

В статье описано моё понимание COM изучив часть книги «Основы COM» Дейла Роджерсона. Если есть пожелания, предложения, критика в адрес статьи, то буду рад её прочесть f1ferrari(собака)mail.ru

Что такое COM?
В связи с ростом ПО появляются трудности с комфортной работой имея в наличии большого количества этого ПО на ПК. COM это технология создания ПО в виде объектов(компонентов), компактного их хранения в определённом виде и гарантирующих, что доступ к COM компонентам будет одинаков для всех пользователей (клиентов), и будет предельно прост не зависимо от количества и расположения этих объектов.

Каждый компонент содержит некое количество методов интерфейсов. Клиент получает их, пользуется и затем удаляет. Чтобы это всё одинаково выполнялось со всеми COM компонентами их интерфейсы должны быть унаследованы от главного COM интерфейса IUnknown – менеджер интерфейсов. Выглядит он примерно так:

interface IUnknown // interface тоже что struct 
{
  //запросить интерфейса по его идентификатору (IID) и преобразовать ppv к типу интерфейса, иначе вернуть ошибку.
  virtual HRESULT __stdcall QueryInterface(const IID &, void **ppv) = 0;
  //следит за количеством созданных объектов, увеличивает их счётчик.
  virtual ULONG __stdcall AddRef() = 0;
  //обратный метод предыдущего, но и ещё удаляет объект если счётчик = 0.
  virtual ULONG __stdcall Release() = 0;
};

Эти чисто виртуальные функции дают возможность разработчику реализовать унаследованные интерфейсы по своему.

Давайте создадим элементарный COM компонент.

// {43F948F7-BE5F-45d0-8C20-651F961E4B97} 
const IID IID_IX = { 0x43f948f7, 0xbe5f, 0x45d0, { 0x8c, 0x20, 0x65, 0x1f, 0x96, 0x1e, 0x4b, 0x97 } };
// {43F948F6-BE5F-45d0-8C20-651F961E4B97} 
const IID IID_IY = { 0x43f948f6, 0xbe5f, 0x45d0, { 0x8c, 0x20, 0x65, 0x1f, 0x96, 0x1e, 0x4b, 0x97 } };

// INTERFACES 

// define the IX interface
interface IX : IUnknown
{
  virtual void __stdcall fx(void) = 0;
}; 

// define the IY interface
interface IY : IUnknown
{
  virtual void __stdcall fy(void) = 0;
}; 

// CLASSES AND COMPONENTS 

// define the COM object
class CComponent : public IX, public IY
{
  virtual HRESULT __stdcall QueryInterface(const IID &iid, void **iface);
  virtual ULONG __stdcall AddRef();
  virtual ULONG __stdcall Release();

  virtual void __stdcall fx(void) {cout << "Function fx has been called." << endl; }
  virtual void __stdcall fy(void) {cout << "Function fy has been called." << endl; }

  int ref_count;

public:
  CComponent() : ref_count(0) {}
  ~CComponent() {}

};

// CLASS METHODS 

HRESULT __stdcall CComponent::QueryInterface(const IID &iid, void **iface)
{
  // requesting the IUnknown base interface
  if (iid==IID_IUnknown)
  {
    cout << "Requesting IUnknown interface" << endl;
    *iface = (IX*)this;	
  } // end if

  // maybe IX?
  if (iid==IID_IX)
  {
    cout << "Requesting IX interface" << endl;
    *iface = (IX*)this;
  } // end if
  else  // maybe IY
  if (iid==IID_IY)
  {
    cout << "Requesting IY interface" << endl;
    *iface = (IY*)this;
  } // end if
  else
  { // cant find it!
    cout << "Requesting unknown interaface!" << endl;
    *iface = NULL;
    return(E_NOINTERFACE);
  } // end else

  // if everything went well cast pointer to IUnknown and call addref()
  ((IUnknown *)(*iface))->AddRef();

  return(S_OK);

} // end QueryInterface

ULONG __stdcall CComponent::AddRef()
{
  // increments reference count
  cout << "Adding a reference" << endl;
  return(++ref_count);
} // end AddRef

ULONG __stdcall CComponent::Release()
{
  // decrements reference count
  cout << "Deleting a reference" << endl;
  if (--ref_count==0)
  {
    delete this;
    return(0);
  } // end if
  else
    return(ref_count);
} // end Release

IUnknown *CoCreateInstance(void)
{
  // Создадим COM объект
  IUnknown *comm_obj = (IX *)new(CComponent);

  cout << "Creating Comm object" << endl;

  // update reference count
  comm_obj->AddRef();

  return(comm_obj);

} // end CoCreateInstance

Создав СОМ компонент мы получили таблицу виртуальных функций, которая даёт доступ ко всем методам интерфейсов.
А так клиент может запрашивать интерфейсы компонента:
main(){
  // create the main COM object
  IUnknown *punknown = CoCreateInstance();

  // create two NULL pointers the the IX and IY interfaces
  IX *pix = NULL;
  IY *piy = NULL;

  // from the original COM object query for interface IX
  punknown->QueryInterface(IID_IX, (void **)&pix);

  // try some of the methods of IX
  pix->fx();

  pix->QueryInterface(IID_IY, (void **)&piy);

  // release the interface
  pix->Release();

  // try some of the methods
  piy->fy();

  // release the interface
  piy->Release();

  // release the COM object itself
  punknown->Release();
}

Предыдущий пример лишь продемонстрировал как клиент получает доступ к интерфейсам компонента. А если компонентов (с множеством интерфейсов) вагон и тележка, то удобней их запихнуть в DLL и оттуль запрашивать интерфейсы. Для этого существует хороший помощник который облегчает создание COM объекта - это фабрика класса. Фабрика класса (IClassFactory) это COM-интерфейс задачей которого является создание COM-компонентов. У каждого компонента есть своя фабрика (на каждого компонента с замком есть своя фабрика с ключом), она описана так:

interface IClassFactory : public IUnknown
{
  //создаёт компонент.
  virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,const IID& iid,void** ppv) = 0;
  // Захват DLL-ки пока используются компоненты. При этом увеличиваем глобальный счётчик
  virtual HRESULT __stdcall LockServer(BOOL bLock); = 0;
};

Чтобы понять как создаётся фабрика и ею компонент, рассмотрим как клиент получает компонент.

HRESULT CoCreateInstance(const CLSID& clsid,// ID компонента
			IUnknown* pUnknownOuter,// NULL
			DWORD dwClsContext,//где находится компонент (у нас там где и клиент)
			const IID& iid,//ID интерфейса (ведь в компоненте может быть не один интерфейс)
			void** ppv)//преобразованный к нужному типу интерфейс
{
  IClassFactory* pIFactory = NULL;

  HRESULT hr = CoGetClassObject(clsid,// ID компонента
				dwClsContext,
				NULL,
				IID_IClassFactory,//ID фабрики
				(void**)&pIFactory);

  if (SUCCEEDED(hr))// Успешно получив фабрику, создадим компонент
  {
    hr = pIFactory->CreateInstance(pUnknownOuter, iid, ppv);
    pIFactory->Release();
  }
  return hr;
}

CoGetClassObject просматривает по CLSID-у запрашиваемый клиентом компонент в реестре, если он найден загружается DLL, далее идёт вызов DllGetClassObject чтобы создать фабрику (по её собственному IID, не тот который в CoCreateInstance) и запрашивает у неё интерфейст IClassFactory. При успешном запросе имеем корректный указатель IClassFactory и создаём компонент точнее его интерфейс путём вызова фабричной функции CreateInstance. В случае успеха ppv будет содержать нужный интерфейс компонента.

Для регистрации и удаления компонентов из реестра используем 6 функций:
RegOpenKeyEx
RegCreateKeyEx
RegSetValueEx
RegEnumKeyEx
RegDeleteKey
RegCloseKey
Которые вызывают DllRegisterServer, DllUnregisterServer – для регистрации и удаления соответственно.
Клиент же вызывает для этого LoadLibrary и CoUninitialize.

Автоматизация

Описанный выше способ реализации компонента пригоден для клиентов имеющих доступ к интерфейсам через таблицу указателей на функции (vtbl), т.е. разработчикам C++, а если компонент использовать для разработчиков VBScript, то доступ к интерфейсу будет невозможен, т.к. язык сценариев не поддерживает vtbl. Для этого используется интерфейс IDispatch дающий возможность обращения к функциям по целочисленным идентификаторам т.е. неявно.

Вот его частичное описание
interface IDispatch : public IUnknown
{
  ...  
  virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
	/* [in] */ REFIID riid,
	/* [size_is][in] */ LPOLESTR __RPC_FAR *rgszNames,
	/* [in] */ UINT cNames,
	/* [in] */ LCID lcid,
	/* [size_is][out] */ DISPID __RPC_FAR *rgDispId) = 0;
        
  virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
	/* [in] */ DISPID dispIdMember,
	/* [in] */ REFIID riid,
	/* [in] */ LCID lcid,
	/* [in] */ WORD wFlags,
	/* [out][in] */ DISPPARAMS __RPC_FAR *pDispParams,
	/* [out] */ VARIANT __RPC_FAR *pVarResult,
	/* [out] */ EXCEPINFO __RPC_FAR *pExcepInfo,
	/* [out] */ UINT __RPC_FAR *puArgErr) = 0;        

};

GetIDsOfNames() в качестве 2-го параметра принимает имя функции и возвращает в последнем параметре идентификатор функции. Invoke() вызывает функцию поместив этот идетнификатор в массив указателей на функции (элементы которого очевидно целые числа).

Этот вызов методов компонента по его ID нужен для языков которые поддерживают свойства. Реализация свойства скрывает несколько функций которые позволяют этому свойству вести себя по разному в зависимости от нужды. Эти функции реализованы в компоненте на С++ (или на любом объектном языке) в виде Get/Set функций (GetDrink() SetDrink()) Клиент вызывает просто Drink и когда ему нужно узнать есть он или нет, и когда ему нужно его приобрести или убрать. А кто же явно вызывает Get | Set (GetDrink() или SetDrink() вот в чём вопрос)? Этот вопрос решает 4-й параметр Invoke() значения которого есть флаг нужной функции клиента. Например клиент узнаёт что будет деловая встреча, а для этой деловой встречи необходим и достаточен Drink, Клиент просто проверяет(на Visual Basic) есть он или нет.


//Псевдокод
// при таком вызове 4-ый параметр Invoke() принимает значение DISPATCH_PROPERTYGET
if (!Drink) 
{
  // а при таком вызове 4-ый параметр Invoke() принимает значение DISPATCH_PROPERTYPUT
  Drink = 3единицы 
}

Во время деловой встречи клиент проверяет закончился ли Drink?
while(проходит деловая встреча)
{
  if (!Drink) 
    Drink = 5единиц.
}

В этом заключаются чудеса автоматизации.

Исходник содержит реализацию двух компонентов с двумя интерфейсами. В интерфейсах реализована одна функция дающая о себе знать. Клиент загружает, выгружает DLL-ку (которая собрана компонентами) создаёт компоненты, запрашивает интерфейсы и юзает методы интерфейсов.

собранный прект проект MVC 6.0 здесь 41 kB.
Hosted by uCoz