Общие замечания

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

Для оптимизации создания однотипных COM-объектов приложением-клиентом, в дополнение к каждому COM-классу может быть реализована так называемая фабрика класса (class factory; широко применяется также довольно неудачный термин class object). Фабрика класса представляет собой специальный COM-класс, реализующий интерфейс IClassFactory. Заметим, что фабрике класса никогда не назначается собственный GUID.

Интерфейс IClassFactory

Заголовочный файл #include <unknwn.h>
Непосредственный предок интерфейс IUnknown

Ниже перечислены методы интерфейса IClassFactory.

HRESULT CreateInstance(
   IUnknown* pUnk,
   REFIID iid,
   void** ppvDest
);

Через параметр pUnk в этот метод передается указатель на интерфейс IUnknown объекта-агрегата; этот указатель может быть равен NULL, если поддержка агрегации не была затребована приложением-клиентом. Через параметр iid передается идентификатор интерфейса, реализуемого COM-классом, с которым связана данная фабрика. Метод должен создать соответствующий COM-объект, запросить у него указатель на интерфейс iid и записать полученный указатель в переменную по адресу ppvDest. Возвращать необходимо одно из следующих значений:

S_OK
успешное выполнение;
CLASS_E_NOAGGREGATION
класс не поддерживает агрегацию;
E_NOINTERFACE
класс не реализует интерфейс с идентификатором iid.
HRESULT LockServer(
   BOOL fLock
);

Через переметр fLock в этот метод передается TRUE, если приложение-клиент пытается блокировать COM-сервер в памяти, или FALSE — если оно пытается его разблокировать. В реализации метода необходимо соответствующим образом, в зависимости от значения параметра fLock, изменять значение счетчика блокировок сервера, который обычно реализуется как глобальная целочисленная переменная. Метод должен возвращать одно из следующих значений:

S_OK
успешное выполнение;
E_OUTOFMEMORY
для выполнения требуемой операции недостаточно памяти;
E_FAIL
произошла фатальная ошибка.

Заметим, что COM-сервер не должен позволять выгружать себя из памяти, если значение счетчика блокировок больше нуля.

Создание COM-объекта

Для создания COM-объекта при помощи фабрики класса приложение-клиент должно вначале вызвать функцию

HRESULT CoGetClassObject(
   REFCLSID clsid,
   DWORD fdwContext,
   COSERVERINFO* pInfo,
   REFIID iid,
   void** ppvDest
);

Через параметр clsid передается идентификатор COM-класса, объект-экземпляр которого необходимо создать. Параметр fdwContext должен быть комбинацией флагов CLSCTX_xxx, определяющих, какие виды COM-серверов необходимо задействовать. Через параметр pInfo необходимо передать адрес структуры, определяющей местоположение COM-сервера; для внутрипроцессных серверов, всегда выполняющихся на том же компьютере, что и приложение-клиент, этот параметр задается равным NULL. Через параметр iid передается идентификатор реализованного фабрикой класса стандартного интерфейса; как правило, это интерфейс IClassFactory, имеющий идентификатор IID_IClassFactory (фабрика класса может также реализовывать интерфейс IClassFactory2, являющийся расширением IClassFactory и позволяющий блокировать выполнение COM-сервера на компьютере, не имеющем соответствующего лицензионного ключа). Функция записывает указатель на полученный интерфейс в переменную по адресу ppvDest и возвращает одно из следующих значений:

S_OK
указатель на интерфейс с идентификатором iid успешно получен;
REGDB_E_CLASSNOTREG
класс с идентификатором clsid не зарегистрирован в системном реестре;
REGDB_E_READREGDB
не удалось прочесть системный реестр;
E_NOINTERFACE
фабрика класса с идентификатором clsid не реализует интерфейс iid;
E_ACCESSDENIED
не удалось загрузить требуемый COM-сервер;
CO_E_DLLNOTFOUND
файл внутрипроцессного сервера не найден;
CO_E_ERRORINDLL
файл сервера поврежден;
CO_E_APPNOTFOUND
файл автономного или удаленного сервера не найден;
CO_E_APPDIDNTREG
автономный или удаленный сервер не смог записать информацию в системный реестр.

