Глава 5
Динамическая компоновка

Что же это получается? Еще в первой главе я говорил, как важна динамическая компоновка для построения системы из "кирпичиков". И вот мы добрались уже до пятой главы - и не только попрежнему компонуем клиента с компонентом статически, но и располагаем их все время в одном и том же файле! На самом деле у меня были основательные причины отложить обсуждение динамической компоновки. Главная из них в том, что пока мы не реализовали полностью IUnknown, клиент был слишком сильно связан с компонентом.

Сначала компонент нельзя было изменить так, чтобы не потребовалось изменять и клиент. Затем при помощи QueryInterface мы перешли на следующий уровень абстракции и представили компонент как набор независимых интерфейсов. Раздробив компонент на интерфейсы, мы сделали первый шаг к тому, чтобы раздробить монолитное приложение. Затем нам понадобился способ управления временем жизни компонента. Подсчитывая ссылки на каждый интерфейс, клиент управляет их временем жизни; компонент же сам определяет, когда ему себя выгрузить. Теперь, когда мы реализовали IUnknown, клиент и компонент связаны не титановой цепью, а тонкой ниткой. Столь непрочная связь уже не мешает компоненту и клиенту изменяться, не задевая друг друга.

В этой главе мы попробуем поместить компонент в DLL. Обратите внимание - я не сказал, что мы собираемся сделать компонент DLL. Компонент - это не DLL, думать так значило бы слишком ограничивать концепцию компонента. DLL для компонента - сервер, или средство доставки. Компонент - это набор интерфейсов, которые реализованы в DLL. DLL - это грузовик, а компонент - груз.

Чтобы познакомиться с динамической компоновкой, мы посмотрим, как клиент создает компонент, содержащийся в DLL. Затем мы возьмем листинг 4-1 из гл. 4 и разобьем его на отдельные файлы для клиента и компонента. Разобрав полученный код, мы создадим три разных клиента и три разных компонента, использующие разные комбинации трех интерфейсов. Для чего все это? В качестве грандиозного финала мы сконструируем компанию клиентов и компонентов, где каждый сможет общаться с каждым.

Если Вы уже знакомы с DLL, то большая часть содержания этой главы Вам известна. Однако Вы можете полюбопытствовать, как я "растащу" клиент и компонент по разным файлам (раздел "Разбиваем монолит"). Надеюсь, Вам понравится сочетание разных клиентов и компонентов в разделе "Связки объектов" в конце главы.

Создание компонента

В этом разделе мы увидим, как компонент динамически компонуется с клиентом. Мы начнем с клиента, создающего компонент. Это временная мера; в последующих главах мы увидим, как изолировать клиент от компонента еще сильнее.

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

Экспорт функции из DLL

Экспорт функции из DLL осуществляется без проблем. Сначала необходимо обеспечить использование компоновки С (С linkage), пометив функцию как extern "С". Например, функция CreateInstance в файле CMPNT1.CPP выглядит так:.

//
// Функция создания
//
extern "С" IUnknown* CreateInstance()
{
	IUnknown* pI = (IUnknown*)(void*)new CA;
	pI->AddRef();
	return pI;
}

Слово extern "С" в описании нужно, чтобы компилятор C++ не "довешивал" к имени функции информацию о типе. Без extern "C" Microsoft Visual C++ 5.0 превратит CreateInstance в

?CreateInstance@@YAPAUIUnknown@@XZ

Другие компиляторы используют иные схемы дополнения имени инфорнацией о типе. На дополненные имена нет стандарта, так что они не переносимы. Кроме того, работать с ними - изрядная морока.
Дамп экспортов
Если Вы пользуетесь Microsoft Visual C++, то при помощи DUMPBIN.ЕХЕ можете получить листинг символов, экспортированных из DLL.
Следующая команда
dumpbin -exports Cmpnt1.dll

генерирует для CMNPT1.DLL такие результаты:
Microsoft (R) COFF Binary File Dumper Version 4.20.6281
Copyrignt (C) Microsoft Corp. 1992-1996. All rights reserved.

Dump of file Cmpnt1.dll

