3 кита COM. Кит второй: dll — Архив WASM.RU

Все статьи

3 кита COM. Кит второй: dll — Архив WASM.RU

Скачать материалы для статьи

Второй важнейшей основой, без которой невозможна инфраструктура COM, является использование DLL. Причем не просто использование, а очень активное использование; и использование не только самих DLL, но и тех принципов, которые применяются в технологии динамического связывания.

Технология COM претендует на универсальность своей компонентной модели. Это значит, что безболезненно взаимодействовать между собой должны не только компоненты, написанные на разных языках и созданные с использованием различных сред разработки, но и разные виды компонентов: и созданный специально для внедрения в другие приложения код DLL; и компоненты, входящие в EXE-модули самостоятельных приложений; и код, размещенный на другой машине и потенциально созданный для другой платформы. Естественно, сразу возникает проблема передачи данных через границы процессов и систем, а также единообразия работы с внутрипроцессными и внепроцессными объектами.

COM в качестве решения этой проблемы использует единый и единственный указатель интерфейса: все, что нужно для работы с объектом - это получить указатель на его интерфейс, и ничего более. Этот указатель представляет собой некий адрес в адресном пространстве клиента, по которому соответствующая область памяти структурирована определенным образом в соответствии с бинарным стандартом. А это значит, что область эта может принадлежать только DLL, и любой объект - будь он в другом процессе или на другой машине - имеет свою DLL (неважно, созданную разработчиком компонента или предоставленную системой), которая внедряется в адресное пространство клиента. Более того, именно эта часть объекта и предоставляет клиенту собственно интерфейс; можно сказать, что интерфейс объекта всегда реализуется в DLL, независимо от того, внутрипроцессный это объект, внепроцессный или удаленный.

"Интерфейсная" область структурируется иерархически с использованием других указателей наподобие связанного списка. Начинающих - и даже не только начинающих - COM-программистов часто смущает это обилие вложенных указателей. Чего стоит, например, аргумент метода QueryInterface в виде двойного указателя на неопределенный тип! Один этот void** языка С++ способен свести с ума, потому что понять его смысл невозможно в принципе. А если кто-то попытался использовать С и выяснил, что там на самом деле есть еще и скрываемая объектными языками виртуальная таблица - чтобы добраться до метода, необходимо тройное перенаправление?..

Даже авторы некоторых статей по COM-программированию на ассемблере как-то слишком уж поспешно проскакивают этот момент, бросаясь сразу в объятия спасительных макросов и предопределенных структур и пробормотав что-то невнятное о запутанности указателей, так что возникает подозрение, что они сами не до конца разобрались с этой проблемой. Между тем это вопрос, в котором можно (и нужно) разобраться - один раз, но досконально.

Попробуем спроектировать собственную объектную модель и посмотрим, какие проблемы при этом возникают. Главной идеей ООП было объединить данные и обрабатывающие их и семантически связанные с ними функции в одну конструкцию. Предположим, что у нас есть указатель на некий объект, представляющий собой область с данными, за которой непосредственно следуют обрабатывающие их методы (рис. 1). Доступ и к данным, и к методам этого объекта можно получить, используя простое смещение, добавляемое к значению указателя - как при работе с обычной структурой. Например, если указатель на объект находится в регистре eax, метод 2 можно вызвать следующим способом:

add eax,m2
call eax

Гипотетическая модель объекта, доступ к методам и данным которого осуществляется через один и тот же указатель
Рис. 1. Гипотетическая модель объекта, доступ к методам и данным которого осуществляется через один и тот же указатель

Какие могут тут быть проблемы? Если создаются несколько экземпляров одного и того же объекта, в памяти оказываются несколько копий одного и того же кода, хотя это совершенно излишне. Для решения этой проблемы данные и код можно разместить в различных секциях, причем секцию кода сделать общей, а секцию данных для каждого экземпляра объекта выделить свою. Именно по такому принципу строятся DLL (хотя формат PE-файла и соответствующие опции компиляторов позволяют создавать при необходимости и общие секции данных).

Как только появляется общая секция кода, возникает и вопрос отслеживания количества ссылок на него, чтобы своевременно освобождать память после завершения работы. Обычно эта проблема решается созданием счетчиков, значение которых увеличивается на один при создании нового экземпляра и уменьшается на один при его разрушении. Когда значение счетчика при очередном уменьшении достигает 0, секцию кода тоже можно выгрузить из памяти. Этот механизм используется операционной системой при загрузке и выгрузке DLL, но он не виден приложениям; в компонентной же модели он становится явным.

Модель с различными секциями данных и единой секцией кода.
Рис. 2. Модель с различными секциями данных и единой секцией кода.

Для того, чтобы обращаться и к данным, и к коду через один и тот же указатель объекта, в области данных можно записать указатель на общую для всех экземпляров секцию кода с реализацией методов объекта (рис. 2). При такой схеме методы объекта не имеют непосредственного неявного доступа к данным и последние придется передавать через аргументы, как в случае самых обычных процедур. А это разрушает саму идею объектного подхода - стоило ли ради этого огород городить?

Поэтому возникает еще одна схема, реализующая один из основных принципов ООП - инкапсуляцию: непосредственный доступ к данным объекта через указатель на объект закрывается - он превращается в "чёрный ящик" (рис. 3). Из всех данных доступным остается лишь указатель на секцию кода (благодаря тому, что он расположен в самом начале структуры данных и именно его адрес содержит указатель объекта) с реализацией методов объекта, которые непосредственно имеют дело с данными. Поскольку у каждого объекта своя секция данных, тогда как реализация методов одна для всех объектов, каждый метод при своем вызове в качестве первого параметра получает указатель на секцию данных именно того объекта, для которого вызывается метод. (В терминологии языка С++ этот параметр получил название указателя "this").

"Расплатой" за это является один уровень перенаправления, добавляемый на пути от указателя объекта к его методу. Теперь, если указатель объекта находится в регистре eax, метод вызывается следующим образом:

		; перенаправление:
mov eax,[eax]	; получить адрес секции кода
add eax,m2	; добавить смещение второго метода
call eax

Реализация принципа инкапсуляции.
Рис. 3. Реализация принципа инкапсуляции.

Еще одна проблема возникает в связи с реализацией другого основного принципа ООП - полиморфизма. В среде COM времени исполнения нет имен. В принципе можно было бы создать и систему с использованием имен во время исполнения - например, DLL пользуются именно такой схемой. (К слову сказать, основанный на COM большой раздел OLE - автоматизация - использует модель позднего связывания, построенную именно по такому принципу. Но об этом поговорим как-нибудь в другой раз). Для доступа к методам вычисляются их адреса с использованием указателя интерфейса и постоянных смещений, а имена могут существовать лишь во время компиляции.

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

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

Проблема различных реализаций одного интерфейса.
Рис. 4. Проблема различных реализаций одного интерфейса.