После получения указателя на интерфейс IClassFactory можно воспользоваться его методом CreateInstance для создания соответствующего COM–объекта и получения указателя на один реализуемых им интерфейсов. Ниже приведен пример фрагмента кода условного приложения-клиента.

IClassFactory* pFact;
IFoo* pFoo;

// получаем указатель на интерфейс фабрики класса
::CoGetClassObject(CLSID_CoMyClass, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**)&pFact);

// создаем COM-объект и получаем указатель на интерфейс
pFact->CreateInstance(NULL, IID_IFoo, (void**)&pFoo);

...   // повызывали методы интерфейса

// интерфейс больше не нужен
pFoo->Release();

// фабрика класса больше не нужна
pFact->Release();

Экспортируемые функции

Внутрипроцессный COM-сервер должен экспортировать следующие четыре функции, вызываемые COM-библиотекой в процессе взаимодействия приложения-клиента с этим сервером.

STDAPI DllGetClassObject(
   REFCLSID clsid,
   REFIID iid,
   void** ppvDest
);

Реализация этой функции должна создавать экземпляр фабрики класса, имеющего идентификатор clsid, получать у этого экземпляра указатель на интерфейс с идентификатором iid, записывать полученный указатель в переменную по адресу ppvDest и возвращать одно из следующих значений:

S_OK
успешное выполнение;
CLASS_E_CLASSNOTAVAILABLE
данный сервер не реализует класс с идентификатором clsid;
E_INVALIDARG
функции передан недопустимый аргумент;
E_OUTOFMEMORY
для выполнения требуемой операции недостаточно памяти;
E_UNEXPECTED
произошла непредвиденная ошибка.

Таким образом, при создании COM-объекта с использованием фабрики класса выполняются следующие действия:

  1. приложение-клиент вызывает функцию CoGetClassObject;
  2. COM-библиотека находит через системный реестр имя файла сервера, загружает сервер в память и вызывает экспортируемую им функцию DllGetClassObject;
  3. сервер создает в функции DllGetClassObject экземпляр фабрики класса, получает у него указатель на интерфейс IClassFactory и возвращает этот указатель приложению-клиенту;
  4. приложение-клиент вызывает через этот указатель метод CreateInstance для создания COM-объекта.
STDAPI DllCanUnloadNow(void);

Именно в реализации этой функции должно анализироваться значение счетчика блокировок сервера в памяти, а также количество существующих в данный момент COM-объектов, созданных им. Если обе эти величины равны нулю, функция должна возвращать значение S_OK, показывающее COM-библиотеке, что сервер может быть выгружен из памяти. В противном случае, необходимо возвращать значение S_FALSE, запрещающее выгружать сервер из памяти.

Наличие следующих двух функций не является обязательным, но каждый уважающий себя COM-сервер их реализует.

STDAPI DllRegisterServer(void);

Реализация этой функции должна записывать в системный реестр информацию, необходимую для корректного функционирования COM-сервера, и возвращать одно из следующих значений:

S_OK
успешное выполнение;
SELFREG_E_TYPELIB
невозможно завершить регистрацию библиотек типов для реализуемых COM-классов;
SELFREG_E_CLASS
невозможно завершить регистрацию реализуемых COM-классов;
E_OUTOFMEMORY
для выполнения требуемой операции недостаточно памяти;
E_UNEXPECTED
произошла непредвиденная ошибка.
STDAPI DllUnregisterServer(void);

Реализация этой функции должна удалять из системного реестра информацию, записанную туда функцией DllRegisterServer и возвращать одно из следующих значений:

S_OK
успешное выполнение;
S_FALSE
все известные записи были успешно удалены, но в системном реестре остались записи, относящиеся к реализуемым COM-классам;
SELFREG_E_TYPELIB
невозможно удалить информацию обо всех библиотеках типов для реализуемых COM-классов;
SELFREG_E_CLASS
невозможно удалить информацию обо всех реализуемых COM-классах;
E_OUTOFMEMORY
для выполнения требуемой операции недостаточно памяти;
E_UNEXPECTED
произошла непредвиденная ошибка.

Информация в системном реестре

Каждый COM-класс, помимо числового идентификатора в формате GUID, должен иметь так называемый программный идентификатор (programmatic identifier, PROGID). Этот идентификатор должен быть строкой, длиной не более 39 символов, начинающейся с буквы, состоящей из букв и цифр и не содержащей никаких других символов, кроме точек. Как правило, этот идентификатор имеет вид

vendor.component.version

Здесь vendor — это название фирмы-разработчика COM-класса (иногда название продукта, частью которого он является), component — строковое имя COM-класса, а version — номер версии данного класса. Таким образом, получается что-то вроде SuperCorp.SuperCoClass.1 (если Вы являетесь программистом-индивидуалом, то в качестве названия фирмы можете указать свое имя).

Получить программный идентификатор COM-класса, зная его GUID, можно с помощью функции

WINOLEAPI ProgIDFromCLSID(
   REFCLSID clsid,
   LPOLESTR* ppszDest
);

Через параметр clsid передается идентификатор COM-класса, а через параметр ppszDest — адрес переменной, в которую будет записан указатель на строку, содержащую PROGID. Память под эту строку выделяется COM-библиотекой и приложение-клиент должно освободить ее, используя метод Free интерфейса IMalloc. Функция возвращает одно из следующих значений:

S_OK
успешное выполнение;
REGDB_E_CLASSNOTREG
класс с идентификатором clsid не зарегистрирован в системном реестре;
REGDB_E_READREGDB
не удалось прочеть системный реестр.

