Сущность технологии COM

       

Классы и серверы


СОМ-сервер — это двоичный файл, содержащий код метода для одного или более СОМ-классов. Сервер может быть упакован или в динамически подключаемую библиотеку (DLL), или в нормальный исполняемый файл. В любом случае за загрузку любого типа сервера автоматически отвечает диспетчер управления сервисами SCM.

Если в запросе на активацию объекта указана внутрипроцессная активация, то вариант сервера на базе DLL должен быть доступен для загрузки в адресном пространстве клиента. Если же в запросе на активацию указаны внепроцессная или внехостовая активация, то для запуска серверного процесса на указанной хост-машине (она может совпадать с машиной клиента) будет использован исполняемый файл. СОМ поддерживает также выполнение DLL-серверов в суррогатных процессах (surrogate processes) с целью разрешить использование внепроцессной и внехостовой активации существующих внутрипроцессных серверов. Подробности того, как суррогатные процессы связаны с внепроцессной и внехостовой активацией, будут изложены в главе 6.

Чтобы клиенты могли активировать объекты, не беспокоясь о том, как упакован сервер или где он инсталлирован, в СОМ предусмотрена конфигурационная база данных, отображающая CLSID на тот сервер, который реализует этот класс. При использовании версий Windows NT 5.0 или выше основным местом расположения этой конфигурационной базы данных является директория NT (NT Directory). Эта директория является рассредоточенной защищенной базой данных, в которой хранится служебная информация об учетных записях пользователей, хост-машинах и прочее. С тем же успехом в директории NT можно хранить информацию и о СОМ-классах. Эта информация записывается в области директории, называемой СОМ Class Store (хранилище СОМ-классов). СОМ использует Class Store для перевода CLSID в файлы реализации (в случае локальных запросов на активацию) или в удаленные хост-имена (в случае удаленных запросов на активацию). Если запрос на активацию для CLSID сделан на данной машине, то в первую очередь опрашивается локальный кэш.
Если в локальном кэше нет доступной конфигурационной информации, то СОМ посылает запрос в Class Store о том, чтобы реализация стала доступной из локальной машины. Это может просто означать добавление некоторой информации в локальный кэш, чтобы переадресовать запрос на другую хост-машину, или же это может привести к загрузке реализации класса на локальную машину и к запуску программы инсталляции. В любом случае, если класс зарегистрирован в Class Store, он доступен для запроса на активацию со стороны клиента в рамках ограничений безопасности.

Локальный кэш, упоминавшийся при обсуждении Class Store, официально называется системным реестром, или базой конфигурации системы (Registry). Реестр является иерархической базой данных, хранящейся в файлах на каждой машине, которую СОМ использует для преобразования CLSID в имена файлов (в случае локальной активации) или удаленные имена хостов (в случае удаленной активации). До Windows NT 5.0 реестр был единственным местом размещения конфигурационной информации СОМ. Быстрый поиск в реестре может быть осуществлен с помощью иерархических ключей (keys), имена которых представляют собой строки, разделенные обратными косыми чертами. Каждый ключ в реестре может иметь одно или несколько значений, которые могут иметь в своем составе строки, целые значения или двоичные данные. В реализации СОМ на Windows NT 4.0 большая часть ее конфигурационной информации записывается под именем

HKEY_LOCAL_MACHINE\Software\Classes

в то время как большинство программ используют более удобный псевдоним

HKEY_CLASSES_ROOT

Реализация СОМ на Windows NT 5.0 продолжает использовать HKEY_CLASSES_ROOT для установок в рамках всей машины, но также разрешает каждому пользователю назначить свою конфигурацию CLSID для обеспечения большей безопасности и гибкости. Под Windows NT 5.0 СОМ вначале опрашивает

HKEY_CURRENT_USER\Software\Classes

прежде чем опрашивать HKEY_CLASSES_ROOT. Для удобства записи часто используются аббревиатуры HKLM, HKCR и HKCU вместо HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT и HKEY_CURRENT_USER, соответственно.





