3 кита COM. Кит первый: реестр — Архив WASM.RU

Все статьи

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 и запустить его:

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:

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-файл:

<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. Редактор реестра.
Рис. 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. А теперь слегка перепишем наш скрипт:

Set x = CreateObject("MyName")
x.Visible = True

Естественно, вместо "MyName" у вас должно быть то имя, которое вы выбрали для своего ProgID. Если все было сделано правильно, теперь Excel послушно "откликается" на новое имя.

Утилита для ProgID

Копаясь в реестре, можно обнаружить множество интересных вещей. Однако, ручной поиск по перекрестным ссылкам, особенно если компонентов очень много, весьма утомителен. Сочетая приятное с полезным, создадим для наших поисков небольшую утилиту, которая будет переводить данный ProgID компонента в его CLSID и наоборот.

Рис. 2. Интерфейс утилиты COM_1
Рис.2. Интерфейс утилиты COM_1

Дизайн пользовательского интерфейса утилиты приведен на рис. 2. Он реализуется следующим файлом ресурсов (COM_1.rc):

#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, поэтому необходимо включить файлы для поддержки этого модуля.

.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"

Утилита реализована в виде модального диалогового окна, создаваемого из шаблона ресурса:

.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.

.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

Если текста нет - вывести сообщение с предложением его ввести:

.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, строка заканчивается нулем и длина строки определяется автоматически;
  • адрес буфера для преобразованной строки;
  • размер буфера для преобразованной строки.

	invoke MultiByteToWideChar,CP_ACP,0,ADDR buf,-1,ADDR wbuf,510

Еще одно затруднение. Функция ProgIDFromCLSID принимает в качестве аргумента адрес структуры с числовым представлением CLSID, а не с его строковым представлением. Подробнее этот нюанс мы обсудим в разделе о CLSID, а сейчас используем для преобразования строкового представления в числовой еще одну вспомогательную функцию COM API - CLSIDFromString (где cls - оставленный под структуру CLSID буфер размером 16 байт):

	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.

		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:

	.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".

.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

На этом обработка сообщений диалогового окна заканчивается:

		.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-битного значения была использована структура следующего вида:

GUID	STRUCT
    Data1 	dd ?
    Data2 	dw ?
    Data3 	dw ?
    Data4 	db 8 dup(?)
GUID ENDS

В реестре же для представления GUID используется строка такого формата:

{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
Рис.3. Интерфейс утилиты COM_2

Вот соответствующий файл ресурсов (COM_2.rc):

#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:

.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.

	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). Итак, обработка команд:

.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 не зарегистрирован в реестре, или зарегистрированный тип сервера не указан в флаге типа сервера, возвращается сообщение об ошибке.

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", в терминологии языка С++). Об этой структуре можно сказать лишь то, что первое ее поле является указателем на другую структуру - "виртуальную таблицу", содержащую адреса процедур, реализующих методы интерфейса. Для вызова того или иного метода необходимо найти адрес его реализации по заранее известному и постоянному смещению в виртуальной таблице. Подробнее "кухню" этого вопроса мы разберем во второй статье, а пока просто приведем код:

	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 и любых других сообщений закончена.

			.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
Рис.4. Интерфейс утилиты COM_3

Для экономии места мы не будем приводить здесь полный код приложения; соответствующие файлы находятся в каталоге COM_3 архива ComKit1.rar. Вместо этого рассмотрим лишь некоторые существенные моменты, имеющие отношение к логике работы программы.

invoke RegOpenKeyEx,HKEY_CLASSES_ROOT,ADDR SubKey1,0,KEY_ALL_ACCESS,ADDR HKey
.if eax==ERROR_SUCCESS 

Начинаем с того, что открываем раздел реестра "HKEY_CLASSES_ROOT\CLSID", сохраняя описатель в переменной HKey. При успешном открытии начинается основной цикл по перечислению имеющихся в открытом разделе ключей.

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 строк с типом объекта, выбранную из выпадающего списка.

	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 компонента}") и запрашиваем значение по умолчанию, которое пересылаем в окно списка и переходим к новой итерации:

	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