Обратная задача (получение GUID'а COM-класса по его программному идентификатору) решается с помощью функции

WINOLEAPI CLSIDFromProgID(
   LPCOLESTR pszProgID
   CLSID* pDest,
);

Через параметр pszProgID передается адрес строки, содержащей программный идентификатор, а через pDest — адрес переменной, в которую будет скопирован идентификатор COM-класса. Функция может вернуть одно из следующих значений:

S_OK
успешное выполнение;
CO_E_CLASSSTRING
строка по адресу pszProgID содержит некорректный программный идентификатор;
REGDB_E_WRITEREGDB
ошибка записи в системный реестр.

Для обеспечения корректной работы COM-библиотеки (в частности, двух перечисленных выше функций) необходимо, чтобы в системном реестре каждому COM-классу соответствовала информация приведенной ниже структуры.

1. Должен существовать ключ вида

HKEY_CLASSES_ROOT\CLSID\{идентификатор_класса}

содержащий в качестве значения по умолчанию (безымянного значения) строку вида

программный_идентификатор

2. Должен существовать ключ вида

HKEY_CLASSES_ROOT\CLSID\{идентификатор_класса}\InprocServer32

содержащий в качестве значения по умолчанию строку с полным именем исполняемого файла сервера.

3. Должен существовать ключ вида

HKEY_CLASSES_ROOT\программный_идентификатор\CLSID

содержащий в качестве значения по умолчанию строку вида

{идентификатор_класса}

Заметим, что при записи в системный реестр идентификатор_класса должен указываться в строковом представлении (соответствующий вариант в утилите guidgen.exe так и называется — Registry Format); сама же запись «значений по умолчанию» выполняется с помощью функции Win32 API RegSetValue.

Вся перечисленная информация должна записываться в системный реестр функцией DllRegisterServer и удаляться из него функцией DllUnregisterServer; в этом случае, для «ручной» регистрации COM–сервера можно будет воспользоваться командой вида

regsvr32[.exe] имя_файла_сервера

а для «разрегистрации» (unregistration) — командой вида

regsvr32[.exe] /u имя_файла_сервера

Процесс разработки

В качестве примера мы рассмотрим процесс написания простейшего COM-сервера, предоставляющего клиентам всего один COM-класс — CoFile, который инкапсулирует базовые операции файлового ввода/вывода. Эти операции разделены на три группы (инициализация, чтение/запись данных и получение информации), образующие соответственно интерфейсы IFileInit, IFileOperate и IFileStatus.

Шаг 1: генерация идентификаторов и связывание их с предоставляемыми интерфейсами.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Identifiers.h

#define S_UUID_IFileInit "65D49710-716A-4629-85C1-7FBBCCA5E296"
#define S_UUID_IFileOperate "661D690B-5DC4-4E06-8AD3-BB7404A4B4C9"
#define S_UUID_IFileStatus "FF544CDC-1B16-4FB6-A462-F1D71BC99901"
#define S_UUID_CoFile "C629BA33-069D-460F-B531-5AC7A1668192"

struct __declspec(uuid(S_UUID_IFileInit)) IFileInit;
#define IID_IFileInit __uuidof(IFileInit)
struct __declspec(uuid(S_UUID_IFileOperate)) IFileOperate;
#define IID_IFileOperate __uuidof(IFileOperate)
struct __declspec(uuid(S_UUID_IFileStatus)) IFileStatus;
#define IID_IFileStatus __uuidof(IFileStatus)

class __declspec(uuid(S_UUID_CoFile)) CoFile;
#define CLSID_CoFile __uuidof(CoFile)

Для генерации идентификаторов была использована уже упоминавшаяся утилита guidgen.exe, которая поставляется вместе с Microsoft Visual Studio (исходные тексты этой утилиты входят в число примеров, прилагающихся к MSDN), причем в качестве формата был выбран последний из предлагаемых ей вариантов — Registry Format.

Связывание сгенерированных идентификаторов с предоставляемыми интерфейсами (строки 8, 10, 12) и COM-классом (строка 15) выполнено с помощью описателя класса памяти (storage-class specifier) вида

__declspec(uuid("идентификатор"))

который может использоваться в объявлении структур и классов C++. Этот «описатель» является расширением языка C++, которое поддерживается всеми последними версиями компиляторов от Borland и Microsoft. Заметим, что достаточно однократного употребления этого описателя (например, в опережающем объявлении класса или структуры, как это сделано в рассматриваемом исходном тексте); в дальнейших объявлениях он может быть опущен, хотя его повторение и не воспринимается компилятором как синтаксическая ошибка.

Для «извлечения» из класса или структуры связанного с ней подобным образом идентификатора используется оператор

__uuidof(выражение)

где выражение может быть либо именем типа, либо указателем или ссылкой на переменную соответствующего типа.

С целью соблюдения единообразия символических имен идентификаторов интерфейсов и COM-классов в исходном тексте объявлены четыре макроподстановки (строки 9, 11, 13 и 16).

Шаг 2: объявление предоставляемых интерфейсов.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Interfaces.h

struct IFileInit: public IUnknown
{
   virtual BOOL __stdcall Open(BSTR bstrName, BSTR bstrMode) = 0;
   virtual BOOL __stdcall Close(void) = 0;
};

struct IFileOperate: public IUnknown
{
   virtual UINT __stdcall Read(void* pvDest, UINT uSize) = 0;
   virtual UINT __stdcall Write(const void* pvSrc, UINT uSize) = 0;
   virtual BOOL __stdcall Seek(long nOffset, int nOrigin) = 0;
};

struct IFileStatus: public IUnknown
{
   virtual long __stdcall Tell(void) = 0;
};

Все предоставляемые интерфейсы (строки 3…19) объявлены обычным для C++ образом — как структуры, наследуемые от IUnknown, и содержащие только чисто-виртуальные методы. Заметим, что все методы интерфейсов должны использовать соглашение о вызовах __stdcall (by design of COM).

Шаг 3: объявление класса, реализующего предоставляемые интерфейсы.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// CoFileClass.h

class CoFile: IFileInit, IFileOperate, IFileStatus
{
   friend HRESULT WINAPI DllCanUnloadNow(void);
public:
   CoFile(void);
   ~CoFile(void);
public:
   virtual HRESULT __stdcall QueryInterface(REFIID iid, void** ppvDest);
   virtual ULONG __stdcall AddRef(void);
   virtual ULONG __stdcall Release(void);
public:
   virtual BOOL __stdcall Open(BSTR bstrName, BSTR bstrMode);
   virtual BOOL __stdcall Close(void);
public:
   virtual UINT __stdcall Read(void* pvDest, UINT uSize);
   virtual UINT __stdcall Write(const void* pvSrc, UINT uSize);
   virtual BOOL __stdcall Seek(long nOffset, int nOrigin);
public:
   virtual long __stdcall Tell(void);
private:
   int m_nNumRefs;
   FILE* m_fp;
   static int m_nNumObjs;
};

Для реализации COM-класса, являющегося, с формальной точки зрения, обычным классом C++, использовано множественное наследование от всех предоставляемых интерфейсов (строка 3); заметим, что указание интерфейса IUnknown в качестве предка не требуется, поскольку все интерфейсы уже являются его потомками.

Функция DllCanUnloadNow объявлена дружественной (строка 5) для получения доступа к закрытому полю m_nNumObjs (строка 25), в котором хранится количество созданных сервером COM-объектов — экземпляров данного класса.

Класс CoFile содержит все методы всех предоставляемых им интерфейсов (включая методы IUnknown). Поле m_nNumRefs (строка 23) используется для реализации счетчика ссылок, а m_fp (строка 24) — для хранения идентификатора файла.

Шаг 4: реализация методов COM-класса.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// CoFileClass.cpp

CoFile::CoFile(void)
{
   m_nNumRefs = 0;
   m_fp = NULL;
   ++m_nNumObjs;
}

CoFile::~CoFile(void)
{
   --m_nNumObjs;
}

HRESULT __stdcall CoFile::QueryInterface(REFIID iid, void** ppvDest)
{
   if (iid == IID_IUnknown)
   {
      *ppvDest = static_cast<IUnknown*>(static_cast<IFileInit*>(this));
   }
   else if (iid == IID_IFileInit)
   {
      *ppvDest = static_cast<IFileInit*>(this);
   }
   else if (iid == IID_IFileOperate)
   {
      *ppvDest = static_cast<IFileOperate*>(this);
   }
   else if (iid == IID_IFileStatus)
   {
      *ppvDest = static_cast<IFileStatus*>(this);
   }
   else
   {
      *ppvDest = NULL;
      return (E_NOINTERFACE);
   }
   reinterpret_cast<IUnknown*>(*ppvDest)->AddRef();
   return (S_OK);
}

ULONG __stdcall CoFile::AddRef(void)
{
   return (++m_nNumRefs);
}

ULONG __stdcall CoFile::Release(void)
{
   if (--m_nNumRefs == 0)
   {
      delete this;
   }
   return (m_nNumRefs);
}

BOOL __stdcall CoFile::Open(BSTR bstrName, BSTR bstrMode)
{
#if defined(_UNICODE)
   m_fp = _wfopen(bstrName, bstrMode);
#else
   char szName[_MAX_PATH];
   char szMode[16];

   wcstombs(szName, bstrName, sizeof(szName));
   wcstombs(szMode, bstrMode, sizeof(szMode));
   m_fp = fopen(szName, szMode);
#endif   // _UNICODE
   return (m_fp != NULL);
}

BOOL __stdcall CoFile::Close(void)
{
   return (m_fp != NULL ? fclose(m_fp) == 0 : FALSE);
}

UINT __stdcall CoFile::Read(void* pvDest, UINT uSize)
{
   return (m_fp != NULL ? fread(pvDest, 1, uSize, m_fp) : 0);
}

UINT __stdcall CoFile::Write(const void* pvSrc, UINT uSize)
{
   return (m_fp != NULL ? fwrite(pvSrc, 1, uSize, m_fp) : 0);
}

BOOL __stdcall CoFile::Seek(long nOffset, int nOrigin)
{
   return (m_fp != NULL ? fseek(m_fp, nOffset, nOrigin) == 0 : FALSE);
}

long __stdcall CoFile::Tell(void)
{
   return (m_fp != NULL ? ftell(m_fp) : -1);
}

int CoFile::m_nNumObjs = 0;

Конструктор класса CoFile (строки 3…8) обнуляет счетчик ссылок на созданный объект и увеличивает на единицу значение количества созданных объектов; в деструкторе (строки 10…13) это значение соответствующим образом уменьшается.

Реализация метода QueryInterface (строки 15…40) проверяет значение, переданное через параметр iid, и если оно совпадает с идентификатором одного из реализуемых интерфейсов — записывает требуемый указатель по адресу ppvDest, выполняя соответствующее приведение типа. Заметим, что для предоставленного интерфейса вызывается метод AddRef (строка 38). Если же запрошенный интерфейс не реализуется данным COM-классом, метод обнуляет указатель, находящийся по адресу ppvDest и возвращает значение E_NOINTERFACE (ну нету такого интерфейса).

Реализация метода AddRef (строки 42…45) очень проста — она увеличивает на единицу значение счетчика ссылок и возвращает его. В методе Release (строки 47…54) это значение, наоборот, уменьшается и если оно оказывается равным нулю — объект уничтожается за ненадобностью (строка 51).

Остальные методы класса CoFile (строки 56…94) реализуют «содержательную часть» и позволяют выполнять с помощью таких объектов базовые операции файлового ввода/вывода. По сути, все эти методы являются «тонкими» оболочками функций стандартной библиотеки C.

Шаг 5: объявление соответствующей фабрики класса.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// CoFileFact.h

class CoFileFactory: IClassFactory
{
   friend HRESULT WINAPI DllCanUnloadNow(void);
public:
   CoFileFactory(void);
   ~CoFileFactory(void);
public:
   virtual HRESULT __stdcall QueryInterface(REFIID iid, void** ppvDest);
   virtual ULONG __stdcall AddRef(void);
   virtual ULONG __stdcall Release(void);
public:
   virtual HRESULT __stdcall CreateInstance(IUnknown* pUnk, REFIID iid, void** ppvDest);
   virtual HRESULT __stdcall LockServer(BOOL fLock);
private:
   int m_nNumRefs;
   static int m_nNumLocks;
   static int m_nNumObjs;
};

Фабрика класса выполнена в виде отдельного класса C++, унаследованного от интерфейса IClassFactory, и реализующего его методы (строки 14…15), а также методы IUnknown (строки 10…12), который является предком этого интерфейса. Еще раз подчеркнем, что фабрике класса не назначается собственный GUID.

Функция DllCanUnloadNow объявлена дружественной (строка 5) для получения доступа к закрытому полю m_nNumObjs (строка 19), в котором хранится количество созданных сервером объектов — экземпляров данного класса.

В поле m_nNumRefs (строка 17) хранится текущее значение счетчика ссылок, а статическое поле m_nNumLocks (строка 18) предназначено для подсчета количества блокировок COM-сервера, выполняемых с помощью метода LockServer.

Шаг 6: реализация фабрики класса.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// CoFileFact.cpp

CoFileFactory::CoFileFactory(void)
{
   m_nNumRefs = 0;
   ++m_nNumObjs;
}

CoFileFactory::~CoFileFactory(void)
{
   --m_nNumObjs;
}

HRESULT __stdcall CoFileFactory::QueryInterface(REFIID iid, void** ppvDest)
{
   if (iid == IID_IUnknown)
   {
      *ppvDest = static_cast<IUnknown*>(this);
   }
   else if (iid == IID_IClassFactory)
   {
      *ppvDest = static_cast<IClassFactory*>(this);
   }
   else
   {
      *ppvDest = NULL;
      return (E_NOINTERFACE);
   }
   static_cast<IUnknown*>(*ppvDest)->AddRef();
   return (S_OK);
}

ULONG __stdcall CoFileFactory::AddRef(void)
{
   return (++m_nNumRefs);
}

ULONG __stdcall CoFileFactory::Release(void)
{
   if (--m_nNumRefs == 0)
   {
      delete this;
   }
   return (m_nNumRefs);
}

HRESULT __stdcall CoFileFactory::CreateInstance(IUnknown* pUnk, REFIID iid, void** ppvDest)
{
   if (pUnk != NULL)
   {
      *ppvDest = NULL;
      return (CLASS_E_NOAGGREGATION);
   }
   else
   {
      CoFile* pCoFile = new CoFile();
      HRESULT hr = pCoFile->QueryInterface(iid, ppvDest);
      if (IS_ERROR(hr))
      {
         delete pCoFile;
      }
      return (hr);
   }
}

HRESULT __stdcall CoFileFactory::LockServer(BOOL fLock)
{
   fLock ? ++m_nNumLocks : --m_nNumLocks;
   return (S_OK);
}

int CoFileFactory::m_nNumLocks = 0;
int CoFileFactory::m_nNumObjs = 0;

Конструктор класса CoFileFactory (строки 3…7) обнуляет счетчик ссылок на созданный объект и увеличивает на единицу значение количества созданных объектов; в деструкторе (строки 9…12) это значение соответствующим образом уменьшается.

Реализация методов IUnknown (строки 14…45) стандартна и по смыслу полностью совпадает с тем, что было описано выше для класса CoFile.

Метод CreateInstance (строки 47…64) вначале проверяет, не пытается ли приложение-клиент создать экземпляр класса CoFile как составляющую часть объекта-агрегата (строка 49) и если это так — возвращает значение CLASS_E_NOAGGREGATION, показывающее, что наш класс агрегацию не поддерживает. В противном случае, создается экземпляр класса (строка 56) и у него запрашивается указатель на интерфейс с идентификатором iid (строка 57). Если при получении этого указателя происходит ошибка (например, класс не реализует такого интерфейса), созданный объект тут же уничтожается (строка 60).

В рализации метода LockServer (строки 66…70) выполняется изменение счетчика блокировок сервера в соответствии со значением параметра fLock.

Шаг 7: реализация экспортируемых функций и точки входа COM-сервера.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// EntryPoint.cpp

static HINSTANCE g_hInstance = NULL;
static const TCHAR g_szProgID[] = _T("Zarezky.CoFile.1");
static const TCHAR g_szClassID[] = _T(S_UUID_CoFile);

STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, void** ppvDest)
{
   if (clsid == CLSID_CoFile)
   {
      CoFileFactory* pFact = new CoFileFactory();
      HRESULT hr = pFact->QueryInterface(iid, ppvDest);
      if (IS_ERROR(hr))
      {
         delete pFact;
      }
      return (hr);
   }
   else
   {
      return (CLASS_E_CLASSNOTAVAILABLE);
   }
}