File Type: DLL

	Section contains the following Exports for Cmpnt1.dll

		0 characteristics
		325556С5 time date stamp Fri Oct 04 11:26:13 2001
		0.00 version
		1 ordinal base
		1 number of functions
		1 number of names

	ordinal hint name

		1    0   CreateInstance   (00001028)

	Summary
		7000 .data
		1000 .idata
		3000 .rdata
		2000 .reloc
		10000 .text

Конечно, чтобы экспортировать функцию, недостаточно пометить ее как extern "С". Необходимо еще сообщить компоновщику, что функция экспортируется. Для этого надо создать надоедливый файл DEF. Файлы DEF так надоедливы потому, что очень легко позабыть внести в файл имя функции; если же Вы об этом забыли, компоновка к этой функции будет невозможна. Из-за такой забывчивости я лишился изрядной части волос.

Создавать файлы DEF очень легко. Вы можете скопировать их из примеров и изменить несколько строк. DEF файл для CMPNT1.DLL показан в листинге 5-1.

CMPNTJ.DEF


;
;Файл определения модуля для Cmpnt1
; LIBRARY Cmpnt1.dll DESCRIPTION '(c)1996-1997 Dale E. Rogerson' EXPORTS CreateInstance @1 PRIVATE

ЛИСТИНГ 5-1. В файле определения модуля перечислены функции, экспортированные динамически компонуемой библиотекой.

Все, что Вам нужно сделать, - перечислить экспортируемые функции в разделе EXPORTS данного файла. При желании можно назначить каждой функции порядковый номер (ordinal number). В строке LIBRAR Y следует указать фактическое имя DLL.

Таковы основы экспорта функций из DLL. Теперь мы посмотрим, как загрузить DLL и обратиться к функции.

Загрузка DLL

Файлы CREATE.H и CREATE.CPP реализуют функцию CallCreateInstance.

CallCreateInstance принимает имя DLL в качестве параметра, загружает DLL и пытается вызвать экспортированную функцию с именем СreateInstance. Соответствующий код показан в листинге 5-2.

CREATE.CPP
//
// Create.cpp
//

#include 
#include     // Объявление IUnknown.
#include "Create.h"

typedef IUnknown* (*CREATEFUNCPTR)();

IUnknown* CallCreateInstance(char* name)

{

	// Загрузить в процесс динамическую библиотеку.
	HINSTANCE hComponent = ::LoadLibrary(name);
	if (hComponent == NULL)
	{
	  cout << "CallCreateInstance:\tOшибка: Не могу загрузить компонент." 
	       << endl;
	  return NULL;
	}

	// Получить адрес функции CreateInstance.
	CREATEFUNCPTR CreateInstance
		= (CREATEFUNCPTR)::GetProcAddress(hComponent, 
		"CreateInstance");
	if (CreateInstance == NULL)
	{
		cout 
			<< "СаllСrеаtеIпstапсе:\tOшибка: 
			<< "He могу найти функцию CreateInstance." 
			<< endl;
		return NULL;
	}
	return CreateInstance();
}

Для загрузки DLL CallCreateInstance вызывает функцию Win32 LoadLibrary:

HINSTANCE LoadLibrary(
	LPCTSTR lpLibFileName // имя файла DLL
);

LoadLibrary принимает в качестве параметра имя файла DLL и возвращает описатель загруженной DLL. Функция Win32 GetProcAddress принимает этот описатель и имя функции (CreateInstance), возвращая адрес последней:

FARPROC GetProcAddress(
	HMODULE hModule,   // описатель модуля DLL
	LPCSTR lpProcName // имя функции
);
С помощью двух этих функций клиент может загрузить DLL в свое адресное пространство и получить адрес CreateInstance. Имея этот адрес, создать компонент и получить указатель на его IUnknown не составляет труда. CallCreateInstance приводит возвращенный указатель к типу, пригодному для использования, и, в соответствии со своим названием, вызывает CreateInstance.

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

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

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

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

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

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

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

В гл. 6 и 7 мы разъединим клиент и компонент, используя более общий и гибкий метод создания компонентов. В гл. 7 функция CoCreateInstance библиотеки СОМ заменит CallCreateInstance; пока же CallCreateInstance нам будет достаточно.

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

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

РИС. 5-2. файлы клиента и компонента.