СОМ хранит информацию, относящуюся к CLSID всех машин, под ключом реестра

HKCR\CLSID

В версии Windows NT 5.0 или выше СОМ ищет информацию о классах каждого пользователя под ключом

HKCU\Software\Classes\CLSID

Под одним из этих ключей будет сохранен список локальных CLSID, для каждого CLSID — свой подключ. Например, класс Gorilla, использовавшийся ранее в этой главе, мог бы иметь по всей машине запись по подключу:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}] @="Gorilla"

Для обеспечения локальной активации объектов Gorilla запись для CLSID Gorilla в реестре должна иметь подключ, показывающий, какой файл содержит исполняемый код для методов класса. Если сервер упакован как DLL, то требуется такая запись:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\InprocServer32] @="C:\ServerOfTheApes.dll"

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

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\LocalServer32] @="С:\ServerOfTheApes.exe"

Допустимо обеспечивать обе записи и позволить клиенту выбирать местоположение, исходя из требований времени задержки и устойчивости. Для поддержки функции ProgIDFromCLSID необходимо указать такой подключ:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\ProgID] @="Apes.Gorilla.1"

Наоборот, для поддержки функции CLSIDFromProgID требуются следующие величины:

[HKCR\Apes.Gorilla.1] @="Gorilla" [HKCR\Apes.Gorilla.1\CLSID] @="\{571F1680-CC83-11d0-8C48-0080C73925BA}

Программные идентификаторы (ProgID) не являются обязательными, но они рекомендуются, чтобы в тех средах, которые не могут просто копировать необработанные CLSID, тоже можно было осуществлять вызовы на активацию.

Все стандартно реализованные серверы СОМ поддерживают саморегистрацию. Для внутрипроцессного сервера это означает, что DLL должен экспортировать известные функции

STDAPI DllRegisterServer(void); STDAPI DllUnregisterServer(void);

Отметим, что STDAPI является просто макросом, индицирующим, что функция возвращает НRESULT и использует стандартное соглашение СОМ по вызову глобальных функций.


Эти подпрограммы должны быть явно экспортированы с использованием или файла определения модуля, или переключателей компоновщика, или директив компилятора. Эти подпрограммы используются хранилищем классов Class Store для конфигурирования локального кэша после загрузки файла на машину клиента. Кроме Class Store эти известные подпрограммы используются различными средами (например, Microsoft Transaction Server, ActiveX Code Download, а также различными инсталляционными программами) для инсталляции или деинсталляции серверов на хост-машинах. В Win32 SDK включена утилита REGSVR32.EXE, которая может инсталлировать или деинсталлировать внутрипроцессный сервер СОМ с использованием этих известных экспортированных функций.

Реализации внутрипроцессных серверов DllRegisterServer и DllUnregisterServer должны запросить реестр на добавление или удаление соответствующих ключей, преобразующих CLSID и ProgID сервера в файловые имена сервера. Хотя существуют различные способы реализации этих подпрограмм, наиболее гибким и эффективным из них является создание строковой таблицы, содержащей соответствующие ключи, названия величин, сами величины и простое перечисление всех записей в таблице, путем вызова RegSetValueEx для инсталляции и RegDeleteKey для деинсталляции. Чтобы осуществить регистрацию, основанную на этой технологии, сервер может просто задать массив строк размером Nx3, где каждый ряд массива содержит строки для использования в качестве ключей, имена величин и величины:

const char *g_RegTable[][3] = { // format is { key, value name, value } { "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}", 0, "Gorilla" }, { "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}\\InprocServer32",0, (const char*)-1 // rogue value indicating file name // нестандартное значение, указывающее имя файла },

{ "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}\\ProgID", 0, "Ареs.Gorilla.1" },

{ "Apes.Gorillа.1", 0, "Gorilla" }, { "Apes.Gorilla.1\\CLSID", 0, "{571F1680-CC83-11d0-8C48-0080C73925BA}" }, };



Имея эту таблицу, весьма несложно осуществить реализацию DllRegisterServer:

