public:
// initialize pointer count to zero
// сбросить счетчик указателя в нуль
FastString(const char *psz) : mcPtrs(0) { }
void DuplicatePointer(void)
{
// note duplication of pointer
// отметить дублирование указателя
++mcPtrs;
}
void DestroyPointer(void)
{
// destroy object when last pointer destroyed
// уничтожить объект, когда уничтожен последний указатель
if (-mcPtrs == 0) delete this;
}
: : :
};
Этот совершенно стандартный код мог бы просто быть включен в базовый класс или в макрос С-препроцессора, чтобы его могли использовать все реализации.
Чтобы поддерживать эти методы, все программы, которые манипулируют или управляют указателями интерфейса, должны придерживаться двух простых правил
IFastString* CreateFastString(const char *psz)
{
IFastString *pfsResult = new FastString(psz);
if (pfsResult) pfsResult->DuplicatePointer();
return pfsResult;
}
Реализация копирует указатель и в другом месте – в методе Dynamic_Cast:
void *FastString::Dynamic_Cast(const char *pszType)
{
void *pvResult = 0;
if (strcmp(pszType, «IFastString») == 0) pvResult = static_cast<IFastString*>(this);
else if (strcmp(pszType, «IPersistentObject») == 0) pvResult = static_cast<IPersistentObject*>(this);
else if (strcmp(pszType, «IExtensibleObject») == 0) pvResult = static_cast<IFastString*>(this);
else return 0;
// request for unsupported interface
// запрос на неподдерживаемый интерфейс
// pvResult now contains a duplicated pointer, so
// we must call DuplicatePointer prior to returning
// теперь pvResult содержит скопированный указатель,
// поэтому нужно перед возвратом вызвать DuplicatePointer
((IExtensibleObject*)pvResult)->DuplicatePo1nter();
return pvResult;
}
С этими двумя усовершенствованиями соответствующий код пользователя становится значительно более однородным и прозрачным:
void f(void)
{
IFastString *pfs = 0;
IPersistentObject *ppo = 0;
pfs = CreateFastString(«Feed BOB»);
if (pts) {
рро = (IPersistentObject *) pfs->DynamicCast(«IPersistentObject»);
if (ppo) { ppo->Save(«C:\\autoexec.bat»);
ppo->DestroyPointer(); }
pfs->DestroyPointer(); }
}
Поскольку каждый указатель теперь трактуется как автономный объект с точки зрения времени жизни, клиенту можно не интересоваться тем, какой указатель соответствует какому объекту. Вместо этого клиент просто придерживается двух простых правил и предоставляет объектам самим управлять своим временем жизни. При желании способ вызова
Использование этой схемы вычисления ссылок позволяет объекту весьма единообразно выставлять множественные интерфейсы. Возможность выставления нескольких интерфейсов из одного класса реализации позволяет типу данных участвовать в различных контекстах. Например, новая постоянная подсистема могла бы определить собственный интерфейс для управления автозагрузкой и автозаписью объектов на некоторый специализированный носитель. Класс
Где мы находимся?
Мы начали эту главу с простого класса C++ и рассмотрели проблемы, связанные с объявлением этого класса как двоичного компонента повторного использования. Первым шагом было употребление этого класса в качестве библиотеки
Короче, мы только что создали модель компонентных объектов (
Глава 2. Интерфейсы
void *pv = malloc(sizeof(int));
int *pi = (int*)pv;
(*pi)++;
free(pv);
Снова об интерфейсах и реализациях
Цель отделения интерфейса от реализации заключалась в сокрытии от клиента всех деталей внутренней работы объекта. Этот фундаментальный принцип предусматривал уровень косвенности, или изоляции (
Хотя этот последний аспект и полезен, он далеко не достаточен для обеспечения универсальной основы для двоичных компонентов. Важно отметить, что хотя клиенты могут использовать любой выбранный ими транслятор C++, в конечном счете это будет всего лишь транслятор C++. Приемы, описанные в предыдущей главе, обеспечивают независимость от транслятора. В конце концов, главное, что необходимо для создания действительно универсальной основы для двоичных компонентов, – это независимость от языка. А чтобы достичь независимости от языка, принцип отделения интерфейса от реализации должен быть применен еще раз.
Рассмотрим определения интерфейса, использованные в предыдущей главе. Каждое определение интерфейса принимало форму определения абстрактного базового класса C++ в заголовочном файле C++. Тот факт, что определение интерфейса постоянно находится в файле, читаемом только на одном языке, вскрывает один остаточный признак реализации этого объекта – язык, на котором он был написан. Но, в конечном счете, объект должен быть доступен для любого языка, а не только для того, который выбрал разработчик объекта. Предусматривая только совместимое с C++ определение интерфейса, разработчик объекта тем самым вынуждает всех использующих этот объект также работать на C++.
Хотя C++ – чрезвычайно полезный язык программирования, существует множество областей программирования, где больше подходят другие языки. Но точно так же, как проблемы совместимости при компоновке можно решить путем обеспечения всех существующих компиляторов файлами определения модуля, возможно и перевести определение интерфейса с C++ на любые другие языки программирования. А так как двоичная сигнатура интерфейса есть просто сочетание
Проделывание этих языковых преобразований данных для всех известных интерфейсов потребовало бы огромного количества работы, а главное – невозможно успевать делать это для бурного потока языков программирования, которые индустрия программного обеспечения не устает изобретать чуть ли не каждую декаду. Идеально было бы написать сервисную программу, которая переводила бы определения класса C++ в некую абстрактную промежуточную форму. Из этой промежуточной формы такая программа могла бы преобразовывать данные для любого языка программирования, имеющего соответствующий выходной генератор (back-end generator). По мере того как новые языки приобретают значимость, могли бы добавляться новые выходные генераторы, и все ранее определенные интерфейсы смогли бы тотчас использоваться в совершенно новом контексте.
К сожалению, язык программирования C++ полон неоднозначностей, что делает его малопригодным для преобразования данных на все мыслимые языки. Многие из этих неоднозначностей приводят к неопределенным соотношениям между указателями, памятью и массивами. Это не является проблемой, когда оба объекта: вызывающий (
IDL
СОМ IDL базируется на языке определения интерфейсов основного открытого математического обеспечения удаленного вызова процедур в распределенной вычислительной среде – Open Software Foundation Distributed Computing Environment Remote Procedure Call (OSF DCE RPC). DCE IDL позволяет описывать удаленные вызовы процедур не зависящим от языка способом. Это дает возможность компилятору IDL генерировать код для работы в сети, который прозрачным образом (transparently), то есть незаметно для пользователя, переносит описанные операции на всевозможные сетевые средства сообщения. СОМ IDL просто добавляет некоторые расширения, специфические для СОМ, в DCE IDL для поддержки объектно-ориентированных понятий СОМ (например, наследование, полиморфизм). Не случайно, что когда обращение к объектам СОМ осуществляется через границу контекста выполнения[1] или через границы между машинами, все связи клиент-объект используют
Win32 SDK включает в себя компилятор
Эти заголовочные файлы также содержат совместимые с С, основанные на структурах определения (structure-based definitions), которые позволяют С-программам обращаться к интерфейсам, описанным на IDL, или обеспечивать их выполнение. То, что MIDL автоматически генерирует С/С++-заголовочный файл, означает, что ни один из СОМ-интерфейсов не нужно определять на C++ вручную. Исход определений из одной точки исключает возникновение множества несовместимых версий определений интерфейсов, которые со временем могут вызвать асинхронность. MIDL также генерирует исходный код, который позволяет использовать интерфейсы в различных потоках, процессах и машинах. Этот код будет обсуждаться в главе 5. И наконец, MIDL может генерировать двоичный файл, который позволяет другим средам, принимающим СОМ, отображать интерфейсы, определенные в исходном IDL-файле, на другие языки. Этот двоичный файл называется библиотекой типа (
Чтобы понять IDL, необходимо рассмотреть логический и физический аспекты интерфейса. Обсуждение методов интерфейса и выполняемых ими операций относятся к логическому аспекту интерфейса. Обсуждение памяти, стекового фрейма, сетевых пакетов и других динамических явлений обычно относятся к физическому аспекту интерфейса. Некоторые физические аспекты интерфейса могут непосредственно наследовать логическому описанию (например, расположение таблицы
IDL позволяет разработчикам интерфейса работать непосредственно в сфере логики, используя синтаксис С. Но в то же время IDL требует от разработчиков точно определять все те аспекты интерфейса, которые не могут быть воспроизведены непосредственно по их логическому описанию на С, с помощью использования аннотаций, формально называемых атрибутами. Атрибуты IDL легко распознать в основном тексте IDL: разделенные запятыми, они заключены в скобки. Атрибуты всегда предшествуют описанию объекта, к которому они относятся. Например, в следующем IDL– фрагменте
[
v
]
enum COLOR { RED, GREEN, BLUE };
атрибут v1_enum относится к описанию перечисления (enumeration) COLOR. Этот атрибут информирует компилятор IDL о том, что представление COLOR при передаче значения через сеть должно иметь 32 бита, а не 16, как принято по умолчанию. Атрибут helpstring также относится к СОLОR и добавляет строку «This is a color!» («Это – цвет!») в создаваемую библиотеку типа как описание этого перечисления. Если игнорировать атрибуты в IDL-файле, то его синтаксис такой же, как в С. IDL поддерживает структуры, объединения, массивы, перечисления, а также определения типа (typedef) – с синтаксисом, идентичным их аналогам в С.
Определяя методы СОМ в IDL, необходимо четко указать, кто – вызывающий или вызываемый объект – будет записывать или читать каждый параметр метода. Это выполняется с помощью атрибутов параметра
void Method1([in] long arg1, [out] long *parg2, [in, out] long *parg3);
Для этого фрагмента IDL предполагается, что вызывающий объект передаст значение в объект
long arg2 = 20, arg3 = 30;