Для решения этой проблемы вводится еще один посредник между указателем на объект и реализацией его методов - виртуальная таблица (рис. 5). В секции данных объекта теперь располагается указатель не на секцию кода, а на виртуальную таблицу, в которой содержатся указатели на соответствующие методы. Смещение указателя на один и тот же метод относительно начала виртуальной таблицы всегда постоянно, хотя объем кода метода и его расположение в секции кода в различных реализациях могут сильно различаться. Но это вводит еще один уровень перенаправления. Вызов метода выглядит теперь следующим образом:

			; первое перенаправление:
mov eax,[eax]		; получить адрес виртуальной таблицы
			; второе перенаправление:
mov eax,[eax+m2]	; получить адрес метода по смещению 
			; в виртуальной таблице
call eax

Правда, это может быть записано короче таким образом:

mov eax,[eax]		; первое перенаправление
call dword ptr [eax+m2]	; косвенный вызов процедуры
			; со вторым перенаправлением

Реализация принципа полиморфизма.
Рис. 5. Реализация принципа полиморфизма.

Полиморфизм означает, что в качестве реализации метода интерфейса может выступать абсолютно любой код, удовлетворяющий данному синтаксису вызова метода. Именно это и используется в реализации знаменитой "прозрачности удаленного доступа" COM (хотя этот подход использовался еще до COM в RPC). Благодаря полиморфизму код, реализующий интерфейс в той части сервера, которая размещается в DLL и загружается в адресное пространство клиента, может исполнять не алгоритм метода, а заниматься совершенно другими вещами - упаковывать полученные для метода параметры со всеми сопутствующими данными в некую структуру и отправлять ее куда-то в адресное пространство другого процесса или даже по сети на другую машину, где находится другая часть кода, реализующая собственно алгоритм метода. DLL выступает в этом случае "представителем" кода, размещенного в другом процессе или на другой системе. Клиент же ничего этого не видит; максимум, что он может заметить - увеличение задержек при выполнении методов.

Если свести все воедино, мы получим схему, приведенную на рис. 6. Это и является бинарной структурой интерфейса, используемой в COM. Таким образом, несколько упрощая, можно сказать, что первое перенаправление указателя интерфейса COM на пути к реализации метода делается "во славу" и ради принципа инкапсуляции, а второе - "во славу" и ради принципа полиморфизма. Возможно, это поможет кому-нибудь лучше запомнить эту схему и не запутаться в ней. :)

Бинарная структура интерфейса COM.
Рис. 6. Бинарная структура интерфейса COM.

Осталось лишь выяснить, откуда берутся двойные указатели на интерфейс и зачем нужно третье перенаправление. На самом деле, это искусственное абстрактное образование, создаваемое языками высокого уровня и не имеющее собственно к COM никакого отношения. Дело в том, что указатель интерфейса должен быть возвращен функцией, создающей объект. Но функции COM по соглашению возвращают результат типа HRESULT, сообщающий об успешности или ошибке хода выполнения. Для возврата указателя используется аргумент функции, а это значит, что указатель передается по ссылке: его значение копируется в переменную, которую подготовил клиент и передал функции ее адрес. Формально тип соответствующего аргумента функции оказывается двойным указателем; но гораздо проще понимать его как адрес переменной (на стороне клиента), в которую должен быть возвращен указатель интерфейса. И соответственно, следует не создавать переменную типа двойного указателя (в нотации С - void** ppv) и передавать функции ее значение (ppv), а создать переменную типа указателя на интерфейс (void* pv) и передать функции ее адрес (&pv).

Раз уж речь шла о принципах ООП, придется упомянуть еще и третий основной принцип - наследования. На уровне интерфейсов COM он проявляется в том, что интерфейс-потомок наследует всю виртуальную таблицу интерфейса-родителя, добавляя указатели к собственным методам в ее конец. Поскольку все интерфейсы COM прямо или косвенно наследуются от IUnknown, мы можем с абсолютной уверенностью заявить, что первые три указателя виртуальной таблицы любого интерфейса COM содержат адреса реализаций трех методов IUnknown в одном и том же порядке: QueryInterface, AddRef и Release (со смещениями относительно начала виртуальной таблицы соответственно 0, 4 и 8 байт). Т.е. если у нас есть указатель некоторого интерфейса pSomeInterface, эти методы вызываются так:

; здесь аргументы (если есть) помещаются в стек
mov eax,pSomeInterface
push eax			; первый аргумент всех методов - "this"
mov eax,[eax]
call dword ptr [eax+x]		; x=0 для QueryInterface,
				; x=4 для AddRef,
				; x=8 для Release

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

Структура dll-сервера

Ну что ж, пора приступить к практике и посмотреть, каким образом задействуется та самая бинарная структура интерфейса COM, которую мы так долго разбирали. Система предоставляет своего рода "точки входа в COM". Клиентскую сторону мы уже видели - это функция API CoCreateInstance, с помощью которой мы создавали универсальный клиент в прошлой статье. Со стороны же сервера DLL это функция DllGetClassObject, которая должна быть реализована и экспортирована DLL. Система передает ей такие параметры:

  • адрес структуры, содержащей CLSID объекта класса;
  • адрес структуры, содержащей IID интерфейса, который запрашивается у объекта класса;
  • адрес переменной, в которой возвращается указатель на затребованный интерфейс.

Эта вызываемая системой функция создает объект класса - но это не запрашиваемый клиентом объект. Да, вы узнали страшную тайну. COM - это царство посредников. Объект класса - это еще один посредник, имеющий даже собственное имя - "фабрика классов". Именно эта фабрика создает экземпляры объектов компонента для клиента, предоставляя для этой цели собственный интерфейс - чаще всего это IClassFactory, хотя могут быть и другие варианты.

Функция CoCreateInstance, которую мы использовали в нашем клиенте (COM_2.exe), на самом деле является "оберткой" вокруг функции, которая является действительной точкой входа со стороны клиента - CoGetClassObject. Параметры этой функции:

  • адрес структуры, содержащей CLSID компонента, для которого создается фабрика классов;
  • тип сервера (внутрипроцессный, локальный, удаленный или внутрипроцессный обработчик);
  • адрес структуры, содержащей имя и другие параметры удаленного сервера (этот параметр может быть 0);
  • адрес структуры, содержащей IID интерфейса фабрики классов (обычно IClassFactory);
  • адрес переменной, в которой будет возвращен указатель на требуемый интерфейс.

CoCreateInstance вызывает CoGetClassObject, запрашивая у нее указатель на интерфейс IClassFactory для компонента с данным CLSID. Затем через полученный указатель вызывает метод CreateInstance интерфейса IClassFactory, запрашивая уже указатель на интерфейс самого компонента, а указатель на IClassFactory сразу же освобождает (вызвав через него метод Release).

Но об этом позже; а сейчас построим простейший сервер, вернее, псевдосервер COM - потому что он экспортирует DllGetClassObject, но функция эта все время возвращает код ошибки CLASS_E_CLASSNOTAVAILABLE. Сервер DLL должен экспортировать еще одну функцию - DllCanUnloadNow, которая в зависимости от состояния внутренних счетчиков реализуемых объектов возвращает S_OK или S_FALSE, в соответствии с чем система может выгрузить DLL из памяти. Вот исходный код полностью (файл COM_6.asm):

