Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Роджерсон Д. - Основы COM - 2000.pdf
Скачиваний:
412
Добавлен:
13.08.2013
Размер:
2.4 Mб
Скачать

68

Но CallCreateInstance слишком тесно привязывает клиент к реализации компонента. От клиента нельзя требовать знания имени DLL, в которой реализован компонент. Нам нужна и возможность перемещать компонент из одной DLL в другую или даже из одного каталога в другой.

Почему можно использовать DLL

Почему DLL можно использовать для размещения компонентов? Потому, что DLL используют адресное пространство приложения, с которым скомпонованы.

Как уже обсуждалось выше, клиент и компонент взаимодействуют через интерфейсы. Интерфейс — это по сути таблица указателей на функции. Компонент выделяет память для vtbl и инициализирует ее адресами всех функций. Чтобы использовать vtbl, клиент должен иметь доступ к выделенной для нее компонентом памяти. Клиент также должен «понимать» адреса, помещенные компонентом в vtbl. В Windows клиент может работать с vtbl, так как динамически компонуемая библиотека использует то же адресное пространство, что и он сам.

В Windows исполняющаяся программа называется процессом. Каждое приложение (EXE) исполняется в отдельном процесса, и у каждого процесса имеется свое адресное пространство в 4 Гбайт. Адрес в одном процессе отличен от того же адреса в другом процессе. Указатели не могут передаваться из одного приложения в другое, поскольку они находятся в разных адресных пространствах. Пусть у нас есть некий адрес, скажем, дом 369 по Персиковой аллее. Этот дом может оказаться как супермаркетом в Атланте, так и кофейней в Сиэтле. Если не указан город, адрес на самом деле не имеет смысла. В рамках этой аналогии процессы — это города. Указатели в двух процессах могут иметь одно и то же значение, но фактически они будут указывать на разные участки памяти.

К счастью, динамически компонуемая библиотека располагается в том же процессе, что и использующее ее приложение. Поскольку и DLL, и EXE используют один и тот же процесс, они используют одно и то же адресное пространство. По этой причине DLL часто называют серверами внутри процесса (in-proc server). В гл. 10 мы рассмотрим сервера вне процесса (out-of-proc), или локальные и удаленные серверы, которые реализуются как EXE-модули. Серверы вне процесса имеют адресные пространства, отличные от адресных пространств своих клиентов, но мы по-прежнему будем использовать DLL для поддержки связи такого сервера с его клиентом. На рис. 5-1 показано размещение DLL в адресном пространстве ее клиентского приложения.

Процесс 1

Память приложения

Память DLL 1

Память DLL 2

Свободно

4 Гбайт

Процесс 2

Память приложения

Память DLL 3

Память DLL 2

Память DLL 4

Свободно

4 Гбайт

Рис. 5-1 Динамически компонуемые библиотеки резмещаются в адресном пространстве процесса, содержащего приложение, с которым они скомпонованы

Важно отметить, что после того, как клиент получил у компонента указатель на интерфейс, все, что их связывает, — это двоичная «развертка» интерфейса. Когда клиент запрашивает у компонента интерфейс, он запрашивает участок памяти определенного формата. Возвращая указатель на интерфейс, компонент сообщает клиенту, где находится этот участок. Поскольку интерфейс располагается в памяти, доступной и клиенту, и компоненту, ситуация аналогична той, когда клиент и компонент расположены в одном и том же EXE файле. С точки зрения клиента, единственное различие динамической и статической компоновки состоит

вспособе, которым он получает указатель на интерфейс.

Вгл. 6 и 7 мы разъединим клиент и компонент, используя более общий и гибкий метод создания компонентов. В

гл. 7 функция CoCreateInstance библиотеки СОМ заменит CallCreateInstance, пока же CallCreateInstance нам будет достаточно.

Разбиваем монолит

Мой отец всегда подтрунивал надо мной, когда я говорил «большой гигант». Он спрашивал: «А ты уверен, что это был не маленький гигант?» Итак, специально для моего отца я разбиваю наш маленький монолит-пример на отдельные файлы. В этом разделе мы выясним, как можно разделить программу листинга 4-1 на несколько файлов, которые мы затем рассмотрим по отдельности. Файлы с этими примерами помещены в каталоге CHAP05 на прилагающемся диске. В этом каталоге достаточно файлов для реализации трех клиентов и трех компонентов. На рис. 5-2 показаны файлы, содержащие по одному клиенту и компоненту.

69

Файлы клиента

Общие файлы

Файлы компонента

CLIENT1.CPP

CREATE.H IFACE.H CMPNT1.CPP

CREATE.CPP GUIDS.CPP CMPNT1.DEF

Рис. 5-2 Файлы клиента и компонента

Теперь клиент находится в файле CLIENT1.CPP. Он включает файл CREATE.H и компонуется вместе с файлом CLIENT1.CPP. Эти два файла инкапсулируют создание компонента, находящегося в DLL. (Файл CREATE.CPP мы уже видели в листинге 5-2.) В гл. 7 два этих файла исчезнут, их заменят функции, предоставляемые библиотекой СОМ.

Компонент теперь размещается в файле CMPNT1.CPP. Для динамической компоновки требуется файл определения модуля, в котором перечисляются функции, экспортируемые из DLL. Это файл CMPNT1.DEF, приведенный в листинге 5-1.