STDAPI DllRegisterServer(void) { HRESULT hr = S_OK; // look up server's file name // ищем имя файла сервера char szFileName[MAX_PATH]; GetModuleFileNameA(g_hinstDll, szFileName, MAX_PATH); // register entries from table // регистрируем записи из таблицы int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable); for (int i = 0; SUCCEEDED(hr) && i < nEntries; i++) { const char *pszKeyName = g_RegTable[i][0]; const char *pszValueName = g_RegTable[i][1]; const char *pszvalue = g_RegTable[i][2]; // map rogue value to module file name // переводим нестандарное значение в имя файла модуля if (pszValue == (const char*)-1) pszValue = szFileName; HKEY hkey; // create the key // создаем ключ long err = RegCreateKeyA(HKEY_CLASSES_ROOT, pszKeyName, &hkey); if (err == ERROR_SUCCESS) { // set the value // присваиваем значение err = RegSetValueExA(hkey, pszVvalueName, 0, REG_SZ, (const BYTE*) pszValue, (strlen(pszValue) + 1)); RegCloseKey(hkey); } if (err != ERROR_SUCCESS) { // if cannot add key or value, back out and fail // если невозможно добавить ключ или значение, то откат и сбой DllUnregisterServer(); hr = SELFREG_E_CLASS; } } return hr; }

Соответствующая DllUnregisterServer будет выглядеть так:

STDAPI DllUnregisterServer(void) { HRESULT hr = S_OK; int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable); for (int i = nEntries - 1; i >= 0; i--) { const char *pszKeyName = g_RegTable[i][0]; long err = RegDeleteKeyA(HKEY_CLASSES_ROOT, pszKeyName); if (err != ERROR_SUCCESS) hr = S_FALSE; } return hr; }

Отметим, что реализация DllUnregisterServer просматривает таблицу с конца, начиная с последней входной точки. Делается это для преодоления ограничения RegDeleteKey, в котором разрешаются только такие ключи, у которых нет подключей, подлежащих удалению. Реализация DllUnregisterServer требует такой организации таблицы, чтобы все подключи каждого ключа появлялись в таблице после входа родительского ключа.



Так как СОМ преобразует CLSID в данный файл реализации, то для объявления в СОМ относящихся к серверу объектов класса необходимо использовать определенные стандартные методики. Для сервера, основанного на исполняемой программе, в СОМ предусмотрены явные API-функции для связывания объектов класса с их CLSID. Эти API-функции мы будем подробно обсуждать в главе 6. Для сервера, основанного на DLL, DLL должна экспортировать известную функцию, которая будет вызываться с помощью CoGetClassObject, когда потребуется объект класса. Эту функцию необходимо экспортировать с использованием файла определения модулей, причем она должна иметь следующий вид:

HRESULT DllGetClassObject( [in] REFCLSID rclsid, // which class object? // какой объект класса? [in] REFIID riid, // which interface? // какой интерфейс? [out, iid_is(riid)] void **ppv); // put it here! // разместить его здесь!

Для удобства и эффективности данный сервер может содержать код для более чем одного класса. Первый параметр DllGetClassObject показывает, какой класс в данный момент запрашивается. Второй и третий параметры просто дают функции возможность возвращать типизированный указатель интерфейса для СОМ.

Рассмотрим сервер, реализующий три класса: Gorilla, Chimp и Orangutan. Сервер, возможно, будет содержать шесть отдельных классов C++: три из них создают экземпляры каждого класса, а другие три — объекты класса для каждого класса. В соответствии с этим сценарием, серверная реализация DllGetClassObject будет выглядеть следующим образом:

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv) { // define a singleton class object for each class // определяем одноэлементный объект класса // для каждого класса static GorillaClass s_gorillaClass; static OrangutanClass s_orangutanClass; static ChimpClass s_chimpClass; // return interface pointers to known classes // возвращаем указатели интерфейсов известных классов if (rclsid == CLSID_Gorilla) return s_gorillaClass.QueryInterface(riid, ppv); else if (rclsid == CLSID_Orangutan) return s_orangutanClass.QueryInterface(riid, ppv); else if (rclsid == CLSID_Chimp) return s_chimpClass.QueryInterface(riid, ppv); // if we get to here, rclsid is a class we don't implement, // so fail with well-known error code // если мы добрались сюда, то rclsid - это класс, который // мы не реализуем, поэтому сбой с известным кодом ошибки *ppv = 0; return CLASS_E_CLASSNOTAVAILABLE; }



Заметим, что приведенный код не заботится о том, какой интерфейс объявляется каждым из объектов класса. Он просто отправляет запрос QueryInterface соответствующему объекту класса.

Следующий псевдокод показывает, как API-функция CoGetClassObject устанавливает связь с серверным DllGetClassObject:

// pseudo-code from OLE32.DLL // псевдокод из OLE32.DLL

HRESULT CoGetClassObject(REFCLSID rclsid, DWORD dwClsCtx, COSERVERINFO *pcsi , REFIID riid, void **ppv) { HRESULT hr = REGDB_E_CLASSNOTREG; *ppv = 0; if (dwClsCtx & CLSCTX_INPROC) { // try to perform inproc activation // пытаемся выполнить внутрипроцессную активацию HRESULT (*pfnGCO)(REFCLSID, REFIID, void**) = 0; // look in table of already loaded servers in this process // просматриваем таблицу уже загруженных серверов внутри // этого процесса pfnGCO = LookupInClassTable(rclsid, dwClsCtx); if (pfnGCO == 0) { // not loaded yet! // еще не загружен! // ask class store or registry for DLL name // запрашиваем DLL-имя в хранилище классов или в реестре char szFileName[MAX_PATH]; hr = GetFileFromClassStoreOrRegistry(rclsid, dwClsCtx, szFileName); if (SUCCEEDED(hr)) { // try to load the DLL and scrape out DllGetClassObject // пытаемся загрузить DLL и вытащить DllGetClassObject HINSTANCE hInst = LoadLibrary(szFileName); if (hInst == 0) return CO_E_DLLNOTFOUND; pfnGCO = GetProcAddress(hInst, "DllGetClassObject"); if (pfnGCO == 0) return CO_E_ERRORINDLL; // cache DLL for later use // кэшируем DLL для дальнейшего использования InsertInClassTable(rclsid, dwClsCtx, hInst, pfnGCO); } } // call function to get pointer to class object // вызываем функцию для получения указателя на объект класса hr = (*pfnGCO)(rclsid, riid, ppv); } if ((dwClsCtx & (CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER)) && hr == REGDB_E_CLASSNOTREG) { // handle out-of-proc/remote request // обрабатываем внепроцессный/удаленный запрос } return hr; }

Отметим, что реализация CoGetClassObject является единственным местом, откуда вызывается DllGetClassObject.


Чтобы укрепить это обстоятельство, компоновщики в модели СОМ выдадут предупреждение в случае, если входная точка DllGetClassObject экспортируется без использования ключевого слова private в соответствующем файле определения модулей:

// from APELIB.DEF // из APELIB.DEF LIBRARY APELIB EXPORTS DllGetClassObject private

Фактически в модели СОМ компоновщики предпочитают, чтобы во всех связанных с СОМ точках входа использовалось это ключевое слово.

1

Эти аббревиатуры не допускаются в исходном коде или в конфигурационных файлах. Они просто дают возможность длинным именам ключей фигурировать в виде одной строки без разделителей в документации или других текстах о СОМ. Читателю следует раскрывать аббревиатуры при чтении вслух или при использовании в исходном коде.

2

Приведенный здесь способ записи использует стандартный синтаксис REGEDIT4. Строки, содержащиеся внутри скобок, соответствуют именам ключей. Пары имя=значение (name = value) под ключом обозначают значения, присвоенные указанному ключу. Необычное имя "@" показывает значение ключа по умолчанию.


Содержание раздела