.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
ms1	db "DllGetClassObject",0
ms2	db "DllCanUnloadNow",0
app	db "FooDll",0
.code

DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
	invoke MessageBox,0,addr ms1,addr app,0
	mov eax,CLASS_E_CLASSNOTAVAILABLE
	ret
DllGetClassObject endp

DllCanUnloadNow proc
	invoke MessageBox,0,addr ms2,addr app,0
	mov eax,S_OK
	ret
DllCanUnloadNow endp
end

Необходимо создать еще def-файл (COM_6.def):

LIBRARY COM_6
EXPORTS	DllGetClassObject PRIVATE
	DllCanUnloadNow	PRIVATE

Ключевое слово "PRIVATE" используется для того, чтобы экспортируемые функции не попали в создаваемый при построении DLL lib-файл - чтобы кто попало не мог импортировать эти функции, ибо они предназначены только "для своих" (т.е. системы COM). Хотя это обстоятельство может остановить разве что делающих все по правилам прикладных программистов, но никак не низкоуровневиков, способных подключиться непосредственно к DLL. :)

Строим DLL со следующими опциями (обратите внимание, наша DLL в данном конкретном случае не имеет функции инициализации и соответственно точки входа):

\masm32\bin\ml /c /coff /Cp COM_6.asm
\masm32\bin\Link /DLL /DEF:COM_6.def /NOENTRY /SUBSYSTEM:WINDOWS /VERSION:4.0 /LIBPATH:\masm32\lib COM_6.obj

Предполагается, что MASM32 расположен в каталоге \MASM32 текущего диска; если это не так, необходимо соответствующим образом отредактировать пути.

Теперь запускаем редактор реестра и находим созданный в прошлый раз "хулиганский" ключ {00000000-0000-0000-0000-000000000001} в разделе "HKEY_CLASSES_ROOT\CLSID". Удалим оттуда подключ "TreatAs", если он там есть, и создадим новый: "InprocServer32". В качестве значения запишем полный путь к созданной нами DLL (например, "C:\COM_6\COM_6.dll"). Запускаем наш универсальный клиент (COM_2.exe), заполняем CLSID, для IID жмем кнопку "IUnknown", тип сервера - "InProc server" и - "Connect". Остальное не описываю - сами увидите. Надеюсь, то, что клиент сообщает об ошибке, никого не удивляет. :)

IClassFactory

Интерфейс IClassFactory, помимо функций-членов IUnknown, содержит два собственных: CreateInstance и LockServer. Приступим к реализации второй версии нашего "сервера" (файл COM_7.asm):

.386
.model flat,stdcall
option casemap:none

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
pVtbl	DWORD offset Vtbl
counter	DWORD 0

Эти две переменные и составляют содержание простейшего инстанциированного "экземпляра интерфейса" - указатель на виртуальную таблицу (в соответствии с бинарным стандартом COM) и счетчик ссылок для данного объекта. Других данных у нашей реализации этого объекта нет. Дальше расположим саму виртуальную таблицу:

Vtbl	DWORD offset IUnknown_QueryInterface	
	DWORD offset IUnknown_AddRef
	DWORD offset IUnknown_Release
	DWORD offset IClassFactory_CreateInstance
	DWORD offset IClassFactory_LockServer

На самом деле названия функций не играют никакой роли - можно было использовать любые названия. Важен порядок расположения функций в виртуальной таблице - он должен строго соответствовать интерфейсу. Само собой, указатели на функции нельзя чередовать с посторонними данными. В данном случае, в названии первых трех функций имеется префикс IUnknown, чтобы подчеркнуть, что эти члены наследуются от базового интерфейса, хотя все эти функции являются членами интерфейса IClassFactory.

Далее следуют обычные данные:

IUnk	dd 0	; IID интерфейса IUnknown
	dw 0
	dw 0
	db 0C0h,0,0,0,0,0,0,46h
ICFct	dd 1	; IID интерфейса IClassFactory
	dw 0
	dw 0
	db 0C0h,0,0,0,0,0,0,46h
ms	db "Unloading",0
ms0	db "DllGetClassObject",0
ms1	db "QueryInterface",0
ms2	db "AddRef",10,13,"counter = %i",0
ms3	db "Release",10,13,"counter = %i",0
ms4	db "CreateInstance",0
ms5	db "LockServer",10,13,"counter = %i",0
app	db "ClassFactory",0
buf	db 128 dup(0)

Теперь реализации функций и методов.

.code
DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
	invoke MessageBox,0,offset ms0,offset app,0
	; перенаправить вызов IUnknown_QueryInterface
	push ppv	; ppvObject
	push riid	; iid
	lea eax,pVtbl
	push eax	; указатель 'this'
	call IUnknown_QueryInterface
	ret		; возвратить результат QueryInterface
DllGetClassObject endp

В данной реализации эта функция (как и остальные) отображает окно сообщения, а затем просто перенаправляет вызов функции интерфейса IUnknown_QueryInterface. Параметр rclsid нужен в тех случаях, когда один сервер dll реализует сразу несколько компонентов с различными CLSID (и функция DllGetClassObject должна решить, объект какого именно класса создавать). В нашем случае "компонент" всего один - вернее, его нет вообще, поэтому этот "сервер" будет работать с любым CLSID. В случае реального сервера именно в этом месте CLSID реализуемых компонентов жестко вшиты в код.

Обратите внимание, что мы вызываем IUnknown_QueryInterface напрямую. Хотя это и метод интерфейса COM, но мы, как авторы реализации, обладаем знанием внутреннего устройства нашего объекта, которого нет у клиента. При желании ничто не может нам помешать вызвать этот метод стандартным для COM способом, т.е. через указатель интерфейса - в данном случае, например, используя регистр eax, хотя это не имеет смысла.

Поскольку теперь у нас есть счетчик объекта, DllCanUnloadNow должна возвращать результат, зависящий от его значения:

DllCanUnloadNow	proc
	.if counter>0
		mov eax,S_FALSE
	.else
		invoke MessageBox,0,offset ms,offset app,0
		mov eax,S_OK
	.endif
	ret
DllCanUnloadNow endp

Переходим к самому интересному - реализации QueryInterface:

IUnknown_QueryInterface	proc thisptr:DWORD,iid:DWORD,ppvObject:DWORD
	invoke MessageBox,0,offset ms1,offset app,0
	invoke IsEqualGUID,offset IUnk,iid
	.if eax==0		; не IUnknown
		invoke IsEqualGUID,offset ICFct,iid
		.if eax==0	; не IClassFactory
			mov eax,E_NOINTERFACE
			ret
		.endif
	.endif