STDAPI DllCanUnloadNow(void)
{
   int nNumObjs = CoFileFactory::m_nNumObjs + CoFile::m_nNumObjs;
   return (CoFileFactory::m_nNumLocks == 0 && nNumObjs == 0 ? S_OK : S_FALSE);
}

STDAPI DllRegisterServer(void)
{
   TCHAR szKey[80];
   TCHAR szSrvPath[_MAX_PATH];
   TCHAR szValue[64];

   ::wsprintf(szKey, _T("CLSID\\{%s}"), g_szClassID);
   ::RegSetValue(HKEY_CLASSES_ROOT, szKey, REG_SZ, g_szProgID, ::lstrlen(g_szProgID) + 1);

   ::wsprintf(szKey, _T("CLSID\\{%s}\\InprocServer32"), g_szClassID);
   ::GetModuleFileName(g_hInstance, szSrvPath, _MAX_PATH);
   ::RegSetValue(HKEY_CLASSES_ROOT, szKey, REG_SZ, szSrvPath, ::lstrlen(szSrvPath) + 1);

   ::wsprintf(szKey, _T("%s\\CLSID"), g_szProgID);
   ::wsprintf(szValue, _T("{%s}"), g_szClassID);
   ::RegSetValue(HKEY_CLASSES_ROOT, szKey, REG_SZ, szValue, ::lstrlen(szValue) + 1);

   return (S_OK);
}

