Сущность технологии СОМ. Библиотека программиста - Дональд Бокс
Шрифт:
Интервал:
Закладка:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dw);
эквивалентен следующему:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER | CLSCTX_INPROC, REGCLS_MULTI_SEPARATE, &dw);
В любом случае, если бы из процесса вызывающего объекта был осуществлен такой вызов:
hr = CoGetClassObject(CLSID_Me, CLSCTX_INPROC, 0, IID_IUnknown, (void**)&pUnkCO);
то никакая DLL не была бы загружена. Вместо этого COM удовлетворила бы запрос, используя объект класса, зарегистрированный посредством CoRegisterClassObject. Если, однако, серверный процесс вызвал CoRegisterClassObject таким образом:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE, &dw);
то любые внутрипроцессные запросы на активацию CLSID_Me, исходящие изнутри серверного процесса, заставят DLL загрузиться.
CoRegisterClassObject связывает зарегистрированный объект класса с апартаментом вызывающего объекта. Это означает, что все поступающие запросы методов будут выполняться в апартаменте вызывающей программы. В частности, это означает, что если объект класса экспортирует интерфейс IClassFactory, то метод CreateInstance будет выполняться в апартаменте вызывающей программы. Результаты метода CreateInstance будут маршалированы из апартамента объекта класса, а это, в свою очередь, означает, что экземпляры класса будут принадлежать к тому же апартаменту, что и объект класса[3].
Серверные процессы могут регистрировать объекты класса для более чем одного класса. Если объекты класса зарегистрированы для выполнения в МТА процесса, то это означает, что поступающие запросы на активацию могут быть обслужены, как только будет завершен первый вызов CoRegisterClassObject. Во многих серверных процессах, основанных на МТА, это может вызвать проблемы, так как бывает, что процесс должен выполнить дальнейшую инициализацию. Чтобы избежать этой проблемы, в реализации COM под Windows NT 4.0 введен флаг REGCLS_SUSPENDED. При добавлении этого флага в вызов CoRegisterСlassObject библиотека COM не извещает SCM о том, что класс доступен. Это предотвращает поступление в серверный процесс входящих активационных запросов. Библиотека COM связывает CLSID с объектом класса; однако она помечает этот элемент в таблице библиотеки класса как отложенный. Для снятия этой пометки в COM предусмотрена API-функция CoResumeClassObjects:
HRESULT CoResumeClassObjects(void);
CoResumeClassObjects делает следующее. Во-первых, она помечает все отложенные объекты класса как легальные для использования. Во-вторых, она посылает SCM единственное сообщение, информируя его, что все ранее отложенные объекты класса теперь являются доступными в серверном процессе. Это сообщение обладает огромной силой, так как при его получении обновляется таблица класса SCM по всей машине и сразу для всех классов, зарегистрированных вызывающим объектом.
Получив три только что описанные API-функции, легко создать серверный процесс, экспортирующий один или более классов. Ниже приводится простая программа, которая экспортирует три класса из МТА сервера:
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
// define a singleton class object for each class
// определяем синглетон для каждого класса
static GorillaClass s_gorillaClass; static OrangutanClass s_orangutanClass;
static ChimpClass s_chimpClass; DWORD rgdwReg[3];
const DWORD dwRegCls = REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED;
const DWORD dwClsCtx = CLSCTX_LOCAL_SERVER;
// enter the MTA
// входим в МТА
HRESULT hr = GoInitializeEx(0, COINIT_MULTITHREADED);
assert(SUCCEEDED(hr));
// register class objects with СОM library's class table
// регистрируем объекты класса с помощью
// таблицы класса библиотеки COM
hr = CoRegisterClassObject(CLSID_Gorilla, &s_gorillaClass, dwClsCtx, dwRegCls, rgdwReg);
assert(SUCCEEDED(hr));
hr = CoRegisterClassObject(CLSID_Orangutan, &s_orangutanClass, dwClsCtx, dwRegCls, rgdwReg + 1);
assert(SUCCEEDED(hr)) ;
hr = CoRegisterClassObject(CLSID_Chimp, &s_chimpClass, dwClsCtx, dwRegCls, rgdwReg + 2);
assert(SUCCEEDED(hr));
// notify the SCM
// извещаем SCM
hr = CoResumeClassObjects();
assert(SUCCEEDED(hr));
// keep process alive until event is signaled
// сохраняем процессу жизнь, пока событие не наступило
extern HANDLE g_heventShutdown; WaitForSingleObject(g_heventShutdown, INFINITE);
// remove entries from COM library's class table
// удаляем элементы из таблицы класса библиотеки COM
for (int n = 0; n < 3; n++)
CoRevokeClassObject(rgdwReg[n]);
// leave the MTA
// покидаем MTA CoUninitialize();
return 0;
}
В данном фрагменте кода предполагается, что событие (Win32 Event object) будет инициализировано где-нибудь еще внутри процесса таким образом:
HANDLE g_heventShutdown = CreateEvent(0, TRUE, FALSE, 0);
Имея данное событие, сервер может быть мирно остановлен с помощью вызова API-функции SetEvent:
SetEvent(g_heventShutdown);
которая запустит последовательность выключения в главном потоке. Если бы сервер был реализован как сервер на основе STA, то главный поток должен был бы вместо ожидания события Win32 Event запустить конвейер обработки оконных сообщений (windows message pump). Это необходимо для того, чтобы позволить поступающим ORPC-запросам входить в апартамент главного потока.
Снова о времени жизни сервера
В примере, показанном в предыдущем разделе, не было точно показано, как и когда должен прекратить работу серверный процесс. В общем случае серверный процесс сам контролирует свое время жизни и может прекратить работу в любой выбранный им момент. Хотя для серверного процесса и допустимо неограниченное время работы, большинство из них предпочитают выключаться, когда не осталось неосвобожденных ссылок на их объекты или объекты класса. Это аналогично стратегии, используемой большинством внутрипроцессных серверов в их реализации DllCanUnloadNow. Напомним, что в главе 3 говорилось, что обычно сервер реализует две подпрограммы, вызываемые в качестве интерфейсных указателей, которые запрашиваются и освобождаются внешними клиентами:
// reasons to remain loaded
// причины оставаться загруженными
LONG g_cLocks = 0;
// called from AddRef + IClassFactory::LockServer(TRUE)
// вызвано из AddRef + IClassFactory::LockServer(TRUE)
void LockModule(void) {
InterlockedIncrement(&g_cLocks);
}
// called from Release + IClassFactory::LockServer(FALSE)
// вызвано из Release + IClassFactory::LockServer(FALSE)
void UnlockModule(void) {
InterlockedDecrement(&g_cLocks);
}
Это сделало реализацию DllCanUnloadNow предельно простой:
STDAPI DllCanUnloadNow() { return g_cLocks ? S_FALSE : S_OK; }
Подпрограмму DllCanUnloadNow нужно вызывать в случаях, когда клиент решил «собрать мусор» в своем адресном пространстве путем вызова CoFreeUnusedLibraries для освобождения неиспользуемых библиотек.
Имеются некоторые различия в том, как ЕХЕ-серверы прекращают работу серверов. Во-первых, обязанностью серверного процесса является упреждающее инициирование процесса своего выключения. В отличие от внутрипроцессных серверов, здесь не существует «сборщика мусора», который запросил бы внепроцессный сервер, желает ли он прекратить работу. Вместо этого серверный процесс должен в подходящий момент явно запустить процесс своего выключения. Если для выключения сервера используется событие Win32 Event, то процесс должен вызвать API-функцию SetEvent:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) ==0)
{
extern HANDLE g_heventShutdown;
SetEvent(g_heventShutdown);
}
}
Если вместо серверного основного потока обслуживается очередь событий Windows MSG , то для прерывания цикла обработки сообщений следует использовать некоторые из API-функций. Проще всего использовать PostThreadMessage для передачи в основной поток сообщения WM_QUIT:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) == 0) {
extern DWORD g_dwMainThreadID;
// set from main thread
// установлено из основного потока
PostThreadMessage(g_dwMainThreadID, WNLQUIT, 0, 0);
}
}
Если серверный процесс на основе STA знает, что он никогда не будет создавать дополнительные потоки, то он может использовать несколько более простую API-функцию PostQuitMessage:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) == 0) PostQuitMessage(0);
}
Этот способ работает только при вызове из главного потока серверного процесса.
Второе различие в управлении временем жизни внутрипроцессного и внепроцессного сервера связано с тем, что должно поддерживать сервер в загруженном или работающем состоянии. В случае внутрипроцессного сервера такой силой обладают неосвобожденные ссылки на объекты и неотмененные вызовы IClassFactory::LockServer(TRUE). Неосвобожденные ссылки на объекты необходимо рассмотреть в контексте внепроцессного сервера.
Безусловно, сервер должен оставаться доступным до тех пор, пока внешние клиенты имеют неосвобожденные ссылки на объекты класса сервера. Для внутрипроцессного сервера это реализуется следующим образом:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) {
LockModule();
// note outstanding reference
// отмечаем неосвобожденную ссылку
return 2;
// non-heap-based object
// объект, размещенный не в «куче»
}
STDMETHODIMP_(ULONG) MyClassObject::Release(void) {
UnlockModule();
// note destroyed reference
// отмечаем уничтоженную ссылку
return 1;
// non-heap-based object
// объект, размещенный не в «куче»
}
Такое поведение является обязательным, поскольку если DLL выгружается, несмотря на оставшиеся неосвобожденные ссылки на объекты класса, то даже последующие вызовы метода Release приведут клиентский процесс к гибели.