Сначала с помощью вспомогательной функции IsEqualGUID (принимающей в качестве аргументов адреса двух содержащих GUID'ы структур, которые необходимо сравнить между собой) проверяем, является ли запрошенный интерфейс IUnknown или IClassFactory. Если ни то, ни другое, возвращается ошибка. Если же запрошен один из этих двух интерфейсов, в переменную, адрес которой передан в аргументе ppvObject, копируется адрес переменной pVtbl, находящейся в самом начале нашего виртуального "объекта". Поскольку функция возвращает новый указатель на объект, должен быть увеличен его счетчик, что также делается непосредственным вызовом IUnknown_AddRef:

	mov eax,ppvObject	; адрес переменной для результата
	lea ecx,pVtbl		; указатель на IClassFactory
	mov [eax],ecx
	push ecx		; указатель 'this'
	call IUnknown_AddRef
	mov eax,S_OK
	ret
IUnknown_QueryInterface	endp

Реализации AddRef и Release тривиальны - они просто соответственно увеличивают или уменьшают значение счетчика объекта и отображают окно сообщения с его текущим содержимым. Обычно при достижении счетчиком 0 реализация Release должна удалить объект, освобождая занимаемую им память. В нашем же случае никакой объект реально не создавался, все данные были статическими, поэтому и уничтожать ничего не надо.

IUnknown_AddRef	proc thisptr:DWORD
	inc counter
	invoke wsprintf,offset buf,offset ms2,counter
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,counter
	ret
IUnknown_AddRef	endp

IUnknown_Release proc thisptr:DWORD
	dec counter	
	invoke wsprintf,offset buf,offset ms3,counter
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,counter
	ret
IUnknown_Release endp

Теперь собственные функции интерфейса IClassFactory. CreateInstance принимает, помимо указателя 'this', еще 3 аргумента:

  • указатель на "внешний" IUnknown, который применяется при агрегировании объектов. Если агрегирования нет, этот аргумент равен NULL;
  • адрес структуры, содержащей IID запрошенного интерфейса;
  • адрес переменной, в которую должен быть возвращен указатель интерфейса.

Поскольку наш "сервер" не реализует ни одного компонента, он просто на все запросы возвращает E_NOINTERFACE:

IClassFactory_CreateInstance proc thisptr:DWORD,pUnkOuter:DWORD,riid:DWORD,ppvObject:DWORD
	mov eax,ppvObject
	xor ecx,ecx
	mov [eax],ecx	; указатель интерфейса = NULL
	invoke MessageBox,0,offset ms4,offset app,0
	mov eax,E_NOINTERFACE
	ret
IClassFactory_CreateInstance endp

Метод LockServer кроме 'this' принимает лишь один аргумент - булевую переменную, указывающую, увеличить (TRUE) или уменьшить (FALSE) значение счетчика замка сервера. Замок позволяет сохранить сервер в памяти (невыгруженным), даже когда счетчики всех объектов равны 0. В нашем случае замок использует общую со счетчиком объекта глобальную переменную:

IClassFactory_LockServer proc thisptr:DWORD,fLock:DWORD
	.if fLock
		inc counter
	.else
		dec counter
	.endif
	invoke wsprintf,offset buf,offset ms5,counter
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,S_OK
	ret
IClassFactory_LockServer endp
end

Вот и вся программа. Def-файл такой же, как в предыдущем случае (COM_7.def):

LIBRARY COM_7
EXPORTS	DllGetClassObject	PRIVATE
		DllCanUnloadNow	PRIVATE

Это одна из возможных реализаций "точки входа" COM и фабрики классов, притом не самая оптимальная (зато простая). Реализация сильно зависит в том числе и от применяемой модели многопоточности COM (это тема для отдельной большой статьи). В нашем случае как сервер, так и его клиент однопоточные - система COM будет осуществлять принудительную синхронизацию поступающих серверу запросов, что в некоторых случаях может сильно снижать производительность. Некоторые свидетельства "посредничества" COM можно пронаблюдать, экспериментируя с данной утилитой; в частности, попробуйте повторный вход (re-entrance) в сервер из клиента, нажав на кнопку "Connect" (с теми же параметрами) в середине процесса обработки первого запроса (при отображении окна сообщения с AddRef), и сравните это с результатами, когда запущены одновременно два (или более) клиента для одного и того же сервера.

Реализация простейшего объекта

Добавим к нашему серверу "компонент". Для простоты он будет содержать единственный интерфейс IUnknown, но создавать его экземпляры мы будем с выделением памяти из кучи. Благодаря этому мы сможем, наконец, увидеть настоящее применение указателя "this" (даже в таких простейших, казалось бы, методах как AddRef и Release).

Каждый экземпляр объекта будет представлен небольшой структурой, первый член которой (в соответствии со спецификацией COM) является указателем на виртуальную таблицу реализации методов IUnknown. У нас уже есть реализация IUnknown для объекта класса, но мы должны написать другую реализацию методов этого же интерфейса, поскольку объект компонента и объект класса - разные объекты (вот вам принцип полиморфизма в действии). Вторым элементом в структуре объекта компонента будет счетчик ссылок для данного экземпляра объекта, а третьим - идентификатор объекта (для того, чтобы отображать его в окне сообщения). В качестве идентификатора используем простой счетчик созданных объектов, который будет вести объект фабрики классов; при создании это значение будет скопировано в соответствующее поле структуры объекта.

Для представления экземпляра объекта удобнее всего было бы использовать структуру MASM, но в данной реализации я использовал непосредственное обращение к соответствующим данным на низком уровне, чтобы как следует "прочувствовать", что же это такое. Желающие могут переписать этот пример, используя высокоуровневые конструкции MASM'а - весьма неплохое упражнение для закрепления материала.

Перейдем к коду (COM_8.asm):

.386
.model flat,stdcall
option casemap:none

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
objcnt	DWORD 0	;тот самый счетчик созданных объектов

Статическая структура для "объекта класса":

pVtbl		DWORD offset Vtbl
counter	DWORD 0

За ней следуют две виртуальные таблицы: первая - для реализации интерфейса IClassFactory объекта класса, вторая - для реализации (единственного) интерфейса IUnknown объекта компонента:

; виртуальная таблица для объекта класса
Vtbl	DWORD offset IClassFactory_QueryInterface	
	DWORD offset IClassFactory_AddRef
	DWORD offset IClassFactory_Release
	DWORD offset IClassFactory_CreateInstance
	DWORD offset IClassFactory_LockServer

; виртуальная таблица для объекта компонента
Vtbl1	DWORD offset IUnknown_QueryInterface
	DWORD offset IUnknown_AddRef
	DWORD offset IUnknown_Release

Далее идут обычные данные:

IUnk	dd 0	; IID интерфейса IUnknown
	dw 0
	dw 0
	db 0C0h,0,0,0,0,0,0,46h
ICFct	dd 1	; IID интерфейса IClassFactory
	dw 0
	dw 0
	db 0C0h,0,0,0,0,0,0,46h
ms	db "Unloading",0
ms0	db "DllGetClassObject",0
ms1	db "ClassFactroy:",10,13,"QueryInterface",0
ms2	db "ClassFactory:",10,13,"AddRef",10,13,"counter = %i",0
ms3	db "ClassFactory:",10,13,"Release",10,13,"counter = %i",0
ms4	db "ClassFactory:",10,13,"CreateInstance",0
ms5	db "ClassFactory:",10,13,"LockServer",10,13,"counter = %i",0
app	db "DllServer",0
ms6	db "Object %i:",10,13,"QueryInterface",0
ms7	db "Object %i:",10,13,"AddRef",10,13,"objref: %i",0
ms8	db "Object %i:",10,13,"Release",10,13,"objref: %i",0
buf	db 128 dup(0)

Реализация "точек входа" и интерфейса IUnknown объекта класса практически не претерпела никаких изменений, за исключением того, что в DllCanUnloadNow было добавлено условие проверки значения счетчика созданных объектов objcnt.

.code

DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
	invoke MessageBox,0,offset ms0,offset app,0
	; перенаправить вызов IClassFactory_QueryInterface
	push ppv	; ppvObject
	push riid	; iid
	lea eax,pVtbl
	push eax	; указатель 'this'
	call IClassFactory_QueryInterface
	ret		; возвращает результат вызова QueryInterface
DllGetClassObject endp

DllCanUnloadNow	proc
	.if counter>0 || objcnt>0
		mov eax,S_FALSE
	.else
		invoke MessageBox,0,offset ms,offset app,0
		mov eax,S_OK
	.endif
	ret
DllCanUnloadNow endp

IClassFactory_QueryInterface	proc thisptr:DWORD,iid:DWORD,ppvObject:DWORD
	invoke MessageBox,0,offset ms1,offset app,0
	.if ppvObject==0
		mov eax, E_INVALIDARG
		ret
	.endif
	invoke IsEqualGUID,offset IUnk,iid
	.if eax==0		; не  IUnknown
		invoke IsEqualGUID,offset ICFct,iid
		.if eax==0	; не IClassFactory
			mov eax,E_NOINTERFACE
			ret
		.endif
	.endif
	; iid является либо IUnknown, либо IClassFactory
	mov eax,ppvObject	; адрес переменной для указателя интерфейса
	lea ecx,pVtbl		; указатель на IClassFactory
	mov [eax],ecx
	push ecx		; указатель 'this'
	call IClassFactory_AddRef
	mov eax,S_OK
	ret
IClassFactory_QueryInterface	endp

IClassFactory_AddRef	proc thisptr:DWORD
	inc counter
	invoke wsprintf,offset buf,offset ms2,counter
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,counter
	ret
IClassFactory_AddRef	endp

IClassFactory_Release proc thisptr:DWORD
	dec counter	
	invoke wsprintf,offset buf,offset ms3,counter
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,counter
	ret
IClassFactory_Release endp

Зато существенные изменения претерпел метод CreateInstance. Теперь он создает настоящие объекты, выделяя для них память из кучи. Кроме того, нужно организовать проверку параметров и соответствующую обработку ошибок:

IClassFactory_CreateInstance proc uses ebx thisptr:DWORD,pUnkOuter:DWORD,
riid:DWORD,ppvObject:DWORD
	invoke MessageBox,0,offset ms4,offset app,0
	.if ppvObject==0
		mov eax,E_INVALIDARG
		ret
	.endif
	mov ebx,ppvObject	; сохраним в регистре адрес переменной
				; для указателя интерфейса объекта
.if pUnkOuter!=0 
	xor ecx,ecx
	mov [ebx],ecx
		mov eax,CLASS_E_NOAGGREGATION 
		ret
	.endif

Если кто-то пытается агрегировать наш объект в состав какого-то другого объекта, необходимо сообщить ему, что мы на это вовсе не рассчитывали. Параметр pUnkOuter должен содержать NULL. Необходимо проверить также параметр riid: наш компонент поддерживает единственный интерфейс - IUnknown.

	invoke IsEqualGUID,offset IUnk,riid
	.if eax==0	; не IUnknown
		mov [ebx],eax
		mov eax,E_NOINTERFACE
		ret
	.endif

Теперь "сердцевина" метода: создаем объект. Как уже упоминалось, он представлен структурой из трех полей размером DWORD каждое, поэтому необходимо выделить из кучи блок памяти в 12 байт:

	invoke LocalAlloc,LPTR,12
	.if eax==0	; ошибка выделения памяти
		mov [ebx],eax
		mov eax,E_OUTOFMEMORY
		ret
	.endif

Выделенную память необходимо инициализировать. Указатель на выделенный блок памяти остался в регистре eax; первый DWORD по этому адресу должен быть указателем на виртуальную таблицу реализации методов нашего объекта (Vtbl1):

	lea ecx,Vtbl1
	mov [eax],ecx	; указатель на виртуальную таблицу

Второй DWORD содержит счетчик ссылок на экземпляр данного объекта; при создании заносим сюда 0:

	push 0
	pop [eax+4]	; счетчик нового объекта

Третий DWORD - идентификатор объекта, в качетве которого выступает счетчик созданных объектов фабрики классов:

	inc objcnt	; еще один объект
	push objcnt
	pop [eax+8]	; идентификатор нового объекта

Осталось вернуть указатель на вновь созданный объект клиенту, который для этой цели передал через аргумент ppvObject адрес соответствующей переменной (в начале метода мы скопировали его в ebx), а также увеличить счетчик ссылок на наш объект на 1:

	mov [ebx],eax	; записать указатель на объект
			; по данному клиентом адресу
	push eax	; указатель 'this'
	call IUnknown_AddRef
	mov eax,S_OK
	ret
IClassFactory_CreateInstance endp

Реализация метода LockServer осталась без изменений:

IClassFactory_LockServer proc thisptr:DWORD,fLock:DWORD
	.if fLock
		inc counter
	.else
		dec counter
	.endif
	invoke wsprintf,offset buf,offset ms5,counter
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,S_OK
	ret
IClassFactory_LockServer endp

Перейдем к реализации IUnknown нашего компонента. Как и в реализации фабрики классов, методы отображают простые окна сообщений, по наличию которых можно судить об их вызове.

IUnknown_QueryInterface	proc uses ebx esi thisptr:DWORD,
iid:DWORD,ppvObject:DWORD
	mov ebx,thisptr
	mov esi,ppvObject	; адрес переменной клиента, в которую
				; необходимо возвратить указатель интерфейса
	invoke wsprintf,offset buf,offset ms6,dword ptr [ebx+8]	; id объекта
	invoke MessageBox,0,offset buf,offset app,0

Проверка переданных параметров:

	.if ppvObject==0
		mov eax,E_INVALIDARG
		ret
	.endif
	invoke IsEqualGUID,offset IUnk,iid
	.if eax==0		; не IUnknown
		mov [esi],eax
		mov eax,E_NOINTERFACE
		ret
	.endif

Сюда попадаем, если затребован интерфейс IUnknown; возвращаем указатель на текущий объект (т.е. "this"), просто увеличив его счетчик ссылок. Указатель на объект был сохранен в регистре ebx, а в регистре esi содержится адрес переменной клиента, в которую надо записать возвращаемое значение:

	mov [esi],ebx		; указатель на текущий объект возвращаем
				; по адресу, указанному клиентом
	push ebx		; (указатель 'this')
	call IUnknown_AddRef
	mov eax,S_OK
	ret
IUnknown_QueryInterface	endp

Перейдем к оставшимся двум методам. Их особенность в том, что теперь счетчик объекта располагается в выделенной памяти, и обращаться к ней необходимо через указатель "this". Кроме того, Release должна уничтожить текущий объект при достижении счетчиком значения 0; это осуществляется путем освобождения выделенной ранее под структуру объекта памяти через тот же указатель "this".

IUnknown_AddRef	proc uses ebx thisptr:DWORD
	mov ebx,thisptr
	inc dword ptr [ebx+4]		; счетчик
	invoke wsprintf,offset buf,offset ms7,dword ptr [ebx+8],dword ptr [ebx+4]
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,dword ptr [ebx+4]	; значение счетчика
	ret
IUnknown_AddRef	endp

IUnknown_Release proc uses ebx thisptr:DWORD
	mov ebx,thisptr
	dec dword ptr [ebx+4]		; счетчик
	invoke wsprintf,offset buf,offset ms8,dword ptr [ebx+8],dword ptr [ebx+4]
	invoke MessageBox,0,offset buf,offset app,0
	.if dword ptr [ebx+4]==0
		invoke LocalFree,ebx	; удалить объект
		dec objcnt
		xor eax,eax
		ret
	.endif
	mov eax,dword ptr [ebx+4]	; значение счетчика
	ret
IUnknown_Release endp

end

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

Exe-серверы

Внепроцессные серверы COM оформляются в виде exe-модулей. Обычно они представляют собой построенные из компонентов самостоятельные приложения (такие как Word или Excel), доступ к которым можно получить с помощью интерфейсов COM. Такое приложение может быть запущено как обычным способом (например, двойным щелчком на названии соответствующего exe-файла в проводнике), так и системой COM с помощью содержащихся в реестре данных. Чтобы приложение могло различать эти два способа запуска, COM запускает приложение с аргументом командной строки '-Embedding'.

Чтобы убедиться в этом, создадим простейшее приложение (COM_9.asm):

.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data

app	db "CmdLine",0

.code

start:
	invoke GetCommandLine
	mov ecx,eax
	invoke MessageBox,0,ecx,offset app,0
	invoke ExitProcess,0
end start

Это приложение отображает в окне сообщения командную строку, с помощью которой оно было запущено. Запишем в подключе 'LocalServer32' нашего "хулиганского" ключа в реестре путь к построенному exe-файлу. Затем запустим наш клиент (COM_2.exe), пометив галочкой "Local server". Как видите, к пути файла добавлен аргумент '-Embedding'. (Наш клиент на время зависнет, как и в случае любого не-COM приложения).

Построим внепроцессный вариант нашего простейшего dll-сервера. Exe-сервер по сравнению с dll-сервером имеет несколько особенностей.

Во-первых, поскольку exe-сервер находится в собственном процессе, он сам должен инициализировать библиотеку COM, вызвав функцию CoInitialize(Ex):

invoke CoInitialize,0
.if eax!=S_OK
	invoke MessageBox,0,offset err1,offset app,MB_ICONERROR
	jmp errexit
.endif

Во-вторых, он должен создать и зарегистрировать объекты класса для компонентов, которые поддерживает. Для этого используется функция CoRegisterClassObject со следующими аргументами:

  • адрес структуры, содержащей CLSID компонента, для которого создается фабрика классов;
  • указатель интерфейса созданного объекта фабрики классов;
  • тип сервера;
  • флаги, указывающие, какой тип соединения с данным сервером поддерживается (например, могут ли сразу несколько клиентов одновременно вызывать объект фабрики классов);
  • адрес переменной, в которой будет возвращен регистрационный номер (он используется для последующего освобождения объекта класса).

На этот раз CLSID создаваемого компонента указывается при регистрации сервера явным образом, поэтому халявы, как в прошлый раз, не будет. Придется вшить CLSID компонента в код (используем наш старый {00000000-0000-0000-0000-000000000001}; он размещен в переменной clsid).

invoke CoRegisterClassObject,ADDR clsid,ADDR pVtbl,
CLSCTX_LOCAL_SERVER,REGCLS_SINGLEUSE,ADDR rgstr
.if eax!=S_OK
	invoke MessageBox,0,offset err2,offset app,MB_ICONERROR
	jmp errexit
.endif

В-третьих, exe-сервер должен иметь цикл обработки сообщений независимо от того, создает он окна или нет. Дело в том, что модель многопоточности COM реализована через механизм т.н. апартаментов: любой объект COM может находиться лишь в одном апартаменте. При инициализации COM создает апартамент для того потока, который вызывает CoInitialize(Ex). Апартамент, в свою очередь, создает скрытое окно, которому посылаются сообщения при вызове методов объекта из других апартаментов (в т.ч. и из других процессов). Именно таким образом (через очередь сообщений потока) осуществляется принудительная синхронизация доступа различных потоков к одному объекту в том случае, если этот объект не создан специально многопоточным. Поэтому любой поток (как клиента, так и сервера), который вызывает CoInitialize(Ex), должен иметь цикл обработки сообщений, содержащий функцию DispatchMessage.

mnloop:
	invoke GetMessage,ADDR m,0,0,0
	cmp eax,0
	je exit
	invoke DispatchMessage,ADDR m
	jmp mnloop
exit:
	invoke CoUninitialize
	invoke MessageBox,0,offset ms,offset app,0
	invoke ExitProcess,m.wParam
errexit:	
	invoke ExitProcess,-1
	ret

В-четвертых, функция CoRegisterClassObject при регистрации объекта в системной таблице вызывает через интерфейс IClassFactory объекта AddRef. Поэтому необходимо вести отдельный счетчик созданных фабрикой классов объектов и замков сервера, по достижении 0 которым вызывается функция CoRevokeClassObject. Этой функции передается единственный аргумент - регистрационный номер, полученный при вызове CoRegisterClassObject для соответствующего объекта.

Наш exe-сервер реализован "ленивым" способом: фабрика классов не создает новые объекты "компонента" (реализующего, как и в случае с dll-сервером, единственный интерфейс IUnknown), а лишь увеличивает счетчик ссылок единственного статического псевдообъекта, который к тому же имеет общий с замком сервера счетчик. При достижении этим счетчиком значения 0 (при вызове либо метода IUnknown_Release "объекта" компонента, либо метода IClassFactory_LockServer объекта класса) нужно отменить регистрацию фабрики классов. Вот реализации соответствующих методов:

IClassFactory_LockServer proc thisptr:DWORD,fLock:DWORD
	.if fLock
		invoke wsprintf,offset buf,offset ms5_1,objcount
		invoke MessageBox,0,offset buf,offset app,0
		inc objcount
	.else
		invoke wsprintf,offset buf,offset ms5_2,objcount
		invoke MessageBox,0,offset buf,offset app,0
		dec objcount
		jnz a2
		; здесь objcount==0; удалить последнюю ссылку
		; на IClassFactory, отменив ее регистрацию
		invoke CoRevokeClassObject,rgstr
a2:
	.endif
	mov eax,S_OK
	ret
IClassFactory_LockServer endp

IUnknown_Release proc thisptr:DWORD
	invoke wsprintf,offset buf,offset ms9,objcount
	invoke MessageBox,0,offset buf,offset app,0
	dec objcount
	jnz d1
	; здесь objcount==0; удалить последнюю ссылку
	; на IClassFactory, отменив ее регистрацию
	invoke CoRevokeClassObject,rgstr
d1:
	mov eax,objcount
	ret
IUnknown_Release endp

В-пятых, при удалении последней ссылки на объект класса для завершения работы сервера необходимо выйти из цикла обработки сообщений. Для этого посылается сообщение WM_QUIT; однако, наш сервер не создавал собственных окон, поэтому сообщение должно быть послано потоку (параметр дескриптора окна функции PostMessage равен 0):

IClassFactory_Release proc thisptr:DWORD
	invoke wsprintf,offset buf,offset ms3,counter
	invoke MessageBox,0,offset buf,offset app,0
	dec counter
	jnz e1
	; поскольку нет явно созданного окна,
	; послать сообщение о завершении потоку
	invoke PostMessage,0,WM_QUIT,0,0	
e1:
	mov eax,counter
	ret
IClassFactory_Release endp

В остальном реализация данного exe-сервера повторяет реализация нашего dll-сервера. Полностью исходные файлы можно найти в каталоге COM_10 архива ComKit2.rar.

Однако, тема нашей статьи не построение exe-серверов, а dll как основа COM, что мы и можем обнаружить в экспериментах с помощью только что созданной утилиты. Построив exe-модуль и записав путь к нему в подключ "LocalServer32", можно запустить нашего клиента (COM_2.exe) и посмотреть, каким образом осуществляется вызов методов внепроцессного сервера. Если до этого вы не имели дела с exe-серверами COM, вы будете удивлены обилием вызовов, которые поступают к нему от системы. Все дело в том, что в дело вступают очередные многочисленные посредники. Попробуем это выяснить.

Для этого реализуем в нашем компоненте простейший нестандартный интерфейс, назвав его, скажем, IFoo. Впрочем, название, как вы уже знаете, не играет особой роли; интерфейс должен иметь уникальный IID. В наших славных традициях присвоим ему IID {00000000-0000-0000-0000-000000000002}. :) Переделка потребуется самая минимальная (COM_11.asm):

  • в области данных добавать IID интерфейса:

IFoo	db 15 dup(0)
	db 2	; IID {00000000-0000-0000-0000-000000000002}

  • добавить одно смещение для дополнительного метода в виртуальной таблице для компонента:

Vtbl0	DWORD offset IFoo_QueryInterface
	DWORD offset IFoo_AddRef
	DWORD offset IFoo_Release
	DWORD offset IFoo_Member1	; новый метод

  • переделать код проверки IID интерфейса в методе QueryInterface для интерфейса компонента:

	invoke IsEqualGUID,iid,offset IUnk
	.if eax==0	; not IUnknown
		invoke IsEqualGUID,iid,offset IFoo
		.if eax==0	; none of 2 interfaces
			mov [ebx],eax
			invoke MessageBox,0,offset ms7_3,offset app,0
			mov eax,E_NOINTERFACE
			ret
		.else		; IFoo
			invoke MessageBox,0,offset ms7_2,offset app,0
		.endif
	.else	; IUnknown
		invoke MessageBox,0,offset ms7_1,offset app,0
	.endif

  • и, наконец, добавить реализацию самого дополнительного метода:

IFoo_Member1 proc thisptr:DWORD
	invoke MessageBox,0,offset fooms,offset app,0
	mov eax,S_OK
	ret
IFoo_Member1 endp

Строим модуль, записываем путь к нему в подключе LocalServer32 и запускаем клиента. Сначала запросим у компонента интерфейс IUnknown, чтобы убедиться, что все построено правильно и ошибок нет. Если все нормально, запрашиваем IID для нашего IFoo. Ошибка клиента! И по коду (80004002h) мы можем определить, что это E_NOINTERFACE. Но как может отсутствовать интерфейс, который мы реализовали собственными руками (или головой?)?