STDAPI DllUnregisterServer(void)
{
   TCHAR szKey[80];

   ::SHDeleteKey(HKEY_CLASSES_ROOT, g_szProgID);
   ::wsprintf(szKey, _T("CLSID\\{%s}"), g_szClassID);
   ::SHDeleteKey(HKEY_CLASSES_ROOT, szKey);

   return (S_OK);
}

BOOL WINAPI DllMain(HINSTANCE hInst, DWORD fdwReason, void* /*pvReserved*/)
{
   if (fdwReason == DLL_PROCESS_ATTACH)
   {
      g_hInstance = hInst;
   }
   return (TRUE);
}

Реализация функции DllGetClassObject (строки 7…23) проверяет значение, переданное через параметр clsid, и если оно совпадает с идентификатором реализуемого данным сервером COM-класса — создается экземпляр фабрики этого класса (строка 11) и у него запрашивается указатель на интерфейс с идентификатором iid (строка 12). Если при получении этого указателя происходит ошибка, созданная фабрика класса уничтожается (строка 15). Вызов данной функции с идентификатором класса, отличным от CLSID_CoFile, приводит к ее завершению со значением CLASS_E_CLASSNOTAVAILABLE (класс абсолютно недосягаем).

Фукнция DllCanUnloadNow (строки 25…29) анализирует количество существующих в данный момент объектов (экземпляров фабрики класса и самого CoFile), а также значение счетчика блокировок сервера. Если сервер не блокирован и живых объектов нет, выгрузка сервера разрешается; в противном случае он остается висеть в памяти, отнимая у системы и без того скудные ресурсы.