Компонент и клиент используют два общих файла. Файл IFACE.H содержит объявления всех интерфейсов, поддерживаемых CMPNT1. Там же содержатся объявления для идентификаторов этих интерфейсов. Определения данных идентификаторов находятся в файле GUIDS.CPP (потерпите, о GUID мы поговорим в следующей главе).

Собрать клиент и компонент можно с помощью следующих команд:

cl Client.cpp Create.cpp GUIDS.cpp UUID.lib

cl /LD Cmpnt1.cpp GUIDS.cpp UUID.lib Cmpnt1.def

Однако, поскольку у нас три клиента и три компонента, я решил заранее создать для Вас make-файл. Правда, я хороший парень? И потом, я не просто создал make-файл, я попытался еще и сделать его читабельным. Я знаю, что это практически невыполнимая задача, но надеюсь, Вы без особых усилий разберетесь, что происходит в этом файле. Для того, чтобы собрать все компоненты и все клиенты, введите следующую командную строку:

nmake –f makefile

Это был краткий обзор файлов примеров. Имена и назначение файлов останутся практически теми же до конца книги (хотя их содержимое и подлежит настройке перед поставкой).

Тексты программ

Теперь давайте рассмотрим код, особенно клиента, поскольку по-настоящему новое и интересное находится именно там. Код, реализующий клиент, представлен в листинге 5-3. Клиент запрашивает у пользователя имя файла используемой DLL. Это имя он передает функции CallCreateInstance, которая загружает DLL и вызывает экспортированную из нее функцию CreateInstance.

CLIENT1.CPP

//

//Client1.cpp

//Комиляция: cl Client1.cpp Create.cpp GUIDs.cpp UUID.lib

//

#include <iostream.h> #include <objbase.h>

#include "Iface.h" #include "Create.h"

void trace(const char* msg) { cout << "Клиент 1:\t" << msg << endl; }

//

// Клиент1

//

int main()

{

HRESULT hr;

// Считать имя компонента

70

char name[40];

cout << "Введите имя файла компонента [Cmpnt?.dll]: "; cin >> name;

cout << endl;

// Создать компонент вызовом функции CreateInstance из DLL trace("Получить указатель на IUnknown");

IUnknown* pIUnknown = CallCreateInstance(name); if (pIUnknown == NULL)

{

trace("Вызов CallCreateInstance неудачен"); return 1;

}

trace("Получить интерфейс IX");

IX* pIX;

hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);

if (SUCCEEDED(hr))

{

trace("IX получен успешно");

pIX->Fx();

// Использовать интерфейс IX

pIX->Release();

 

}

else

{

trace("Не могу получить интерфейс IX");

}

trace("Освободить интерфейс IUnknown"); pIUnknown->Release();

return 0;

}

Листинг 5-3 Клиент запрашивает имя DLL, содержащей компонент. Он загружает DLL, создает компонент и работает с его интерфейсами.

В листинге 5-4 приведен код компонента. За исключением спецификации extern “C” для CreateInstance, он остался практически неизменным. Только теперь компонент находится в своем собственном файле — CMPNT1.CPP. CMPNT1.CPP компилируется с использование флажка /LD. Кроме того, он компонуется с CMPNT1.DEF, который мы уже видели в листинге 5-1.

CMPNT1.CPP

//

//Cmpnt1.cpp

//Компиляция: cl /LD Cmpnt1.cpp GUIDs.cpp UUID.lib Cmpnt1.def

//

#include <iostream.h> #include <objbase.h>

#include "Iface.h"

void trace(const char* msg) { cout << "Компонент 1:\t" << msg << endl; }

//

// Компонент

//

class CA : public IX

{

// Реализация IUnknown

virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv); virtual ULONG __stdcall AddRef();

virtual ULONG __stdcall Release();

// Реализация интерфейса IX

virtual void __stdcall Fx() { cout << "Fx" << endl; }

public:

71

// Конструктор

CA() : m_cRef(0) {}

// Деструктор

~CA() { trace("Ликвидировать себя"); }

private:

long m_cRef; };

HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)

{

if (iid == IID_IUnknown)

{

trace("Возвратить указатель на IUnknown"); *ppv = static_cast<IX*>(this);

}

else if (iid == IID_IX)

{

trace("Возвратить указатель на IX"); *ppv = static_cast<IX*>(this);

}

else

{

trace("Интерфейс не поддерживается");

*ppv = NULL;

return E_NOINTERFACE;

}

reinterpret_cast<IUnknown*>(*ppv)->AddRef(); return S_OK;

}

ULONG __stdcall CA::AddRef()

{

return InterlockedIncrement(&m_cRef);

}

ULONG __stdcall CA::Release()

{

if (InterlockedDecrement(&m_cRef) == 0)

{

delete this; return 0;

}

return m_cRef;

}

//

// Функция создания

//

extern "C" IUnknown* CreateInstance()

{

IUnknown* pI = static_cast<IX*>(new CA); pI->AddRef();

return pI;

}

Листинг 5-4 Компонент, расположенный теперь в своем файле, практически не изменился по сравнению с гл. 4.

Теперь нам осталось взглянуть лишь на два общих файла — IFACE.H и GUIDS.CPP. В файле IFACE.H объявлены все интерфейсы, используемые клиентом и компонентом.

IFACE.H

//

//Iface.h

//Интерфейсы

interface IX : IUnknown

Соседние файлы в предмете Программирование на C++