"Интерфейсная" часть объекта, как уже говорилось, всегда реализуется в dll. И если мы ее не строили, это не значит, что ее нет - она предоставляется системой. То, что мы в своем exe-сервере тоже реализовали "интерфейсную" часть, это явление вторичное - этот интерфейс нужен лишь для взаимодействия с системой COM стандартным образом; внепроцессный сервер может быть реализован вообще без использования интерфейсов COM, по крайней мере, нестандартных. Для этого ему нужно реализовать так называемый нестандартный маршалинг. Но это отдельная тема, и сейчас мы говорить о ней не будем.

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

Чтобы лучше узнать об этих запросах, слегка модернизируем наш сервер (COM_12.asm). Вероятно, вы обратили внимание, что система запрашивала у сервера множество посторонних интерфейсов. Попробуем узнать, что это за интерфейсы, изменив соответствующий участок IFoo_QueryInterface следующим образом (и заодно убрав лишние окна сообщений, оставив их лишь для QueryInterface):

invoke IsEqualGUID,iid,offset IFoo
.if eax==0	; none of 2 interfaces
	mov [ebx],eax
	invoke StringFromCLSID,iid,offset wbufptr
	invoke WideCharToMultiByte,CP_ACP,0,wbufptr,-1,offset iidbuf,128,0,0
	invoke lstrcpy,offset buf,offset ms7_3
	invoke lstrcat,offset buf,offset iidbuf
	invoke MessageBox,0,offset buf,offset app,0
	mov eax,E_NOINTERFACE
	ret