Теперь клиент находится в файле CLIENT1.CPP. Он включает файл CREATE.H и компонуется вместе с файлом CREATE.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 Cmpntl.def

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

nmake -f makefile

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

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

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

CLIENT1.CPP
// Компиляция : cl Client1.cpp Create.cpp GUIDs.cpp UUID.lib
//
#include 
#include 
#include "Iface.h"
#include "Create.h"

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

int main()
{
	HRESULT hr;
	// Считать имя компонента.
	char name[40];
	cout << "Введите имя файла компонента [Cmpnt?.dll]: ";
	cin >> name;
	cout << endl;
	// Создать компонент вызовом функции CreateInstance из DLL
	trace("Получить указатель на IUnknown.");
	IUnknown* pIUnknown = CallCreateInstance(name);
	if (pIUnknown == NULL)
	{
		trасе("Вызов CallCreateInstance неудачен.");
		return 1;
	}
	trасе("Получить интерфейс IX.");
	IX* pIX;
	hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
	if (SUCCEEDED(hr))
	{
		trace("IX получен успешно.");
		pIX->Fx();		// Использовать интерфейс IX.
		pIX->Release();
	}
	else
	{
		trace("He могу получить интерфейс IX.");
	}
	trace("Освободить интерфейс IUnknown.");
	pIUnknown->Release();
	return 0;
}

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

CMPNH.CPP
//Cmpnt1.cpp
// Чтобы скомпилировать: cl /LD Cmpnt1.cpp GUIDs.cpp UUID.lib
Cmpnt1.def
//
#include 
#include 
#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:
		// Конструктор
		СА() : m_cRef(0) {}
		// Деструктор
		~СА() { trасе("Ликвидировать себя.") ;}
	private:
		long m_cRef;
};

HRESULT __stdcall "CA::QueryInterface(const IID& iid. void** ppv)
{
	if (iid == IID_IUnknown)
	{
		trace("Возвратить указатель на IUnknown.");
		*ppv = static_cast(this);
	}
	else if (iid == IID_IX)
	{
		trасе("Возвратить указатель на IX.");
		*ppv = static_cast(this);
	}
	else
	{
		trace("Интерфейс не поддерживается.");
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	reinterpret_cast(*ppv)->AddRef();
	return S_OK;
}
ULONG __stdcall CA::AddRef()
{
	return InterlockedIncretnent(&m_cRef);
}
ULONG __stdcall CA::Release()
{
	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this;
		return 0;
	}
	return m_cRef;

}
//
// Функция создания
//
extern "С" IUnknown* CreateInstance()
{
	IUnknown* pI = static_cast(new CA);
	pI->AddRef();
	return pI;
}

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

IFACE.H
//
// Интерфейсы
interface IX : IUnknown
{
	virtual void __stdcall Fx() = 0;
};
interface IY : IUnknown
{
	virtual void __stdcall Fy() = 0;
};
interface IZ : IUnknown
{
	virtual void __stdcall Fz() =0;
};
// Предварительные объявления GUID
extern "С"
{
	extern const 110 IID_IX;
	extern const IID IID_IY;
	extern const IID IID_IZ;
}

Как видите, клиент и компонент по-прежнему используют интерфейсы IX IY и IZ. Идентификаторы этих интерфейсов объявлены в конце IFACE.H IID будут обсуждаться в следующей главе. Определения идентификаторов интерфейсов находятся в файле GUIDS.CPP, который показан в листинге 5-6.

GUIDS.CPP
//GUIDs.cpp - Идентификаторы интерфейсов
//
#include 
extern "С"
{
	// {32bb8320-b41b-1cf-a6bb-0080c7b2d682}
	extern const IID IID_IX =
		{0x32bb8320, 0xb41b, 0x11cf,
		{0xa6, 0xbb, 0х0, 0х80, 0xc7, 0xb2, 0xd6, 0х82}};
	// {32bb8321-b41b-11cf-a6bb-0080c7b2d682}
	extern const IID IID_IY =
		{0x32bb8321, 0xb41b, 0x11cf,
		{0xa6, 0xbb, 0х0, 0х80, 0xc7, 0xb2, 0xd6, 0х82}};

	// {32bb8322-b41b-11cf-a6bb-0080c7b2d682}
	extern const IID IID_IZ =
		{0x32bb8322, 0xb41b, 0x11cf,
		{0xa6, 0xbb, 0х0, 0х80, 0xc7, 0xb2, 0xd6, 0х82}};

	//extern необходим, чтобы для констант C++ была выделена память.
}