Функция DllRegisterServer (строка 31…49) формирует и записывает в системный реестр информацию, необходимую для корректного взаимодействия COM-библиотеки и приложения-клиента с данным сервером, а функция DllUnregisterServer (строки 51…60) удаляет из реестра эту информацию.

Единственное назначение точки входа DllMain (строки 62…69) состоит в том, чтобы записывать дескриптор модуля в глобальную переменную g_hInstance при его загрузке в адресное пространство очередного процесса (строки 64…67).

Шаг 8: экспорт функций через файл определения модуля.

1
2
3
4
5
6
7
8
9
10
;; Module.def

LIBRARY "FileServer"
DESCRIPTION "Sample In-proc Server"

EXPORTS
   DllGetClassObject @1 PRIVATE
   DllCanUnloadNow @2 PRIVATE
   DllRegisterServer @3 PRIVATE
   DllUnregisterServer @4 PRIVATE

Компоновщик Microsoft требует, чтобы четыре специальные функции, экспортируемые COM-сервером, не попадали в автоматически создаваемую им библиотеку импорта, поэтому их необходимо экспортировать через так называемый файл определения модуля (module definition file). В разделе EXPORTS этого файла необходимо явно задать произвольные номера экспортируемых функций, предваряя их символом «@», и указать атрибут PRIVATE (строки 7…10).

Заключение

Полный текст примера, включающий в себя тестовое приложение-клиент, можно скачать по ссылке, находящейся в конце этой страницы. Заметим, что здесь реализован минимально возможный COM-сревер, обращение к которому из приложений, написанных на других языках программирования (особенно Visual Basic), очень затруднительно. В следующих темах данного раздела мы постараемся избавиться от этого недостатка.

обновлено
29.03.2006
 
Проверка PR и ТИЦ