Следующий после IUnknown интерфейс, который система запрашивает у внепроцессного объекта, - IMarshal. Именно этот интерфейс позволяет осуществлять нестандартный маршалинг, и реализуя его, компонент заявляет системе, что он сам будет осуществлять маршалинг своих интерфейсов. Если же система в ответ на свой запрос получает E_NOINTERFACE, она пытается выяснить, есть ли у компонента собственный внутрипроцессный обработчик (запрашивая интерфейс IStdMarshalInfo). Выяснив, что и он не реализован, система окончательно переключается на стандартный маршалинг.

Стандартный маршалинг

Вот тут и вступает в действие раздел реестра "HKEY_CLASSES_ROOT\Interface", который мы обсуждали в прошлой статье. Система ищет в данном разделе ключ, соответствующий IID запрошенного интерфейса, и если не находит, мы получаем ответ E_NOINTERFACE. Если же такой ключ существует, возможны два варианта. Если этот ключ содержит подключ ProxyStubClsid32 (ProxyStubClsid для 16-разрядной версии), его значение по умолчанию является CLSID компонента, который служит для маршалинга данного интерфейса. Если же такого подключа нет, система пытается использовать свой собственный маршалер по умолчанию; если он не поддерживает данный интерфейс, также выдается ошибка E_NOINTERFACE.

Посмотрим, чего хотят от этого компонента. Напишем очередной фиктивный dll-сервер (COM_13.asm):

.386
.model flat,stdcall
option casemap:none

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

app		db "ProxyStub32",0
clsms		db "Requested CLSID:",13,10,0
iidms		db "Requested interface:",13,10,0
crlf		db 13,10,0
ms1		db "Dll is loading",0
ms2		db "Dll can unload",0

.data?
wbuf		db 256 dup (?)
iidbuf	db 128 dup (?)
buf		db 256 dup (?)
wbufptr	DWORD ?