Это были детали реализации компонента в DLL. Давайте немного поиграем с такими компонентами.

Связки объектов

Теперь Вы можете поиграть с компонентами и посмотреть, как они динамически компонуются. В каталоге СНАР05 содержится код трех клиентов; это Клиент 1, Клиент 2 и Клиент 3. Здесь же находится код трех компонентов, которые мы обозначим как Компонент 1, Компонент 2 и Компонент 3. Код в IFACE.H определяет три интерфейса: IX, ГУтл IZ. Клиент 1 и Компонент 1 поддерживают интерфейс IX. Клиент 2 и Компонент 2 поддерживают интерфейсы IX vs. ГУ. Клиент 3 и Компонент 3 поддерживают все три интерфейса. В табл. 5-1 показан набор интерфейсов, поддерживаемых каждым клиентом и компонентом.

Таблица 5-1. Эта таблица показывает, какие интерфейсы поддерживаются каждым клиентом и компонентом


IXIYIZ 
Клиент 1*  Компонент 1
Клиент 2** Компонент 2
Клиент 3***Компонент 3

Все клиенты и компоненты компилируются по команде

nmake -f makefile

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

Я хотел просто посмотреть, не заснули ли Вы. Клиент не сломается, а компонент напечатает симпатичное маленькое сообщение о том, что не поддерживает интерфейс. Ниже приведен пример работы Клиента 2 с Компонентом 2 и Клиента 3 с Компонентом 1.

C:\>client2
Введите имя файла компонента [Cmpnt?.dll]: cmpnt2.dll
Клиент 2:    Получить указатель на IDnknown.
Клиент 2:    Получить интерфейс IX.
Компонент 2: Возвратить указатель на IX.
Клиент 2:    IX получен успешно.
Fx
Клиент 2:    Запросить интерфейс IY.
Компонент 2: Возвратить указатель на IY.
Клиент 2:    IY получен успешно.
Fy
Клиент 2:    Освободить интерфейс IUnknown.
Компонент 2: Ликвидировать себя.

C:.\>client3
Введите имя файла компонента [Cmpnt?.dll]: cmpntl.dll
Клиент 3:    Получить указатель на IUnknown.
Клиент 3:    Получить интерфейс IX.
Компонент 1: Возвратить указатель на IX.
Клиент 3:    IX получен успешно.
Fx
Клиент 3:    Запросить интерфейс IY.
Компонент 1: Интерфейс не поддерживается.
Клиент 3:    Не могу получить интерфейс IY.
Клиент 3:    Запросить интерфейс IZ.
Компонент 1: Интерфейс не поддерживается.
Клиент 3:    Не могу получить интерфейс IZ.
Клиент 3:    Освободить интерфейс IUnknown.
Компонент 1: Ликвидировать себя.

Компонент 2 реализует все интерфейсы, нужные Клиенту 2. Компонент 1 реализует только IX, тогда как Клиенту 3 нужны все три интерфейса: IX, IY и IZ. Попробуйте другие комбинации компонентов и клиентов.

Вам понравилось? По-моему, это замечательно. Мы успешно создали архитектуру, которая позволяет подключать к друг другу компоненты и клиенты во время выполнения. Подозреваю, что Вы хотите пойти это отметить, так что я закругляюсь.

Негибкое создание, резюме

В этой главе мы добавили нашим компонентам одно свойство - динамическую компоновку. Поместив компоненты в DLL, мы можем заменять их во время выполнения. Как Вы видели из примеров, один клиент может легко работать с разными компонентами без перекомпоновки или перекомпиляции. Динамическая компоновка в сочетании с хорошо спроектированными интерфейсами может обеспечить создание невероятно гибких приложений, которые будут эволюционировать с течением времени.

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

[Назад] [Содержание] [Вперед]


Смотрите также:



Hosted by uCoz