Введение в COM
Существует устойчивое мнение, что COM - чрезвычайно сложная технология и с ней лучше не связываться. В реальности же все не так плохо. Конечно, она не позволяет писать простейшие приложения программистам без какой-либо подготовки как C# или даже C++, но нас ведь это ограничение не касается?
Идеи, лежащие в основе COM, чрезвычайно просты, поняв их, можно с легкостью использовать существующие и писать новые компоненты. Попробуем для начала понять эти идеи с точки зрения потребителя компонента.
COM-компонент реализует т.н. называемые интерфейсы. Интерфейс с точки зрения COM - просто набор функций, объединенных по какому-либо функциональному признаку. Кроме вызова функций интерфейсов вы не имеете возможности как-либо воздействовать с компонентом. Работа с компонентов происходит примерно так: вы запрашиваете нужный вам интерфейс у компонента, если компонент поддерживает этот интерфейс, он возвращает вам указатель на него и вы можете использовать функции интерфейса, затем вы можете получить новый интерфейс и использовать его функции и т.д. Возникает вопрос, каким образом вы можете получать нужные вам интерфейсы - ответ очевиден - через другой интерфейс IUnknown.
Любой интерфейс COM наследует от базового интерфейса IUnknown, и любой компонент COM обязан реализовать этот интерфейс. Поэтому получить указатель на интересующий вас интерфейс вы можете с помощью любого интерфейса компонента, который у вас есть. Рассмотрим IUnknown:
#define interface struct
interface IUnknown
{
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0;
virtual ULONG __stdcall AddRef() = 0;
virtual ULONG __stdcall Release() = 0;
};
Первый объявленный в IUnknown метод QueryInterface как раз и обеспечивает возможность получения интерфейса через его уникальный идентификатор IID. IID, по своей сути, является обычным GUID`ом и уникально идентифицирует во времени и пространстве интерфейс COM. Второй параметр - это указатель на указатель на необходимый нам интерфейс. Идея также предельно проста - когда запрашивается новый интерфейс - счетчик ссылок увеличивается на единицу с помощью функции AddRef, а когда интерфейс становится не нужен уменьшается на единицу с помощью Release. В конце концов, когда счетчик ссылок обнуляется компонент выгружается из памяти. Обратите внимание, что при вызове QueryInterface AddRef вызывается автоматически (в самом QueryInterface), от вас остается вызывать Release по окончании использования интерфейса.
Последние две функции осуществляют подсчет ссылок с помощью которых происходит управление памятью и ресурсами COM-компонента.
Чтобы не быть голословным приведу пример использования функции QueryInterface:
IRowset* pIRowset = NULL;
//... инициализация IRowset и пр.
HRESULT hr = S_OK;
//объявление IColumnsInfo
IColumnsInfo *pIColumnsInfo = NULL;
ULONG ulNumCols;
DBCOLUMNINFO *pDBColumnInfo = NULL;
WCHAR *pStringsBuffer = NULL;
//получение IColumnsInfo с помощью существующего указателя pIRowset
//IID_IColumnsInfo - уникальный идентификатор IColumnsInfo
hr = m_pIRowset->QueryInterface(IID_IColumnsInfo, (void **)&pIColumnsInfo);
if(hr == E_NOINTERFACE)
{
//интерфейс не поддерживается
}
//использование pIColumnsInfo
hr = pIColumnsInfo->GetColumnInfo(&ulNumCols, &pDBColumnInfo, &pStringsBuffer);
if(FAILED(m_error))
{
//ошибка при получении информации о полях таблицы
}
//освобождение интерфейса IColumnsInfo
pIColumnInfo->Release();
В данном примеру у нас был указатель на интерфейс IRowset, и мы задались целью получить список полей этого Rowset`a. Для этого мы запросили у него указатель на интерфейс IColumnsInfo, который и позволяет получить список полей. А затем вызывали соответствующую функцию IColumnInfo для получения списка полей. И в конце мы уведомили COM о том, что pIColumnsInfo больше не используется, вызвав метод Release.
Как правило, использование COM-компонентов происходит по этому сценарию:
1. Запрашиваем интерфейс с помощью QueryInterface (при этом автоматом вызывается AddRef)
2. Вызываем одну или несколько функций полученного интерфейса
3. Сообщаем что интерфейс нам больше не нужен с помощью функции Release.
Большинство функции интерфейсов COM возвращают в качестве результата HRESULT. Существует два простых макроса, позволяющих проверить удачно или неудачно выполнилась функция: SUCCEEDED и FAILED. Если функция выполнилась удачно старший бит HRESULT устанавливается в 0, если неудачно в 1. Теперь становится очевидна реализация макросов SUCCEEDED и FAILED:
#define SUCCEEDED(Status) ((HRESULT)(Status) >= 0)
#define FAILED(Status) ((HRESULT)(Status)<0)
Теперь поговорим о счетчике ссылок. Каждый COM-компонент, как правило, реализует несколько интерфейсов - каждый его интерфейс можно запросить с помощью QueryInterface. У компонента также существует счетчик ссылок - один на все интерфейсы, когда интерфейс запрашивается счетчик ссылок увеличивается на 1, когда интерфейс освобождается счетчик ссылок уменьшается на 1. Если в этот момент счетчик ссылок становится равен нулю - компонент выгружает себя из памяти. В вышеприведенном примере у нас было как минимум два указателя на интерфейсы COM-компонента. Т.е. его счетчик ссылок достигал, как минимум, значения 2. После освобождения pIColumnsInfo компонент не выгрузился из памяти, т.к. оставался еще, как минимум, pIRowset. Поэтому, по идее, мы могли и дальше использовать pIColumnsInfo (что такое по сути интерфейс - лишь таблица в памяти, которая содержит указатели на функции, поэтому если мы повторно запросим IColumnsInfo - новый указатель будет равен старому - т.е. будет указывать на то же самое место). Однако, это не является хорошей практикой - т.к. все построено на допущении, что используется единый счетчик ссылок. Поэтому после вызова Release использование интерфейса недопустимо - даже несмотря на то, что у вас будет все превосходно работать.
Получать указатели на новый интерфейсы посредством существующих мы уже научились, теперь осталась одна загвоздка - как же все-таки получить самый первый указатель на один из интерфейсов компонента. Инициализация компонента и получение первого указателя на интерфейс производится с помощью функции:
HRESULT __stdcall CoCreateInstance(CLSID &rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, IID &riid, LPVOID* ppv);
Первый параметр rclsid - представляет собой ссылку на уникальный идентификатор компонента, о нем поговорим чуть позже. Второй параметр представляет собой указатель на т.н. внешний IUnknown, он используется в случае агрегирования интерфейса компонента. Т.е. в том случае когда вы пишите свой компонент и предоставляете клиентам своего компонента указатель на этот интерфейс. При этом крайне желательно, чтобы данный интерфейс использовал вашу реализацию IUnknown, а не свою собственную (иначе QueryInterface и Release будут работать неправильно). Третий параметр определяет контекст в котором будут работать компонент: CLSCTX_INPROC_SERVER - в адресном пространстве вашего процесса (т.к. будет загружать как DLL), CLSCTX_LOCAL_SERVER - в своем собственном адресном пространстве (как EXE), CLSCTX_REMOTE_SERVER - на удаленной машине (это уже DCOM). Существуют и другие параметры - подробности можете посмотреть в MSDN в разделе о перечислении CLSCTX. iid - ссылка на уникальный идентификатор интерфейса, который мы хотим получить. Ну и ppv, как было замечено ранее, указатель на указатель на интерфейс. Два последних параметра вам отлично знакомы по функции QueryInterface.
Теперь поговорим о самом загадочном и важном параметре CLSID. Как я уже писал это уникальный идентификатор компонента, по сути же это GUID, как и IID. Благодаря CLSID COM узнает из какого конкретно файла загрузить компонент и какой конкретно компонент. Ради интереса и лучшего понимания вы можете посмотреть список установленных компонентов в системе. Для этого откройте редактор реестра и посмотрите в нем раздел HKEY_CLASSES_ROOT\CLSID. Здесь вы увидите список CLSID`ов установленных компонентов. В параметре по умолчанию раздела текущего CLSID указано дружественное имя компонента, а в разделе InprocServer32 - путь к файлу с реализацией компонента. Раздел ProgId содержит дружественный синоним CLSID компонента. Как правило, ProgId имеет следующий формат: Программа.Компонент.Версия. ProgId, используется, как правило, для создания компонентов на языках вроде Visual Basic (и иже им подобной порнографии). Получить CLSID из ProgId можно с помощью следующей функции:
HRESULT CLSIDFromProgID(LPCOLESTR lpszProgID, LPCLSID pclsid);
Для обратного преобразования можно воспользоваться функцией:
WINOLEAPI ProgIDFromCLSID(REFCLSID clsid, LPOLESTR* lplpszProgID);
Ряд функций интерфейсов COM возвращает строки или массивы, при этом память выделяется где-то в недрах компонента. Не зная метода выделения памяти мы не сможем корректно ее освободить. Как правило, память выделяется с помощью функции CoTaskMemAlloc, следовательно освобождать выделенную память следует с помощью CoTaskMemFree, если в документации не оговорено иное
Автор: Александр Игнатьев
Источник: www.vestace.ru
Добавить закладку на материал:
|