.code

DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
	invoke StringFromCLSID,rclsid,offset wbufptr
	invoke WideCharToMultiByte,CP_ACP,0,wbufptr,-1,offset iidbuf,128,0,0
	invoke lstrcpy,offset buf,offset clsms
	invoke lstrcat,offset buf,offset iidbuf
	invoke lstrcat,offset buf,offset crlf
	invoke StringFromCLSID,riid,offset wbufptr
	invoke WideCharToMultiByte,CP_ACP,0,wbufptr,-1,offset iidbuf,128,0,0
	invoke lstrcat,offset buf,offset iidms
	invoke lstrcat,offset buf,offset iidbuf
	invoke MessageBox,0,offset buf,offset app,0
	xor ecx,ecx
	lea eax,ppv
	mov [eax],ecx
	mov eax,E_OUTOFMEMORY
	ret	
DllGetClassObject endp

DllCanUnloadNow	proc
	invoke MessageBox,0,offset ms2,offset app,0
	mov eax,S_OK
	ret
DllCanUnloadNow endp

end

Def-файл и опции ассемблирования те же, что использовались ранее при построении dll-сервера. При вызове DllGetClassObject эта функция отображает окно сообщения, указав запрошенные у нее CLSID и IID и каждый раз возвращая ошибку. Для внедрения этой dll в систему придется опять повозиться с реестром.

В разделе "HKEY_CLASSES_ROOT\Interface" необходимо создать ключ с IID нашего интерфейса ({00000000-0000-0000-0000-000000000002}). Под ним создадим еще два подключа: "NumMethods" со значением 4 и "ProxyStubClsid32" со значением CLSID компонента-маршалера нашего интерфейса; в качестве последнего выберем {00000000-0000-0000-0000-000000000003}. Такой же ключ нужно теперь создать и в разделе "HKEY_CLASSES_ROOT\CLSID", указав в подключе "InprocServer32" полный путь к построенной dll.

Вот теперь снова пробуем вызвать интерфейс IFoo. Как видим, после всех запросов интерфейсов у exe-сервера очередь доходит и до нашего ProxyStub - запрашивается интерфейс, по IID которого можно узнать, что это IPSFactoryBuffer.

Интерфейс IPSFactoryBuffer является своего рода фабрикой классов для объектов двух типов: представителей и заглушек нестандартных интерфейсов. Помимо методов IUnknown, этот интерфейс имеет еще два метода: CreateProxy и CreateStub, создающие соответствующие типы объектов.

Создадим "болванку" для интерфейса IPSFactoryBuffer (COM_14.asm):

.386
.model flat,stdcall
option casemap:none

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
; объект класса
pVtbl		DWORD offset Vtbl
counter	DWORD 0

; виртуальная таблица для интерфейса IPSFactoryBuffer
Vtbl	DWORD offset IPSFactoryBuffer_QueryInterface
	DWORD offset IPSFactoryBuffer_AddRef
	DWORD offset IPSFactoryBuffer_Release
	DWORD offset IPSFactoryBuffer_CreateProxy
	DWORD offset IPSFactoryBuffer_CreateStub

; IID интерфейсов
IUnk		dd 0			; IID для IUnknown
		dw 0
		dw 0
		db 0C0h,0,0,0,0,0,0,46h
IPSFB		dd 0D5F569D0h	; IID для IPSFactoryBuffer
		dw 593Bh
		dw 101Ah
		db 0B5h,69h,8,0,2Bh,2Dh,0BFh,7Ah

app		db "ProxyStub32",0
ms2		db "Dll can unload",0
msproxy	db "CreateProxy requested",0
msstub	db "CreateStub requested",0
msiunk	db "QueryInterface:",13,10,"IUnknown requested",0
msipsfb 	db "QueryInterface:",13,10,"IPSFactoryBuffer requested",0
msnone	db "QueryInterface:",13,10,"other interface requested",0

.code

DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
	; Проверка интерфейса осуществляется в функции
	; QueryInterface, которой этот вызов и перенаправляется
	push ppv
	push riid
	lea eax,pVtbl
	push eax	; this
	call IPSFactoryBuffer_QueryInterface
	ret	; переправить значение, возвращенное QueryInterface
DllGetClassObject endp

DllCanUnloadNow	proc
	.if counter>0
		mov eax,S_FALSE
	.else
		invoke MessageBox,0,offset ms2,offset app,0
		mov eax,S_OK
	.endif
	ret
DllCanUnloadNow endp

IPSFactoryBuffer_QueryInterface proc uses ebx,thisptr:DWORD,
iid:DWORD,ppvObject:DWORD
	.if ppvObject==0	; неверный параметр
		mov eax,E_INVALIDARG
	.endif
	mov ebx,ppvObject
	; принимает IID лишь для IUnknown и IPSFactoryBuffer
	invoke IsEqualGUID,iid,offset IUnk
	.if eax==0		; не IUnknown
		invoke IsEqualGUID,iid,offset IPSFB
		.if eax==0	; ни один из 2 интерфейсов
			mov [ebx],eax
			invoke MessageBox,0,offset msnone,offset app,0
			mov eax,E_NOINTERFACE
			ret
		.else		; IPSFactoryBuffer
			invoke MessageBox,0,offset msipsfb,offset app,0
		.endif
	.else			; IUnknown
		invoke MessageBox,0,offset msiunk,offset app,0
	.endif
	lea eax,pVtbl
	mov [ebx],eax
	push eax	; 'this'
	call IPSFactoryBuffer_AddRef
	mov eax,S_OK
	ret
IPSFactoryBuffer_QueryInterface endp

IPSFactoryBuffer_AddRef proc thisptr:DWORD
	inc counter
	mov eax,counter
	ret
IPSFactoryBuffer_AddRef endp

IPSFactoryBuffer_Release proc thisptr:DWORD
	dec counter
	mov eax,counter
	ret
IPSFactoryBuffer_Release endp

IPSFactoryBuffer_CreateProxy proc thisptr:DWORD,pUnkOuter:DWORD,
iid:DWORD,ppProxy:DWORD,ppv:DWORD
	invoke MessageBox,0,offset msproxy,offset app,0
	mov eax,E_NOINTERFACE
	ret
IPSFactoryBuffer_CreateProxy endp

IPSFactoryBuffer_CreateStub proc thisptr:DWORD,iid:DWORD,
pUnkServer:DWORD,ppStub:DWORD
	invoke MessageBox,0,offset msstub,offset app,0
	mov eax,E_NOINTERFACE
	ret
IPSFactoryBuffer_CreateStub endp

end

Чтобы этот сервер запускался, необходимо изменить путь в соответствующем подключе "InprocServer32".

Как можно видеть, ProxyStub - обычный внутрипроцессный сервер COM, реализующий другой тип фабрики классов. Чтобы и дальше проследить действия системы COM, можно было бы реализовать методы CreateProxy и CreateStub по-настоящему, но здесь мы уже выходим из царства COM и вступаем в царство RPC, а это совсем другой разговор.

2002-2013 (c) wasm.ru