3 кита COM. Кит первый: реестр — Архив WASM.RU
Эта статья была написана несколько лет назад. Может, она так и осталась бы валяться в архиве на CD среди прочего хлама, если бы я недавно не наткнулся на нее, разбирая старые материалы. Хотя задумывалось это в свое время как серия из трех статей, а в наличии пока только две (и я не знаю, сподвигнусь ли завершить эту серию), и даже несмотря на высказываемое некоторыми мнение, что "COM-де нынче устарел, а модно .NET", при перечитывании мне самому стало интересно, а вместе с тем и жалко, что такой материал пропадает - раз уж он был в свое время создан, я решил сделать его доступным для всех желающих, если такие найдутся.
Технология COM для многих программистов так и остается желанным, но слишком дорогим и поэтому недоступным ресурсом. Лишь беглый взгляд на все это нагромождение объектов, интерфейсов, правил их взаимодействия, отягощенный способами их реализации на том или ином языке и громоздкими вспомогательными конструкциями тех или иных сред разработки, призванными "облегчить" использование COM, способен надолго отбить охоту заниматься этим вопросом. А тот счастливчик, кому все же удалось продраться сквозь эти дебри и который вроде начал использовать объекты COM в своих программах, зачастую оказывается совершенно беспомощным вне своей привычной среды программирования.
В чем-то подобной этой ситуации оказался в свое время и автор этих строк, за тем разве исключением, что ему так и не удалось продраться через все эти услужливо придуманные высокоуровневые конструкции. Двигаясь вместо этого с противоположной стороны, я вдруг с удивлением обнаружил, что казавшаяся ранее неприступной преграда начинает отступать, если выкинуть изрядную долю отягощающего хлама. Проблема с COM не в том, что эта технология сложна сама по себе или что она использует какие-то неизвестные доселе принципы. Сами используемые элементы как раз просты и обыкновенны, но их много и они перемешаны со множеством других вещей, не имеющих отношения к COM, и все это приводит к тому, что суть COM теряется и растворяется в этой массе. В этой статье (вернее, серии из трех статей, в соответствии с названием) я попытаюсь описать мои собственные эксперименты и тот путь, который я прошел в надежде, что это может пригодиться кому-нибудь еще .
Предполагается, что читатель уже знаком или хотя бы слышал об основных концепциях COM. Здесь не будут описываться модели компонентного программирования, принципы инкапсуляции, полиморфизма, наследования и т.п. - разве что они будут упомянуты мимоходом. Вместо этого будет предложен подход с точки зрения низкоуровневого программирования, не отягощенного теоретическими абстракциями - своего рода "вид снизу" на архитектуру COM. Возможно, больше всего эти статьи подойдут тем, кто не раз начинал и бросал изучение COM, не справившись и изобилием разнородной информации, но все же не оставил желания разобраться в существе и механизмах этой технологии, а также тем, кто работает с COM на высоком уровне, но хотел бы заглянуть "за кулисы".
В качестве примера рассматриваются несколько утилит, созданных с использованием пакета MASM32. Предполагается, что читатель умеет работать с ним - вопросы создания графических Win32-приложений здесь вообще не будут рассматриваться. (Желающие могут обратиться к обучалкам Iczelion'а; в частности, хорошенько просмотрите раздел о диалоговых окнах - главы 10 и 11.) Более того, исходный код содержит некоторую мешанину - высокоуровневые конструкции MASM (.IF-.ELSE, INVOKE и т.д.) используются для "обыкновенного" каркаса Win32-приложения, а элементы COM реализованы на самом низком уровне - например, в качестве виртуальных таблиц используются даже не структуры (не говоря уже о макросах), а ряд последовательных значений. Это сделано намеренно, чтобы акцентировать внимание именно на имеющих отношение к COM деталях и дать возможность "почувствовать" их и "пощупать руками", по возможности не зацикливаясь на Win32-программировании вообще.
COM и реестр
Ну что ж, пора покончить со вступлениями и перейти к нашим китам. Одна из важнейших основ, которая активно используется и без которой невозможна сама инфраструктура COM, - это системная база данных. На платформе Windows в качестве таковой выступает системный реестр. Прежде чем двинуться дальше, придется все же сказать пару слов предупреждения.
Здесь будут описаны эксперименты с изменениями значений в реестре. Следует помнить, что эти операции нужно производить очень аккуратно, поскольку они чреваты большими неприятностями. При порче реестра компьютер перестанет загружаться, и информация на диске может оказаться утраченной. Поэтому настоятельно рекомендуется перед началом манипуляций с реестром создать аварийную загрузочную дискету (если она не была создана до этого), сделать резервную копию всех важных данных на диске (не забыв и про электронную почту с адресной книгой), а также скопировать файлы реестра (для Win9* - USER.DAT и SYSTEM.DAT в каталоге Windows) в отдельную папку. Сложнее обстоит дело с WinNT/2k/XP, установленным в раздел NTFS. Для WinXP нужно хотя бы создать дополнительную точку восстановления.
Это лишь чрезвычайный минимум сведений; рассказ о методах восстановления реестра сам потребовал бы отдельной статьи. Существует серия книг с названиями "Реестр Windows [98, NT, 2000, XP и др.]", в которых говорится и о методах восстановления реестра; в случае необходимости следует обратиться именно к ним.
Вернемся к нашим китам. Давайте посмотрим на несколько небольших примеров. Если на вашей системе установлен MS Excel, вы можете создать такой файл с расширением .vbs и запустить его:
Код (Text):
Set x = CreateObject("Excel.Application") x.Workbooks.Add x.Cells(1,1).Value = 5 x.Cells(1,2).Value = 10 x.Cells(1,3).Value = 15 x.Range("A1:C1").Select Set ch = x.Charts.Add() x.Visible = True ch.Type = -4100Если установлен Word XP, то можно запустить другой файл .vbs:
Код (Text):
Set w = CreateObject("Word.Application") w.Visible = True Set rng = w.Document.Add.Range(0,0) With rng .InsertBefore "Текст для WordXP" .ParagraphFormat.Alignment = 1 With .Font .Name = "Arial" .Size = 16 .Color = 200 End With End WithЕсли при установке MS Office был установлен элемент управления "Календарь", можно создать и просмотресть в браузере такой html-файл:
Код (Text):
<HTML> <BODY> <OBJECT CLASSID="CLSID:8E27C92B-1264-101C-8A2F-040224009C02"> </OBJECT> </BODY> </HTML>[Кстати, если этот элемент управления действительно установлен на вашем компьютере, вы можете увидеть его здесь под этим абзацем, если читаете статью в IE ]
Вы, наверное, обратили внимание на постоянное повторение "если". Это неотъемлемое свойство компонентного ПО: к сожалению, никогда нельзя быть на 100% уверенным в том, что нужный компонент будет присутствовать на платформе пользователя. К тому же, простого копирования файлов для запусков компонентов недостаточно. Программы должны быть установлены либо с использованием setup, либо отдельной регистрацией компонентов или созданием соответствующих записей в реестре вручную.
После этого становится возможным обращение к компонентам по именам. В нашем случае это были соответственно "Excel.Application", "Word.Application" и "CLSID:8E27C92B-1264-101C-8A2F-040224009C02". Вся эта и другая относящаяся к COM/OLE/ActiveX информация хранится в разделе реестра "HKEY_LOCAL_MACHINE\Software\CLASSES". Этот раздел настолько важен, что для него был создан алиас под видом отдельного корневого раздела реестра "HKEY_CLASSES_ROOT".
Рис. 1. Редактор реестра.Структура информации о COM в реестре
Теперь самое время запустить редактор реестра (набрав в командной строке "regedit"; см. рис. 1) и начать знакомиться с рассматриваемыми вопросами на практике. Относящиеся к COM данные объединяются в 5 групп, для большинства из которых предусмотрены соответствующие разделы реестра: ProgID, CLSID, TypeLib, Interface, AppID. В качестве раздела для ProgID выступает сам корневой раздел "HKEY_CLASSES_ROOT". Вот здесь-то мы и можем обнаружить те самые имена "Excel.Application", "Word.Application", которые мы использовали ранее в скриптах, наряду с расширениями файлов и оставшимися 4 разделами для COM.
ProgID - это так называемый программный идентификатор, он является на самом деле лишь "дружественной для пользователя" меткой "настоящего" имени компонента - CLSID. Считается, что ProgID не способен обеспечить подлинной глобальной уникальности имени, особенно в распределенной межсетевой среде, поэтому в последнем случае в качестве идентификаторов компонентов должны использоваться исключительно CLSID. Но на локальной машине, особенно в скриптах, ProgID широко используется. Однако в любом случае для идентификации компонента используется его CLSID; для этой цели между ключами CLSID и ProgID компонента установлены перекрестные ссылки.
Рассмотрим, например, ключ "Excel.Application". Он имеет подключ CLSID, значение по умолчанию которого и является искомым CLSID в строковом представлении. Это значит, что в разделе "HKEY_CLASSES_ROOT\CLSID" имеется соответствующий ключ, содержащий всю необходимую для загрузки компонента информацию. Наверняка вы обратили внимание и на то, что рядом с ключом "Excel.Application" имеется еще один такой же ключ, только с цифрой в конце (на разных машинах она может быть разной; у меня, например, "Excel.Application.5"); а сам ключ содержит также другой подключ - CurVer. На самом деле именно "Excel.Application.5" и является ProgID, а "Excel.Application" называется VersionIndependentProgID - "независимый от версии идентификатор прогаммы" (именно такие подключи для перекрестных ссылок вы найдете в разделе "HKEY_CLASSES_ROOT\CLSID" для соотвествующего компонента). Таким образом, в скрипте можно не указывать конкретную версию установленного компонента; а если со временем был сделан апгрейд, старые скрипты будут успешно работать с новыми версиями, если в качестве имен объектов они использовали VersionIndependentProgID. Для получения одного ключа из другого система COM предоставляет вспомогательные функции API CLSIDFromProgID и ProgIDFromCLSID.
Чтобы получше впитать в себя эту теорию, можно слегка похулиганить (не забывая, однако, сделанного ранее предупреждения). Убедимся, что "настоящее" имя компонента - это действительно CLSID. Для этого создадим новый ключ в разделе "HKEY_CLASSES_ROOT" и назовем его для разнообразия, скажем, собственным именем. Затем под этим ключем создаем подключ с именем "CLSID". Скопируем значение подключа CLSID для ProgID "Excel.Application" и вставим его в качестве значения по умолчанию нашего вновь созданного CLSID. А теперь слегка перепишем наш скрипт:
Код (Text):
Set x = CreateObject("MyName") x.Visible = TrueЕстественно, вместо "MyName" у вас должно быть то имя, которое вы выбрали для своего ProgID. Если все было сделано правильно, теперь Excel послушно "откликается" на новое имя.
Утилита для ProgID
Копаясь в реестре, можно обнаружить множество интересных вещей. Однако, ручной поиск по перекрестным ссылкам, особенно если компонентов очень много, весьма утомителен. Сочетая приятное с полезным, создадим для наших поисков небольшую утилиту, которая будет переводить данный ProgID компонента в его CLSID и наоборот.
Рис.2. Интерфейс утилиты COM_1Дизайн пользовательского интерфейса утилиты приведен на рис. 2. Он реализуется следующим файлом ресурсов (COM_1.rc):
Код (Text):
#define DS_MODALFRAME 0x80L #define DS_CENTER 0x0800L #define WS_POPUP 0x80000000L #define WS_CAPTION 0x00C00000L #define WS_SYSMENU 0x00080000L #define ES_AUTOHSCROLL 0x0080L #define IDC_EDIT1 1000 #define IDC_EDIT2 1001 #define IDB_ProgID 1002 #define IDB_CLSID 1003 #define IDC_STATIC -1 MyDialog DIALOG DISCARDABLE 200, 200, 200, 66 STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "ProgID <-> CLSID" FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "Get ProgID",IDB_ProgID,35,46,50,14 PUSHBUTTON "Get CLSID",IDB_CLSID,108,46,50,14 LTEXT "CLSID:",IDC_STATIC,7,7,26,12 LTEXT "ProgID:",IDC_STATIC,7,27,26,12 EDITTEXT IDC_EDIT1,38,7,154,12,ES_AUTOHSCROLL EDITTEXT IDC_EDIT2,38,27,154,12,ES_AUTOHSCROLL ENDИсходный код программы содержится в файле COM_1.asm. Начало стандартное; функции COM API содержатся в OLE32.dll, поэтому необходимо включить файлы для поддержки этого модуля.
Код (Text):
.386 .model flat,stdcall option casemap:none DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\ole32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\ole32.lib .data DlgName db "MyDialog",0 App db "ProgID_CLSID",0 ms1 db "Enter CLSID here",0 ms2 db "Enter ProgID here",0 ms3 db "There is no ProgID for this CLSID",0 ms4 db "There is no CLSID for this ProgID",0 Err1 db "The wrong CLSID",0 .data? hInstance HINSTANCE ? hEdit1 DWORD ? ; окно редактирования для CLSID hEdit2 DWORD ? ; окно редактирования для ProgID buf db 256 dup(?) ; для строк ANSI wbuf db 512 dup(?) ; для строк Unicode cls db 16 dup(?) ; буфер для CLSID bufaddr DWORD ? .const IDC_EDIT1 equ 1000 ; окно редактирования для CLSID IDC_EDIT2 equ 1001 ; окно редактирования для ProgID IDB_ProgID equ 1002 ; кнопка "Get ProgID" IDB_CLSID equ 1003 ; кнопка "Get CLSID"Утилита реализована в виде модального диалогового окна, создаваемого из шаблона ресурса:
Код (Text):
.code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL invoke ExitProcess,eax DlgProc proc USES esi edi ebx,hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_INITDIALOG ; сохранить описатели для окон редактирования invoke GetDlgItem,hWnd,IDC_EDIT1 mov hEdit1,eax invoke GetDlgItem,hWnd,IDC_EDIT2 mov hEdit2,eax invoke SetFocus,hEdit1 mov eax,FALSE ret .ELSEIF uMsg==WM_CLOSE invoke EndDialog,hWnd,0Алгоритм работы следующий. При нажатии кнопки "Get ProgID" окно для ProgID очищается и считывается текст окна для CLSID.
Код (Text):
.ELSEIF uMsg==WM_COMMAND mov eax,wParam mov edx,wParam shr edx,16 .if dx==BN_CLICKED .if ax==IDB_ProgID ; преобразовать CLSID в ProgID mov buf,0 invoke SetWindowText,hEdit2,ADDR buf invoke GetWindowText,hEdit1,ADDR buf,255Если текста нет - вывести сообщение с предложением его ввести:
Код (Text):
.if eax==0 ; GetWindowText - нет текста invoke MessageBox,0,ADDR ms1,ADDR App,0 invoke SendMessage,hEdit1,EM_SETSEL,0,-1 invoke SetFocus,hEdit1 .else ; текст полученЗдесь нас ожидает небольшой сюрприз - в COM используются строки в формате Unicode. Поэтому придется преобразовать текст в буфере buf с помощью функции MultiByteToWideChar. Эта функция принимает 6 параметров:
- кодовая страница преобразуемого текста (в нашем случае - ANSI, параметр CP_ACP);
- флаги, указывающие на способ преобразования сложных композитных символов; нам это не требуется - оставляем 0;
- адрес буфера с преобразуемой строкой;
- число символов в преобразуемой строке; если -1, строка заканчивается нулем и длина строки определяется автоматически;
- адрес буфера для преобразованной строки;
- размер буфера для преобразованной строки.
Код (Text):
invoke MultiByteToWideChar,CP_ACP,0,ADDR buf,-1,ADDR wbuf,510Еще одно затруднение. Функция ProgIDFromCLSID принимает в качестве аргумента адрес структуры с числовым представлением CLSID, а не с его строковым представлением. Подробнее этот нюанс мы обсудим в разделе о CLSID, а сейчас используем для преобразования строкового представления в числовой еще одну вспомогательную функцию COM API - CLSIDFromString (где cls - оставленный под структуру CLSID буфер размером 16 байт):
Код (Text):
invoke CLSIDFromString,ADDR wbuf,ADDR cls .if eax==NOERROR invoke ProgIDFromCLSID,ADDR cls,ADDR bufaddr .if eax==S_OKМы получили в bufaddr строковое представление ProgID в формате Unicode, теперь нужно преобразовать его обратно в ANSI с помощью функции WideCharToMultiByte ("напарницей" MultiByteToWideChar). Она принимает 8 аргументов:
- кодовая страница, в которую нужно преобразовать строку Unicode; в нашем случае - снова CP_ACP;
- флаги, указывающие способ обработки неотображаемых символов. Нам это не требуется, оставляем 0;
- адрес строки Unicode;
- число символов в строке Uinicode. Если -1, строка считается завершающейся нулями и ее длина определяется автоматически;
- адрес буфера для преобразованной строки;
- размер буфера для преобразованной строки;
- символ по умолчанию - нам не нужно, оставляем 0;
- флаги, указывающие на способ использования символа по умолчанию - 0.
Код (Text):
invoke WideCharToMultiByte,CP_ACP,0,bufaddr,-1,ADDR buf,255,0,0 invoke SetWindowText,hEdit2,ADDR buf .else ; не удалось преобразовать CLSID в ProgID - ; выдать соответствующее сообщение invoke SetWindowText,hEdit2,ADDR ms3 .endifДальше - блок обработки ошибки функции CLSIDFromString, что рассматривается как неправильный формат введенной строки CLSID:
Код (Text):
.else ; ошибка CLSIDFromString invoke MessageBox,0,ADDR Err1,ADDR App,MB_OK OR MB_ICONERROR invoke SendMessage,hEdit1,EM_SETSEL,0,-1 invoke SetFocus,hEdit1 .endif ;NOERROR - CLSIDFromString .endif ;GetWindowTextБлок обработки нажатия на кнопку "Get ProgID" завершен. Сходным же образом обрабатывается нажатие кнопки "Get CLSID".
Код (Text):
.elseif ax==IDB_CLSID ;преобразовать ProgID в строку CLSID mov buf,0 invoke SetWindowText,hEdit1,ADDR buf invoke GetWindowText,hEdit2,ADDR buf,255 .if eax==0 ; нет текста в окне - сообщение об ошибке invoke MessageBox,0,ADDR ms2,ADDR App,0 invoke SendMessage,hEdit2,EM_SETSEL,0,-1 invoke SetFocus,hEdit2 .else ; текст ProgID получен - преобразовать в Unicode invoke MultiByteToWideChar,CP_ACP,0,ADDR buf,-1,ADDR wbuf,510 ; преобразовать ProgID в числовую форму CLSID invoke CLSIDFromProgID,ADDR wbuf,ADDR cls .if eax==S_OK ; CLSID получено, ; преобразовать в строковую форму invoke StringFromCLSID,ADDR cls,ADDR bufaddr ; преобразовать строку Unicode в ANSI invoke WideCharToMultiByte,CP_ACP,0,bufaddr,-1, ADDR buf,255,0,0 invoke CoTaskMemFree,bufaddr ; освободить память, ; выделенную при вызове StringFromCLSID invoke SetWindowText,hEdit1,ADDR buf .else ; не удалось получить CLSID из ProgID invoke SetWindowText,hEdit1,ADDR ms4 .endif ;S_OK (CLSIDFromProgID) .endif ;GetWindowText .endif ;ax==IDB_ProgIDНа этом обработка сообщений диалогового окна заканчивается:
Код (Text):
.endif ;BN_CLICKED .ELSE mov eax,FALSE ret .ENDIF ;uMsg mov eax,TRUE ret DlgProc endp end startФайлы с исходным кодом данной утилиты (COM_1.rc и COM_1.asm, а также makefile) находятся в каталоге COM_1 архива ComKit1.rar. Makefile написан в предположении, что пакет MASM32 установлен в каталоге \masm32 на текущем диске; если это не так, необходимо его отредактировать.
Раздел CLSID
Вся основная информация о компонентах размещается в разделе реестра "HKEY_CLASSES_ROOT\CLSID". Для каждого компонента создается отдельный ключ, в качестве имени которого выступает строковое представление его CLSID. Как отмечалось выше, CLSID (как и другие виды GUID, например, идентификаторы интерфейсов (IID) или библиотеки типов) имеет две формы представления: числовую и строковую. Числовая форма представляет собой 128-битное значение. В принципе это значение можно было бы разместить, скажем, в регистре XMM; однако COM создавалась еще во времена 16-разрядных машин, когда ни о каких XMM-регистрах и не помышляли. Поэтому для представления этого 128-битного значения была использована структура следующего вида:
Код (Text):
GUID STRUCT Data1 dd ? Data2 dw ? Data3 dw ? Data4 db 8 dup(?) GUID ENDSВ реестре же для представления GUID используется строка такого формата:
Код (Text):
{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}где х обозначает 16-ричную цифру (0-F), а фигурные скобки и дефисы должны присутствовать на своих местах без всяких разделяющих пробелов. На самом деле, эта запись может послужить источником путаницы: обратите внимание, что блок цифр для поля Data4 в строковой записи разделен на две части в 4 и 12 шестнадцатеричных цифр, и часть в 4 цифры можно спутать с предыдущими WORD-значениями. Первое DWORD и два последующих WORD значения записываются в строковом представлении, как обычные числа, т.е. старшие байты идут вначале, тогда как третий "WORD" (первые 2 байта поля Data4) записывается в порядке "little-endian" - вначале идет младший байт.
Как мы уже видели в коде первой утилиты, для преобразования строковой формы представления CLSID в числовую и обратно используются соотвтественно функции CLSIDFromString и StringFromCLSID. Существуют варианты и для других типов GUID - StringFromIID, IIDFromString, StringFromGUID2.
Ключ компонента CLSID содержит, в свою очередь, множество подключей. С двумя из них - ProgID и VersionIndependentProgID - мы уже познакомились. Стоит упомянуть лишь о том, что у компонента не обязательно должно присутствовать ProgID.
Простой универсальный клиент COM
Прежде, чем двинуться дальше в изучении раздела CLSID, нам понадобится еще одна небольшая утилита - универсальный клиент COM. Пользовательский интерфейс ее приведен на рис. 3. Этот клиент позволяет загружать объект COM, CLSID которого вводится в первое окно редактирования, и запрашивает у него указатель на интерфейс, IID которого содержится во втором окне редактирования. Тип сервера указывается набором флажков опций (одновременно может быть указано несколько типов). Кнопка IUnknown позволяет отобразить во втором окне редактирования IID часто употребляемого интерфейса с тем же названием.
Рис.3. Интерфейс утилиты COM_2Вот соответствующий файл ресурсов (COM_2.rc):
Код (Text):
#include "\masm32\include\resource.h" #define IDC_EDIT1 1001 #define IDC_EDIT2 1002 #define IDC_CHECK1 1003 #define IDC_CHECK2 1004 #define IDC_CHECK3 1005 #define IDC_CHECK4 1006 #define IDC_BUTTON1 1007 #define IDC_STATIC -1 MyDialog DIALOGEX 0, 0, 214, 90 STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Simple COM client" FONT 8, "MS Sans Serif" BEGIN LTEXT "CLSID:",IDC_STATIC,7,10,26,8,0,WS_EX_RIGHT LTEXT "IID:",IDC_STATIC,7,28,26,8,0,WS_EX_RIGHT GROUPBOX "Server type:",IDC_STATIC,7,45,142,38 EDITTEXT IDC_EDIT1,40,7,167,14,ES_AUTOHSCROLL EDITTEXT IDC_EDIT2,40,26,167,14,ES_AUTOHSCROLL CONTROL "InProc server",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,55,58,10 CONTROL "InProc handler",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,68,62,10 CONTROL "Local server",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,80,55,55,10 CONTROL "Remoute server",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,80,68,66,10 PUSHBUTTON "IUnknown",IDC_BUTTON1,157,49,50,14 DEFPUSHBUTTON "Connect",IDOK,157,67,50,14 ENDЧтобы не выписывать каждый раз стандартные define'ы, целесообразно скопировать файл "resource.h" из 10 урока обучалки Iczelion'а в каталог "Include" пакета MASM32, а затем вставлять в rc-файлы соответствующую ссылку, как это сделано в данном случае.
Реализация в файле COM_2.asm:
Код (Text):
.386 .model flat,stdcall option casemap:none DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\ole32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\ole32.lib .data DlgName db "MyDialog",0 app db "SimpleClient",0 ms1 db "Enter CLSID here",0 ms2 db "Enter IID here",0 szUnk db "{00000000-0000-0000-C000-000000000046}",0 mscheck db "The server type must be indicated",0 msclsid db "Enter CLSID here",0 msiid db "Enter IID here",0 msok db "Got interface pointer to:",13,10,"CLSID:",9,"%s",13,10,"IID:",9,"%s",0 mserr db "CoCreateInstance failed:",13,10,"HRESULT==%Xh",0 err1 db "The wrong CLSID",0 err2 db "The wrong IID",0 .data? hInstance HINSTANCE ? hEdit1 DWORD ? ; "CLSID" hEdit2 DWORD ? ; "IID" hCheck1 DWORD ? ; "Inproc server" hCheck2 DWORD ? ; "Inproc handler" hCheck3 DWORD ? ; "Local server" hCheck4 DWORD ? ; "Remoute server" ctx DWORD ? ; тип сервера szclsid db 64 dup(?) ; буфер для строки с CLSID sziid db 64 dup(?) ; буфер для строки с IID buf db 256 dup(?) ; для ANSI строк wbuf db 512 dup(?) ; для строк Unicode cls db 16 dup(?) ; CLSID компонента iid db 16 dup(?) ; IID вызываемого интерфейса pUnk DWORD ? ; указатель на интерфейс IUnknown .const IDC_EDIT1 equ 1001 ; "CLSID" IDC_EDIT2 equ 1002 ; "IID" IDC_CHECK1 equ 1003 ; "Inproc server" IDC_CHECK2 equ 1004 ; "Inproc handler" IDC_CHECK3 equ 1005 ; "Local server" IDC_CHECK4 equ 1006 ; "Remoute server" IDC_BUTTON1 equ 1007 ; "IUnknown" .code start: invoke GetModuleHandle, NULL mov hInstance,eax invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL invoke ExitProcess,eax DlgProc proc USES esi edi ebx,hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_INITDIALOG ; сохраняем описатели элементов управления invoke GetDlgItem,hWnd,IDC_EDIT1 mov hEdit1,eax invoke GetDlgItem,hWnd,IDC_EDIT2 mov hEdit2,eax invoke GetDlgItem,hWnd,IDC_CHECK1 mov hCheck1,eax invoke GetDlgItem,hWnd,IDC_CHECK2 mov hCheck2,eax invoke GetDlgItem,hWnd,IDC_CHECK3 mov hCheck3,eax invoke GetDlgItem,hWnd,IDC_CHECK4 mov hCheck4,eax invoke SetFocus,hEdit1Поскольку на этот раз нам необходимо задействовать библиотеку COM, нужно также вызвать функцию CoInitialize. При завершении программы необходимо соответственно вызвать функцию CoUninitialize.
Код (Text):
invoke CoInitialize,0 mov eax,FALSE ret .ELSEIF uMsg==WM_CLOSE invoke CoUninitialize invoke EndDialog,hWnd,0"Ядром" данной утилиты является использование функции CoCreateInstance, которая делает всю работу - локализует соответствующий объект COM, загружает его, запрашивает указанный интерфейс и возвращает указатель на него. Значительная часть кода обработки сообщения WM_COMMAND сводится к сбору данных для передачи в качестве параметров функции CoCreateInstance и проверке введенных значений. CoCreateInstance принимает 5 аргументов:
- адрес структуры, содержащей CLSID запрашиваемого объекта (мы получаем это значение в первом окне редактирования);
- указатель на так называемый внешний IUnknown, если объект агрегируется в состав другого объекта. Если агрегирование не применяется (как в нашем случае), значение аргумента равно 0;
- контекст создания объекта: флаги, указывающие на то, является ли сервер объекта внутрипроцессным, локальным, удаленным или это внутрипроцессный обработчик (данный флаг мы вычисляем на основе проверки состояния флажков опций);
- адрес структуры с IID интерфейса, указатель на который мы хотим получить. Значение IID мы получаем во втором окне редактирования;
- адрес переменной, в которой будет возвращен указатель на запрашиваемый нами интерфейс (или 0, если этот интерфейс не поддерживается).
При нажатии на кнопку IUnknown во второе окно редактирования копируется IID для соответствующего интерфейса (это значение "вшито" в код в виде строки szUnk). Итак, обработка команд:
Код (Text):
.ELSEIF uMsg==WM_COMMAND mov eax,wParam mov edx,wParam shr edx,16 .if dx==BN_CLICKED .if ax==IDC_BUTTON1 ; кнопка 'IUnknown' invoke SetWindowText,hEdit2,offset szUnk .elseif ax==IDOK ; кнопка 'Connect' ; получить CLSID invoke GetWindowText,hEdit1,offset szclsid,64 .if eax==0 ; текст отсутствует - сообщение invoke MessageBox,0,offset msclsid,offset app,MB_ICONEXCLAMATION invoke SendMessage,hEdit1,EM_SETSEL,0,-1 invoke SetFocus,hEdit1 mov eax,TRUE ret .else ; текст получен - перевести в ANSI и ; преобразовать в числовую форму, сохранив в cls invoke MultiByteToWideChar,CP_ACP,0,offset szclsid,-1,offset wbuf,510 invoke CLSIDFromString,offset wbuf,offset cls .if eax!=NOERROR ; ошибка CLSIDFromString рассматривается ; как неверный формат введенной строки с CLSID invoke MessageBox,0,offset err1,offset app,MB_ICONERROR invoke SendMessage,hEdit1,EM_SETSEL,0,-1 invoke SetFocus,hEdit1 mov eax,TRUE ret .endif ;NOERROR - CLSIDFromString .endif ; GetWindowText (hEdit1) ; получить IID invoke GetWindowText,hEdit2,offset sziid,64 .if eax==0 ; текста нет - сообщение invoke MessageBox,0,offset msiid,offset app,MB_ICONEXCLAMATION invoke SendMessage,hEdit2,EM_SETSEL,0,-1 invoke SetFocus,hEdit2 mov eax,TRUE ret .else ; текст с IID получен, преобразовать в ANSI, ; затем в числовую форму, сохранив ее в iid invoke MultiByteToWideChar,CP_ACP,0,offset sziid,-1,offset wbuf,510 invoke IIDFromString,offset wbuf,offset iid .if eax!=NOERROR ; ошибка IIDFromString рассматривается ; как неверный формат введенной строки с IID invoke MessageBox,0,offset err2,offset app,MB_ICONERROR invoke SendMessage,hEdit2,EM_SETSEL,0,-1 invoke SetFocus,hEdit2 mov eax,TRUE ret .endif ;NOERROR - IIDFromString .endif ;GetWindowText (hEdit2) ; проверить состояние флажков опций и определить тип сервера, ; сохранив полученное значение в ctx mov ctx,0 invoke IsDlgButtonChecked,hWnd,IDC_CHECK1 .if eax==BST_CHECKED or ctx,CLSCTX_INPROC_SERVER .endif invoke IsDlgButtonChecked,hWnd,IDC_CHECK2 .if eax==BST_CHECKED or ctx,CLSCTX_INPROC_HANDLER .endif invoke IsDlgButtonChecked,hWnd,IDC_CHECK3 .if eax==BST_CHECKED or ctx,CLSCTX_LOCAL_SERVER .endif invoke IsDlgButtonChecked,hWnd,IDC_CHECK4 .if eax==BST_CHECKED or ctx,CLSCTX_REMOTE_SERVER .endif .if ctx==0 ; не выбран ни один тип сервера - сообщение invoke MessageBox,0,offset mscheck,offset app,MB_ICONEXCLAMATION invoke SetFocus,hCheck1 mov eax,TRUE ret .endifТеперь необходимые параметры собраны: CLSID находится в cls, IID - в iid, тип сервера - в ctx. Функция CoCreateInstance находит объект COM с данным CLSID и создает его экземпляр, запрашивая у него указатель на интерфейс IID. Если объект реализует интерфейс с данным IID, он помещает указатель на него в переменную, адрес которой был передан CoCreateInstance в последнем параметре, а сама функция возвращает S_OK. Если объект не поддерживает данный интерфейс или объект с данным CLSID не зарегистрирован в реестре, или зарегистрированный тип сервера не указан в флаге типа сервера, возвращается сообщение об ошибке.
Код (Text):
invoke CoCreateInstance,offset cls,0,ctx,offset iid,offset pUnk mov ecx,eax .if eax==S_OK ; Объект успешно создан и указатель на затребованный ; интерфейс возвращен в pUnk. Выводим сообщение с ; указанием CLSID объекта и IID интерфейса invoke wsprintf,offset buf,offset msok,offset szclsid,offset sziid invoke MessageBox,0,offset buf,offset app,MB_ICONINFORMATIONНа этом вся "полезная" работа нашего приложения завершается - мы освобождаем объект. Делать это придется уже средствами COM, т.е. с использованием полученного указателя интерфейса. Для этого через полученный указатель интерфейса (pUnk) необходимо вызвать метод Release интерфейса IUnknown (абсолютно все интерфейсы COM наследуются от IUnknown, поэтому метод Release можно вызвать для любого объекта COM одним и тем же способом).
Указатель на интерфейс объекта представляет собой адрес некоей структуры, в которой содержатся данные с состоянием экземпляра объекта. Доступ к ним возможен лишь косвенно, посредством вызова методов соответствующего интерфейса, поэтому каждый метод получает в качестве первого параметра адрес этой структуры (т.н. указатель "this", в терминологии языка С++). Об этой структуре можно сказать лишь то, что первое ее поле является указателем на другую структуру - "виртуальную таблицу", содержащую адреса процедур, реализующих методы интерфейса. Для вызова того или иного метода необходимо найти адрес его реализации по заранее известному и постоянному смещению в виртуальной таблице. Подробнее "кухню" этого вопроса мы разберем во второй статье, а пока просто приведем код:
Код (Text):
mov eax,pUnk push eax ; указатель на экземпляр объекта ("this") mov eax,[eax] ; получаем указатель на виртуальную таблицу, call dword ptr [eax+8] ; затем - указатель на третью функцию ; в таблице (Release) и вызываем ее .else ; ошибка CoCreateInstance invoke wsprintf,offset buf,offset mserr,ecx invoke MessageBox,0,offset buf,offset app,MB_ICONERROR .endif ; eax==S_OK (CoCreateInstance)На этом обработка WM_COMMAND и любых других сообщений закончена.
Код (Text):
.endif ;ax==IDOK .endif ;BN_CLICKED .ELSE ; uMsg не обрабатывается mov eax,FALSE ret .ENDIF ; uMsg mov eax,TRUE ret DlgProc endp end startПодключи раздела CLSID
Теперь можно продолжить эксперименты с реестром. Важнейшими подключами раздела "HKEY_CLASSES_ROOT\CLSID" является ряд подключей, позволяющих определить местонахождение сервера в системе: InprocServer, InprocServer32, InprocHandler, InprocHandler32, LocalServer, LocalServer32. Каждый тип сервера представлен двумя подключами; суффикс "32" означает, что сервер 32-разрядный, подключ без этого суффикса указывает на то, что сервер 16-разрядный (для совместимости со старыми компонентами). Случай удаленного (remoute) сервера обрабатывается особым образом; об этом мы поговорим в разделе, посвященном AppID.
Начнем хулиганить. При создании новых интерфейсов и компонентов для получения уникальных GUID используются специальные утилиты типа guidgen и т.п. Microsoft зарезервировала для собственного употребления большой интервал удобных GUID-ов с наборами нулей; мы же поступим еще круче - используем GUID'ы типа {00000000-0000-0000-0000-000000000001}. Зачем нам париться, выискивая свой компонент среди массы цифр, когда можно удобно разместить его в самом начале раздела? Можно было бы (и вначале автор так и поступал) использовать и GUID со всеми нулями, но это специальный зарезервированный GUID_NULL, употребляющийся в особых случаях; во избежание неприятностей лучше оставить его в покое.
Создадим в разделе "HKEY_CLASSES_ROOT\CLSID" ключ {00000000-0000-0000-0000-000000000001}. Этот ключ может иметь значение по умолчанию, которое является описанием компонента, предназначенным для пользователя. Обратите внимание, это не то же самое, что ProgID (хотя некоторые авторы компонентов используют в качестве описания ProgID). Можно вставить сюда какую-нибудь строку, например, "Мой любимый компонент". Далее, создаем подключ с именем "LocalServer32". А вот значение по умолчанию этого подключа как раз и содержит полный путь к файлу сервера. Запишем сюда "c:\windows\calc.exe" (нужно указать каталог windows на вашей машине). Теперь запустим наш клиент (COM_2.exe), наберем CLSID, нажмем на IUnknown, в качестве сервера выбираем "Local server", жмем "Connect" и... Калькулятор можно закрыть. А наш клиент, похоже, завис - но к этому надо было быть готовым, поскольку калькулятор понятия не имеет о том, что он вдруг стал компонентом COM; тогда как система COM все еще дожидается от него ответа - наш клиент в это время "сидит внутри" CoCreateInstance. Впрочем, примерно через минуту она вернет ошибку таймаута, и наш клиент выдаст соответствующее окно сообщения.
Я не буду описывать здесь все прочие эксперименты - путь указан, утилиты есть; экспериментируйте! Технология COM постигается лишь на практике. Только не забудьте в пылу увлечения предупреждение об аккуратности работы с реестром.
Рассмотрим другие подключи. Важными являются TreatAs и AutoTreatAs, поскольку они позволяют сменить сервер компонента. Если в ключе с данным CLSID имеется подключ TreatAs (значение по умолчанию которого содержит другой CLSID), то будет создан объект с этим другим CLSID, независимо от наличия подключей InprocServer, LocalServer и т.д. Проделаем такой эксперимент. Создадим подключ "TreatAs" в нашем ключе {00000000-0000-0000-0000-000000000001}. В качестве значения вставим CLSID для компонента Excel.Application (воспользовавшись для преобразования ProgID утилитой COM_1.exe). Если теперь с помощью COM_2.exe попытаться загрузить наш компонент, будет загружен Excel (правда, он останется скрытым. Чтобы убедиться, что он запущен, придется посмотреть в менеджере задач).
Подключ TreatAs позволяет осуществлять эмуляцию одного сервера другим. Функция CoGetTreatAsClass позволяет получить значение этого ключа, а функция CoTreatAsClass - установить (или удалить) его. Если подключ TreatAs удаляется с помощью функции CoTreatAsClass, эта функция проверяет значение подключа AutoTreatAs. Если ключ с таким именем существует, его значение копируется в подключ TreatAs; если нет, подключ TreatAs вообще удаляется. Подключ же AutoTreatAs можно создавать или удалять только вручную, с использованием функций API реестра. Таким образом, в подключе AutoTreatAs может сохраняться значение предыдущего CLSID, если данный сервер эмулируется сначала одним, а затем другим сервером, обеспечивая своего рода "постоянную" эмуляцию (в отличие от "временной" эмуляции с помощью подключа TreatAs).
Будучи основным источником сведений о компоненте, раздел "HKEY_CLASSES_ROOT\CLSID" содержит подключи с перекрестными ссылками на другие разделы. О подключах ProgID и VersionIndependentProgID мы уже говорили. Подключ TypeLib содержит GUID, который является идентификатором библиотеки типов для данного компонента; соответствующий подключ содержится в разделе "HKEY_CLASSES_ROOT\TypeLib". Подключ AppID содержит GUID, идентифицирующий подключ другого раздела - "HKEY_CLASSES_ROOT\AppID", и указывает на то, что объект является распределенным (DCOM). Подробнее об этих подключах будет сказано в соответствующих разделах.
В разделе CLSID имеется еще множество других подключей; мы не будем рассматривать их все. Упомянем лишь о тех, которые являются своего рода флагами, указывающими на тип компонента. Наличие подключа "Control" указывает, что компонент является элементом управления; "Insertable" - что объект может внедряться в контейнеры OLE; "OLEScript" - объект является исполнителем сценариев (scripting engine); "DocObject" - объект-документ; "Printable" - объект может быть распечатан; "Programmable" - объект является сервером автоматизации (и, соответственно, им можно управлять с помощью скриптов); "Ole1Class" - объект OLE 1.0 (старой версии).
Утилита для просмотра типов объектов
Создадим еще одну небольшую утилиту, которая будет отображать список зарегистрированных в реестре объектов, относящихся к одному из 7 перечисленных в конце предыдущего параграфа типов. Внешний вид приложения показан на рис. 4. Тип объекта можно выбрать в выпадающем списке; после нажатия кнопки "List" в окне списка отображаются описания соответствующих объектов (значения по умолчанию, связанные с тем или иным CLSID). Если описания нет, строка останется пустой.
Рис.4. Интерфейс утилиты COM_3Для экономии места мы не будем приводить здесь полный код приложения; соответствующие файлы находятся в каталоге COM_3 архива ComKit1.rar. Вместо этого рассмотрим лишь некоторые существенные моменты, имеющие отношение к логике работы программы.
Код (Text):
invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey1,0,KEY_ALL_ACCESS,ADDR HKey .if eax==ERROR_SUCCESSНачинаем с того, что открываем раздел реестра "HKEY_CLASSES_ROOT\CLSID", сохраняя описатель в переменной HKey. При успешном открытии начинается основной цикл по перечислению имеющихся в открытом разделе ключей.
Код (Text):
EnumLoop: mov sbksz,255 invoke RegEnumKeyEx,HKey,idx,ADDR SubKeyBuf,ADDR sbksz,0,0,0,0 .if eax==ERROR_NO_MORE_ITEMS jmp OutLoop .elseif eax!=ERROR_SUCCESS invoke MessageBox,0,ADDR Err2,ADDR App,MB_OK OR MB_ICONERROR jmp OutLoop .endifПри любой ошибке перечисления цикл прерывается, причем если ошибка не связана с просмотром всех ключей, выводится соответствующее сообщение. Для каждого нового найденного ключа составляются две строки вида: "CLSID\{clsid компонента}" и "CLSID\{clsid компонента}\<тип объекта>", где <тип объекта> означает одну из 7 строк с типом объекта, выбранную из выпадающего списка.
Код (Text):
invoke lstrcpy,ADDR SubKey2,ADDR SubKey1 invoke lstrcpy,ADDR SubKey3,ADDR SubKey1 invoke lstrcat,ADDR SubKey2,ADDR s invoke lstrcat,ADDR SubKey3,ADDR s invoke lstrcat,ADDR SubKey2,ADDR SubKeyBuf invoke lstrcat,ADDR SubKey3,ADDR SubKeyBuf invoke lstrcat,ADDR SubKey3,ADDR s invoke lstrcat,ADDR SubKey3,ADDR findbuf invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey3,0, KEY_ALL_ACCESS,ADDR HKey2 .if eax==ERROR_SUCCESSЗатем делается попытка открыть ключ реестра "CLSID\{clsid компонента}\<тип объекта>". Успешная попытка означает, что ключ с данным CLSID имеет подключ с выбранным нами именем; в этом случае открываем другой подготовленный ключ ("CLSID\{clsid компонента}") и запрашиваем значение по умолчанию, которое пересылаем в окно списка и переходим к новой итерации:
Код (Text):
invoke RegCloseKey,HKey2 invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey2,0, KEY_ALL_ACCESS,ADDR HKey3 invoke RegQueryValueEx,HKey3,0,0,0,ADDR namebuf,ADDR bsz invoke RegCloseKey,HKey3 invoke SendMessage,hLst,LB_ADDSTRING,0,ADDR namebuf .endif ;RegOpenKeyEx (2) inc idx jmp EnumLoop OutLoop: invoke RegCloseKey,HKeyРаздел Interface
В отличие от остальных разделов, "HKEY_CLASSES_ROOT\Interface" относится не к отдельным компонентам, а к системе в целом. Приведенные здесь данные об интерфейсах используются при стандартном маршалинге. Подключей немного: BaseInterface, NumMethods, ProxyStubCLSID и ProxyStubCLSID32. BaseInterface содержит имя интерфейса, от которого унаследован данный интерфейс; если он не указан, в качестве базового принимается IUnknown. NumMethods содержит число методов в интерфейсе. ProxyStubCLSID(32) подобен InprocServer(32): он содержит полный путь к серверу (dll), реализующему вспомогательные объекты (т.н. представители и заглушки), которые используются системой при маршалинге методов данного интерфейса. Как обычно, суффикс "32" в имени означает 32-разрядный сервер, его отсутствие - 16-разрядный.
Подробное обсуждение маршалинга и связанных с ним проблем выходит за рамки данной статьи; кое-что будет рассказано во второй статье, а здесь приведем описание полезной утилиты, использующей данный раздел реестра. Внешний вид ее приведен на рис. 5. При нажатии кнопки "List" приложение создает экземпляр COM-объекта, CLSID которого указан в окне редактирования. Затем у созданного объекта запрашиваются все возможные интерфейсы, сведения о которых имеются в системном реестре. В окне списка выводятся имена всех интерфейсов, которые реализованы объектом.
Рис.5. Интерфейс утилиты COM_4Здесь опять приведем лишь ключевые моменты кода, исходные файлы содержатся в каталоге COM_4 архива ComKit1.rar.
Поскольку приложение использует библиотеку COM, при инициализации диалогового окна вызывается процедура CoInitialize, а при его закрытии - соответственно CoUninitialize. При нажатии кнопки "List" окно списка очищается, а из окна редактирования извлекается CLSID уже знакомым нам способом:
Код (Text):
invoke GetWindowText,hEd,ADDR buffer,255 invoke MultiByteToWideChar,CP_ACP,0,ADDR buffer,511,ADDR wbuf,255 invoke CLSIDFromString,ADDR wbuf,ADDR clsЗатем создается экземпляр объекта с данным CLSID; в качестве начального интерфейса запрашивается IUnknown (его IID содержится в IUnk), а контекст объекта допускает любой тип сервера:
Код (Text):
invoke CoCreateInstance,ADDR cls,0, CLSCTX_SERVER,ADDR IUnk,ADDR pUnkЕсли указатель успешно получен (в pUnk), открываем раздел реестра "HKEY_CLASSES_ROOT\Interface" и входим в главный цикл перечисления подключей (т.е. интерфейсов):
Код (Text):
invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey1,0, KEY_ALL_ACCESS,ADDR HKey .if eax==ERROR_SUCCESS Enum1: mov sbksz,255 invoke RegEnumKeyEx,HKey,idx,ADDR SubKeyBuf,ADDR sbksz,0,0,0,0 .if eax==ERROR_NO_MORE_ITEMS jmp Ex_2 .elseif eax!=ERROR_SUCCESS invoke MessageBox,0,ADDR Err2,ADDR App,MB_OK OR MB_ICONERROR jmp Ex_2Выход из цикла осуществляется, когда функция перечисления возвращает ошибку; причем если это не связано с отсутствием ключей для дальнейшего перечисления, отображается сообщение об ошибке. Каждый новый ключ перечисления (IID интерфейса в строковом виде) преобразуется в числовой вид (если в ходе преобразования происходит ошибка, данный ключ просто игнорируется и цикл продолжается).
Код (Text):
.else invoke MultiByteToWideChar,CP_ACP,0,ADDR SubKeyBuf,255,ADDR wbuf,255 invoke CLSIDFromString,ADDR wbuf,ADDR cls .if eax!=NOERROR jmp Cont1 .endifА вот теперь нужно запросить у нашего объекта интерфейс, IID которого мы только что получили. Для этого необходимо вызвать метод QueryInterface интерфейса IUnknown, со следующими аргументами:
- указатель на экземпляр текущего объекта ("this");
- адрес структуры с IID запрашиваемого интерфейса;
- адрес переменной, в которой будет возвращен указатель затребованного интерфейса.
Адрес метода QueryInterface располагается в самом начале виртуальной таблицы (со смещением 0):
Код (Text):
lea eax,pItfc ; pItfc получит указатель нового интерфейса push eax lea eax,cls ; требуемый IID находится в cls push eax mov eax,pUnk ; указатель на интерфейс IUnknown ; нашего объекта ("this") push eax mov eax,[eax] ; получаем адрес виртуальной таблицы call dword ptr [eax] ; вызываем первую функцию ; из виртуальной таблицы (QueryInterface)В случае успешного получения указателя нового интерфейса QueryInterface возвращает S_OK. Нам нужен просто факт поддержки объектом данного интерфейса, он сам не нужен; поэтому мы сразу же освобождаем только что полученный указатель, вызвав через него метод Release:
Код (Text):
.if eax==S_OK mov eax,pItfc ; указатель на затребованный интерфейс push eax ; помещаем в качестве 1-го (и единственного) пар-ра; mov eax,[eax] ; адрес виртуальной таблицы call dword ptr [eax+8] ; вызываем 3-й метод из в.табл. (Release)Одновременно с этим создаем строку вида "Interface\{IID интерфейса}" и открываем соответствующий раздел реестра, запрашивая значение по умолчанию этого раздела (имя интерфейса). Это имя добавляется в окно списка.
Код (Text):
invoke lstrcpy,ADDR SubKey2,ADDR SubKey1 invoke lstrcat,ADDR SubKey2,ADDR s invoke lstrcat,ADDR SubKey2,ADDR SubKeyBuf invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey2,0, KEY_ALL_ACCESS,ADDR HKey2 .if eax==ERROR_SUCCESS mov bsz,255 invoke RegQueryValueEx,HKey2,0,0,0,ADDR namebuf,ADDR bsz .if eax!=ERROR_SUCCESS invoke MessageBox,0,ADDR Err5,ADDR App,MB_OK OR MB_ICONERROR .endif invoke RegCloseKey,HKey2 invoke SendMessage,hLst,LB_ADDSTRING,0,ADDR namebuf .endif ;RegOpenKeyEx (2) .endif ;S_OK (QueryIntervace)В случае, когда QueryInterface или RegOpenKeyEx возвращают код ошибки, данный ключ просто игнорируется, а цикл продолжается дальше.
Код (Text):
Cont1: inc idx jmp Enum1 .endif ;RegEnumKeyEx Ex_2: invoke RegCloseKey,HKeyПосле выхода из цикла метод Release вызывается также через указатель pUnk интерфейса IUnknown, полученный нами при создании экземпляра объекта (аналогично тому, как это было сделано ранее).
Раздел TypeLib
В разделе "HKEY_CLASSES_ROOT\TypeLib" содержатся ключи, имена которых представляют собой строковую форму идентификаторов (GUID) установленных в системе библиотек типов. На данные ключи ссылаются подключи TypeLib компонентов раздела "HKEY_CLASSES_ROOT\CLSID", но обратных ссылок нет. Библиотеки типов обязательны для компонентов, использующих позднее связывание и автоматизацию; в частности, они широко применяются с элементами управления ActiveX. Остальные компоненты могут и не иметь библиотеки типов.
Каждый ключ раздела TypeLib содержит иерархическую структуру подключей. На первом уровне находятся подключи версии библиотеки типов, представленные в строковой форме major.minor (цифры, соответствующие версии). В разделе версии находятся, в свою очередь, подключи HelpDir (полный путь к файлу справки), Flags (флаг библиотеки типов) и строковое представление локального идентификатора языка (lcid). Ключ <lcid> содержит еще один или два подключа: win16 и/или win32 - полные пути к библиотеке типов для соответствующей платформы.
В качестве примера работы с библиотекой типов рассмотрим еще одно приложение (рис. 6). Диалоговое окно приложения имеет всего один элемент управления - окно просмотра списков. В первом столбце отображаются описания компонентов, а рядом с ними во втором - идентификаторы соответствующих библиотек типов. При двойном щелчке мышью на названии компонента либо отображается общая информация о данном компоненте, содержащаяся в его библиотеке типов, либо (если имеется) открывается файл справки, связанный с данным компонентом.
Рис.6. Интерфейс утилиты COM_5Приложение работает следующим образом (детали реализации графического интерфейса опущены; полный исходный код содержится в каталоге COM_5 архива ComKit1.rar). При обработке сообщения WM_INITDIALOG открываем раздел реестра "HKEY_CLASSES_ROOT\CLSID", затем входим в главный цикл перечисления содержащихся в нем ключей:
Код (Text):
invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey1,0,KEY_ALL_ACCESS,ADDR HKey .if eax==ERROR_SUCCESS EnumLoop: lea esi,buf mov sbksz,255 invoke RegEnumKeyEx,HKey,idx,ADDR SubKeyBuf,ADDR sbksz,0,0,0,0 .if eax==ERROR_NO_MORE_ITEMS jmp OutLoop .elseif eax!=ERROR_SUCCESS invoke MessageBox,0,ADDR Err2,ADDR App,MB_OK OR MB_ICONERROR jmp OutLoop .endifКак и в предыдущей утилите, если RegEnumKeyEx возвращает ошибку, цикл прерывается. Для каждого нового ключа составляются по две строки вида: "CLSID\{clsid компонента}" и "CLSID\{clsid компонента}\TypeLib", затем делается попытка открыть раздел реестра со второй строкой в качеcтве имени.
Код (Text):
invoke lstrcpy,ADDR SubKey2,ADDR SubKey1 invoke lstrcpy,ADDR SubKey3,ADDR SubKey1 invoke lstrcat,ADDR SubKey2,ADDR s invoke lstrcat,ADDR SubKey3,ADDR s invoke lstrcat,ADDR SubKey2,ADDR SubKeyBuf invoke lstrcat,ADDR SubKey3,ADDR SubKeyBuf invoke lstrcat,ADDR SubKey3,ADDR s invoke lstrcat,ADDR SubKey3,ADDR findbuf invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey3,0, KEY_ALL_ACCESS,ADDR HKey3Успешное открытие ключа TypeLib говорит о том, что для компонента имеется библиотека типов. В этом случае запрашиваем значение ключа "CLSID\{clsid компонента}" и помещаем соответствующую строку в первую колонку окна просмотра списков, а значение ключа "CLSID\{clsid компонента}\TypeLib" (строковое представление идентификатора библиотеки типов) - во вторую колонку:
Код (Text):
.if eax==ERROR_SUCCESS invoke RegOpenKeyEx,HKEY_CLASSES_ROOT, ADDR SubKey2,0,KEY_ALL_ACCESS,ADDR HKey2 .if eax==ERROR_SUCCESS mov bsz,255 invoke RegQueryValueEx,HKey2,0,0,0,ADDR buf,ADDR bsz mov eax,cnt mov item.iItem,eax mov item.iSubItem,0 invoke SendMessage,hList,LVM_INSERTITEM,0,ADDR item mov bsz,255 invoke RegQueryValueEx,HKey3,0,0,0,ADDR buf,ADDR bsz .if eax==ERROR_SUCCESS mov item.iSubItem,1 invoke SendMessage,hList,LVM_SETITEM,0,ADDR item .endif invoke RegCloseKey,HKey2 inc cnt .endif ;RegOpenKeyEx (SubKey2) invoke RegCloseKey,HKey3 .endif ;RegOpenKeyEx (SubKey3) inc idx jmp EnumLoop OutLoop: invoke RegCloseKey,HKeyДвойной щелчок на элементе окна просмотра списка обрабатывается в сообщении WM_NOTIFY. Соответствующий код вынесен в отдельную процедуру FindHelp, причем в регистре ebx передается адрес структуры NMLISTVIEW с информацией об элементе списка, на котором был произведен двойной щелчок:
Код (Text):
.ELSEIF uMsg==WM_NOTIFY mov ebx,lParam assume ebx:ptr NMHDR .if [ebx].code==NM_DBLCLK call FindHelp .endif assume ebx:nothingПроцедура FindHelp начинается с проверки того, что щелчок действительно произведен на элементе списка:
Код (Text):
FindHelp proc assume ebx:ptr NMLISTVIEW mov eax,[ebx].iItem mov item.iItem,eax .if eax==-1 ret .endif assume ebx:nothingТеперь можно скопировать в буфер buf содержимое второй колонки соответствующего элемента (GUID библиотеки типов в текстовом виде).
Код (Text):
mov item.imask,LVIF_TEXT mov item.iSubItem,1 mov item.pszText,OFFSET buf mov item.cchTextMax,255 invoke SendMessage,hList,LVM_GETITEM,0,ADDR itemИз полученного значения составляем название раздела реестра "TypeLib\{GUID библиотеки}" и открываем его.
Код (Text):
invoke lstrcpy,ADDR SubKey2,ADDR cl2 invoke lstrcat,ADDR SubKey2,ADDR s invoke lstrcat,ADDR SubKey2,ADDR buf invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey2, 0,KEY_ALL_ACCESS,ADDR HKeyНужно обработать не очень удобную форму представления данных для библиотеки типов. Чтобы получить путь к файлу библиотеки типов, необходимо сначала указать версию, а о ней заранее невозможно сказать ничего определенного. Плохо то, что функция для загрузки LoadRegTypeLib требует точного указания старшей (major) версии и отказывается загружать библиотеки с другими версиями. Поэтому придется "вручную" просмотреть подключи с версиями и выделить из имени подключа старшую версию.
Код (Text):
.if eax==ERROR_SUCCESS mov idx,0 mov mjver,0 ; для нахождения max значения majversion TlibLoop: mov sbksz,255 ; перечислим подключи в разделе 'HKCR\TypeLib\{GUID}' ; (в форме majversion.minversion) invoke RegEnumKeyEx,HKey,idx,ADDR SubKeyBuf,ADDR sbksz,0,0,0,0 .if eax==ERROR_SUCCESS xor eax,eax mov al,SubKeyBuf ; первый символ подключа (т.е. majversion) sub eax,30h ; символ преобразуем в значение cmp mjver,eax jge Tlib1 ; выбираем для версии большее значение mov mjver,eax Tlib1: inc idx jmp TlibLoop .endif ;RegEnumKeyEx invoke RegCloseKey,HKeyМы получили номер старшей версии; теперь можно обычным способом преобразовать строковое представление GUID в числовое и передать всё функции LoadRegTypeLib, которая принимает следующие аргументы:
- адрес структуры с GUID загружаемой библиотеки;
- старшая версия загружаемой библиотеки (необходимо указать точно; в нашем случае она находится в переменной mjver);
- младшая версия загружаемой библиотеки (здесь можно оставить 0, потому что система загрузит любую младшую версию, превышающую указанную);
- локальный языковой идентификатор (lcid; можно оставить нейтральный - 0);
- адрес переменной, в которой будет возвращен указатель на интерфейс ITypeLib.
Код (Text):
invoke MultiByteToWideChar,CP_ACP,0,ADDR buf,-1,ADDR wbuf,510 invoke CLSIDFromString,ADDR wbuf,ADDR TlibGuid .if eax==NOERROR invoke LoadRegTypeLib,ADDR TlibGuid,mjver,0,0,ADDR pTlib .if eax==S_OKВозвращенное значение S_OK говорит о том, что у нас есть действительный указатель на интерфейс ITypeLib. Нас интересует метод GetDocumentation, имеющий следующие аргументы:
- индекс описания типа; если -1, возвращаются данные для самой библиотеки типов (что нам и нужно);
- адрес переменной, в которой будет возвращен указатель на строку типа BSTR с именем запрашиваемого элемента (в нашем случае – самой библиотеки типов);
- адрес переменной, в которой будет возвращен указатель на строку типа BSTR с описанием запрашиваемого элемента;
- адрес переменной, в которой будет возвращен контекстный идентификатор для файла справки;
- адрес переменной, в которой будет возвращен указатель на строку типа BSTR с полным путем к файлу справки.
Если какой-нибудь вид данных не нужен, можно передать в соответствующем аргументе 0. Адрес функции GetDocumentation расположен в виртуальной таблице интерфейса ITypeLib по смещению 24h.
Код (Text):
push OFFSET HelpFile push 0 ; контекст не нужен, pBstrHelpCtx=NULL push OFFSET DocString push OFFSET CompName push -1 ; индекс (-1='сама библиотека') mov eax,pTlib push eax ; указатель ‘this’ mov eax,[eax] ; виртуальная таблица call dword ptr [eax+24h] ;GetDocumentation</pre></code> <p>Если по возвращении из метода в переменной HelpFile окажется 0, это значит, что файла справки нет. В этом случае выведем простое окно сообщения с полученными сведениями: <p><pre><code>.if HelpFile==0 invoke WideCharToMultiByte,CP_ACP,0,CompName,-1,ADDR SubKeyBuf,255,0,0 invoke lstrcpy,ADDR wbuf,ADDR SubKeyBuf invoke lstrcat,ADDR wbuf,ADDR CRLF invoke WideCharToMultiByte,CP_ACP,0,DocString,-1,ADDR SubKeyBuf,255,0,0 invoke lstrcat,ADDR wbuf,ADDR SubKeyBuf invoke lstrcat,ADDR wbuf,ADDR CRLF invoke lstrcat,ADDR wbuf,ADDR ms1 invoke MessageBox,0,ADDR wbuf,ADDR App,MB_OK or MB_ICONINFORMATION</pre></code> <p>Если же файл справки указан, преобразуем соответствующую строку из Unicode в ANSI и передадим ее адрес в качестве аргумента функции ShellExecute (чтобы иметь возможность загружать как hlp-, так и chm-файлы): <p><pre><code>.else ; загрузить и отобразить файл справки invoke WideCharToMultiByte,CP_ACP,0,HelpFile,-1,ADDR buf,255,0,0 invoke ShellExecute,0,ADDR cmd,ADDR buf,0,0,SW_SHOW .if eax<33 ; ошибка ShellExecute invoke wsprintf,ADDR wbuf,ADDR fmt,ADDR buf invoke MessageBox,0,ADDR wbuf,ADDR App,MB_OK or MB_ICONERROR .endif .endif ;HelpFile==0</pre></code> <p>Строки типа BSTR необходимо освободить, во избежание утечки ресурсов: <p><pre><code> invoke SysFreeString,HelpFile invoke SysFreeString,CompName invoke SysFreeString,DocString</pre></code> <p>При ошибке вызова метода GetDocumentation отображаем соответствующее сообщение. В любом случае после этого необходимо освободить указатель интерфейса ITypeLib (pTlib): <p><pre><code> .else ; ошибка вызова GetDocumentation invoke MessageBox,0,ADDr Err6,ADDR App,MB_OK or MB_ICONERROR .endif ; GetDocumentation mov eax,pTlib push eax ; указатель ‘this’ mov eax,[eax] ; виртуальная таблица call dword ptr [eax+8] ; функция №3 (Release)Завершают функцию обработки других ошибок:
Код (Text):
.else ; ошибка LoadRegTypeLib invoke MessageBox,0,ADDR Err5,ADDR App,MB_OK or MB_ICONERROR .endif ; LoadRegTypeLib==S_OK .else ; ошибка CLSIDFromString invoke MessageBox,0,ADDR Err4,ADDR App,MB_OK or MB_ICONERROR .endif ; CLSIDFromString==NOERROR .else ; ошибка RegOpenKeyEx invoke MessageBox,0,ADDR Err7,ADDR App,MB_OK or MB_ICONERROR ret .endif ; RegOpenKeyEx==ERROR_SUCCESS ret FindHelp endpС помощью данной утилиты можно обнаружить множество интересных вещей. Попробуйте!
Раздел AppID
Раздел реестра «HKEY_CLASSES_ROOT\AppID» совместно с разделом «HKEY_LOCAL_MACHINE\Software\Microsoft\OLE» определяют установки для распределённой системы COM (DCOM).
Распределённая система имеет дело с передачей данных по сети, и перед ней сразу же встают вопросы аутентификации, авторизации и другие проблемы обеспечения безопасности. Общая схема конфигурирования обычно такова: в разделе «HKEY_LOCAL_MACHINE\Software\Microsoft\OLE» описываются значения параметров безопасности по умолчанию. Раздел «HKEY_CLASSES_ROOT\AppID» содержит ключи (GUID), в которых параметры безопасности могут задаваться для отдельных групп компонентов (в этом случае «HKEY_CLASSES_ROOT\CLSID\{clsid компонента}\AppID» содержит ссылку на соответствующий ключ раздела «HKEY_CLASSES_ROOT\AppID»). Значения из раздела AppID имеют преимущество перед соответствующими значениями по умолчанию. Кроме того, параметры безопасности и другие связанные с распределенной системой параметры могут задаваться явным образом при создании компонента с использованием функции CoCreateInstanceEx или ей подобной. В этом случае используются явно заданные значения.
Раздел «HKEY_LOCAL_MACHINE\Software\Microsoft\OLE» может иметь следующие именованные значения:
- EnableDCOM – разрешает (Y или y) или запрещает (N или n) удаленным клиентам запуск компонентов на данной системе;
- DefaultLaunchPermission – определяет список участников (ACL), кто может запускать по умолчанию компоненты на данной машине;
- DefaultAccessPermission – определяет список участников (ACL), кто может по умолчанию получать доступ к запущенным объектам;
- LegacyAuthenticationLevel – устанавливает уровень аутентификации по умолчанию;
- LegacyImpersonationLevel – устанавливает уровень имперсонации по умолчанию;
- LegacyMutualAuthentication – разрешает (Y или y) или запрещает (любое другое значение или отсутствие именованного значения) взаимную аутентификацию;
- LegacySecureReferences – указывает, охраняются (Y или y) или нет (любое другое значение или отсутствие именованного значения) вызовы AddRef и Release.
Ключи «HKEY_CLASSES_ROOT\AppID\{GUID}» могут иметь следующие именованные значения:
- RemoteServerName – имя сервера, на котором должен быть запущен компонент;
- ActivateAtStorage – указывает, что компонент должен быть запущен на той же машине, на которой хранятся данные объекта;
- LocalService – указывает, что компонент реализован в виде Win32-сервиса;
- ServiceParameters – используется совместно с LocalService. Значение этого параметра передается при запуске соответствующего сервиса в виде аргументов командной строки;
- RunAs – позволяет запустить компонент, не являющийся сервисом, от имени определенного пользователя;
- LaunchPermission – определяет список участников (ACL), кто может запустить сервер с данным компонентом;
- AccessPermission – определяет список участников (ACL), кто может получить доступ к объектам данного класса;
- DllSurrogate – содержит полный путь к программе-оболочке (exe), в которой должен быть запущен удаленный внутрипроцессный (dll) сервер;
- AuthenticationLevel – определяет уровень аутентификации для компонента.
Указанные в реестре значения могут быть перекрыты явным вызовом функции CoInitializeSecurity. © Roustem
3 кита COM. Кит первый: реестр
Дата публикации 2 дек 2006