Рис.5. Интерфейс утилиты COM_4

Здесь опять приведем лишь ключевые моменты кода, исходные файлы содержатся в каталоге COM_4 архива ComKit1.rar.

Поскольку приложение использует библиотеку COM, при инициализации диалогового окна вызывается процедура CoInitialize, а при его закрытии - соответственно CoUninitialize. При нажатии кнопки "List" окно списка очищается, а из окна редактирования извлекается CLSID уже знакомым нам способом:

	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), а контекст объекта допускает любой тип сервера:

	invoke CoCreateInstance,ADDR cls,0, CLSCTX_SERVER,ADDR IUnk,ADDR pUnk

Если указатель успешно получен (в pUnk), открываем раздел реестра "HKEY_CLASSES_ROOT\Interface" и входим в главный цикл перечисления подключей (т.е. интерфейсов):

	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 интерфейса в строковом виде) преобразуется в числовой вид (если в ходе преобразования происходит ошибка, данный ключ просто игнорируется и цикл продолжается).

	.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):

	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:

.if eax==S_OK
	mov eax,pItfc	; указатель на затребованный интерфейс
	push eax		; помещаем в качестве 1-го (и единственного) пар-ра;
	mov eax,[eax]	; адрес виртуальной таблицы
	call dword ptr [eax+8]	; вызываем 3-й метод из в.табл. (Release)

Одновременно с этим создаем строку вида "Interface\{IID интерфейса}" и открываем соответствующий раздел реестра, запрашивая значение по умолчанию этого раздела (имя интерфейса). Это имя добавляется в окно списка.

	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 возвращают код ошибки, данный ключ просто игнорируется, а цикл продолжается дальше.

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
Рис.6. Интерфейс утилиты COM_5

Приложение работает следующим образом (детали реализации графического интерфейса опущены; полный исходный код содержится в каталоге COM_5 архива ComKit1.rar). При обработке сообщения WM_INITDIALOG открываем раздел реестра "HKEY_CLASSES_ROOT\CLSID", затем входим в главный цикл перечисления содержащихся в нем ключей:

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тве имени.

	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" (строковое представление идентификатора библиотеки типов) - во вторую колонку:

.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 с информацией об элементе списка, на котором был произведен двойной щелчок:

.ELSEIF uMsg==WM_NOTIFY
	mov ebx,lParam
	assume ebx:ptr NMHDR
	.if [ebx].code==NM_DBLCLK
		call FindHelp
	.endif
	assume ebx:nothing 

Процедура FindHelp начинается с проверки того, что щелчок действительно произведен на элементе списка:

FindHelp proc 
	assume ebx:ptr NMLISTVIEW
	mov eax,[ebx].iItem
	mov item.iItem,eax
	.if eax==-1
		ret
	.endif
	assume ebx:nothing

Теперь можно скопировать в буфер buf содержимое второй колонки соответствующего элемента (GUID библиотеки типов в текстовом виде).

	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 библиотеки}" и открываем его.

	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) версии и отказывается загружать библиотеки с другими версиями. Поэтому придется "вручную" просмотреть подключи с версиями и выделить из имени подключа старшую версию.

	.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.

	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.

	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

Если по возвращении из метода в переменной HelpFile окажется 0, это значит, что файла справки нет. В этом случае выведем простое окно сообщения с полученными сведениями:

.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

Если же файл справки указан, преобразуем соответствующую строку из Unicode в ANSI и передадим ее адрес в качестве аргумента функции ShellExecute (чтобы иметь возможность загружать как hlp-, так и chm-файлы):

.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

Строки типа BSTR необходимо освободить, во избежание утечки ресурсов:

	invoke SysFreeString,HelpFile	
	invoke SysFreeString,CompName
	invoke SysFreeString,DocString

При ошибке вызова метода GetDocumentation отображаем соответствующее сообщение. В любом случае после этого необходимо освободить указатель интерфейса ITypeLib (pTlib):

	.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)

Завершают функцию обработки других ошибок:

		.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.

2002-2013 (c) wasm.ru