3 кита COM. Кит второй: dll — Архив WASM.RU
Второй важнейшей основой, без которой невозможна инфраструктура COM, является использование DLL. Причем не просто использование, а очень активное использование; и использование не только самих DLL, но и тех принципов, которые применяются в технологии динамического связывания.
Технология COM претендует на универсальность своей компонентной модели. Это значит, что безболезненно взаимодействовать между собой должны не только компоненты, написанные на разных языках и созданные с использованием различных сред разработки, но и разные виды компонентов: и созданный специально для внедрения в другие приложения код DLL; и компоненты, входящие в EXE-модули самостоятельных приложений; и код, размещенный на другой машине и потенциально созданный для другой платформы. Естественно, сразу возникает проблема передачи данных через границы процессов и систем, а также единообразия работы с внутрипроцессными и внепроцессными объектами.
COM в качестве решения этой проблемы использует единый и единственный указатель интерфейса: все, что нужно для работы с объектом - это получить указатель на его интерфейс, и ничего более. Этот указатель представляет собой некий адрес в адресном пространстве клиента, по которому соответствующая область памяти структурирована определенным образом в соответствии с бинарным стандартом. А это значит, что область эта может принадлежать только DLL, и любой объект - будь он в другом процессе или на другой машине - имеет свою DLL (неважно, созданную разработчиком компонента или предоставленную системой), которая внедряется в адресное пространство клиента. Более того, именно эта часть объекта и предоставляет клиенту собственно интерфейс; можно сказать, что интерфейс объекта всегда реализуется в DLL, независимо от того, внутрипроцессный это объект, внепроцессный или удаленный.
"Интерфейсная" область структурируется иерархически с использованием других указателей наподобие связанного списка. Начинающих - и даже не только начинающих - COM-программистов часто смущает это обилие вложенных указателей. Чего стоит, например, аргумент метода QueryInterface в виде двойного указателя на неопределенный тип! Один этот void** языка С++ способен свести с ума, потому что понять его смысл невозможно в принципе. А если кто-то попытался использовать С и выяснил, что там на самом деле есть еще и скрываемая объектными языками виртуальная таблица - чтобы добраться до метода, необходимо тройное перенаправление?..
Даже авторы некоторых статей по COM-программированию на ассемблере как-то слишком уж поспешно проскакивают этот момент, бросаясь сразу в объятия спасительных макросов и предопределенных структур и пробормотав что-то невнятное о запутанности указателей, так что возникает подозрение, что они сами не до конца разобрались с этой проблемой. Между тем это вопрос, в котором можно (и нужно) разобраться - один раз, но досконально.
Попробуем спроектировать собственную объектную модель и посмотрим, какие проблемы при этом возникают. Главной идеей ООП было объединить данные и обрабатывающие их и семантически связанные с ними функции в одну конструкцию. Предположим, что у нас есть указатель на некий объект, представляющий собой область с данными, за которой непосредственно следуют обрабатывающие их методы (рис. 1). Доступ и к данным, и к методам этого объекта можно получить, используя простое смещение, добавляемое к значению указателя - как при работе с обычной структурой. Например, если указатель на объект находится в регистре eax, метод 2 можно вызвать следующим способом:
Код (Text):
add eax,m2 call eax
Рис. 1. Гипотетическая модель объекта, доступ к методам и данным которого осуществляется через один и тот же указательКакие могут тут быть проблемы? Если создаются несколько экземпляров одного и того же объекта, в памяти оказываются несколько копий одного и того же кода, хотя это совершенно излишне. Для решения этой проблемы данные и код можно разместить в различных секциях, причем секцию кода сделать общей, а секцию данных для каждого экземпляра объекта выделить свою. Именно по такому принципу строятся DLL (хотя формат PE-файла и соответствующие опции компиляторов позволяют создавать при необходимости и общие секции данных).
Как только появляется общая секция кода, возникает и вопрос отслеживания количества ссылок на него, чтобы своевременно освобождать память после завершения работы. Обычно эта проблема решается созданием счетчиков, значение которых увеличивается на один при создании нового экземпляра и уменьшается на один при его разрушении. Когда значение счетчика при очередном уменьшении достигает 0, секцию кода тоже можно выгрузить из памяти. Этот механизм используется операционной системой при загрузке и выгрузке DLL, но он не виден приложениям; в компонентной же модели он становится явным.
Рис. 2. Модель с различными секциями данных и единой секцией кода.Для того, чтобы обращаться и к данным, и к коду через один и тот же указатель объекта, в области данных можно записать указатель на общую для всех экземпляров секцию кода с реализацией методов объекта (рис. 2). При такой схеме методы объекта не имеют непосредственного неявного доступа к данным и последние придется передавать через аргументы, как в случае самых обычных процедур. А это разрушает саму идею объектного подхода - стоило ли ради этого огород городить?
Поэтому возникает еще одна схема, реализующая один из основных принципов ООП - инкапсуляцию: непосредственный доступ к данным объекта через указатель на объект закрывается - он превращается в "чёрный ящик" (рис. 3). Из всех данных доступным остается лишь указатель на секцию кода (благодаря тому, что он расположен в самом начале структуры данных и именно его адрес содержит указатель объекта) с реализацией методов объекта, которые непосредственно имеют дело с данными. Поскольку у каждого объекта своя секция данных, тогда как реализация методов одна для всех объектов, каждый метод при своем вызове в качестве первого параметра получает указатель на секцию данных именно того объекта, для которого вызывается метод. (В терминологии языка С++ этот параметр получил название указателя "this").
"Расплатой" за это является один уровень перенаправления, добавляемый на пути от указателя объекта к его методу. Теперь, если указатель объекта находится в регистре eax, метод вызывается следующим образом:
Код (Text):
; перенаправление: mov eax,[eax] ; получить адрес секции кода add eax,m2 ; добавить смещение второго метода call eax
Рис. 3. Реализация принципа инкапсуляции.Еще одна проблема возникает в связи с реализацией другого основного принципа ООП - полиморфизма. В среде COM времени исполнения нет имен. В принципе можно было бы создать и систему с использованием имен во время исполнения - например, DLL пользуются именно такой схемой. (К слову сказать, основанный на COM большой раздел OLE - автоматизация - использует модель позднего связывания, построенную именно по такому принципу. Но об этом поговорим как-нибудь в другой раз). Для доступа к методам вычисляются их адреса с использованием указателя интерфейса и постоянных смещений, а имена могут существовать лишь во время компиляции.
Традиционные языки программирования и среды разработки используют подобную же схему, однако в их случае вычисляемые смещения уникальны для каждой конкретной компиляции и они не используются нигде, кроме данного приложения. В случае же с компонентной моделью один и тот же интерфейс могут реализовать совершенно различные компоненты, и тут проявляется проблема смещений.
Предположим, имеются две реализации одного и того же интерфейса, содержащего три метода. Как видно на рис. 4, в двух реализациях методы требуют разного количества памяти, поэтому их смещения относительно начала секции кода оказываются различными. В результате код, который может работать с первой реализацией, не может работать со второй, и наоборот - полиморфизм невозможен.
Рис. 4. Проблема различных реализаций одного интерфейса.Для решения этой проблемы вводится еще один посредник между указателем на объект и реализацией его методов - виртуальная таблица (рис. 5). В секции данных объекта теперь располагается указатель не на секцию кода, а на виртуальную таблицу, в которой содержатся указатели на соответствующие методы. Смещение указателя на один и тот же метод относительно начала виртуальной таблицы всегда постоянно, хотя объем кода метода и его расположение в секции кода в различных реализациях могут сильно различаться. Но это вводит еще один уровень перенаправления. Вызов метода выглядит теперь следующим образом:
Код (Text):
; первое перенаправление: mov eax,[eax] ; получить адрес виртуальной таблицы ; второе перенаправление: mov eax,[eax+m2] ; получить адрес метода по смещению ; в виртуальной таблице call eaxПравда, это может быть записано короче таким образом:
Код (Text):
mov eax,[eax] ; первое перенаправление call dword ptr [eax+m2] ; косвенный вызов процедуры ; со вторым перенаправлением
Рис. 5. Реализация принципа полиморфизма.Полиморфизм означает, что в качестве реализации метода интерфейса может выступать абсолютно любой код, удовлетворяющий данному синтаксису вызова метода. Именно это и используется в реализации знаменитой "прозрачности удаленного доступа" COM (хотя этот подход использовался еще до COM в RPC). Благодаря полиморфизму код, реализующий интерфейс в той части сервера, которая размещается в DLL и загружается в адресное пространство клиента, может исполнять не алгоритм метода, а заниматься совершенно другими вещами - упаковывать полученные для метода параметры со всеми сопутствующими данными в некую структуру и отправлять ее куда-то в адресное пространство другого процесса или даже по сети на другую машину, где находится другая часть кода, реализующая собственно алгоритм метода. DLL выступает в этом случае "представителем" кода, размещенного в другом процессе или на другой системе. Клиент же ничего этого не видит; максимум, что он может заметить - увеличение задержек при выполнении методов.
Если свести все воедино, мы получим схему, приведенную на рис. 6. Это и является бинарной структурой интерфейса, используемой в 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, эти методы вызываются так:
Код (Text):
; здесь аргументы (если есть) помещаются в стек 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):
Код (Text):
.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):
Код (Text):
LIBRARY COM_6 EXPORTS DllGetClassObject PRIVATE DllCanUnloadNow PRIVATEКлючевое слово "PRIVATE" используется для того, чтобы экспортируемые функции не попали в создаваемый при построении DLL lib-файл - чтобы кто попало не мог импортировать эти функции, ибо они предназначены только "для своих" (т.е. системы COM). Хотя это обстоятельство может остановить разве что делающих все по правилам прикладных программистов, но никак не низкоуровневиков, способных подключиться непосредственно к DLL.
Строим DLL со следующими опциями (обратите внимание, наша DLL в данном конкретном случае не имеет функции инициализации и соответственно точки входа):
Код (Text):
\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):
Код (Text):
.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) и счетчик ссылок для данного объекта. Других данных у нашей реализации этого объекта нет. Дальше расположим саму виртуальную таблицу:
Код (Text):
Vtbl DWORD offset IUnknown_QueryInterface DWORD offset IUnknown_AddRef DWORD offset IUnknown_Release DWORD offset IClassFactory_CreateInstance DWORD offset IClassFactory_LockServerНа самом деле названия функций не играют никакой роли - можно было использовать любые названия. Важен порядок расположения функций в виртуальной таблице - он должен строго соответствовать интерфейсу. Само собой, указатели на функции нельзя чередовать с посторонними данными. В данном случае, в названии первых трех функций имеется префикс IUnknown, чтобы подчеркнуть, что эти члены наследуются от базового интерфейса, хотя все эти функции являются членами интерфейса IClassFactory.
Далее следуют обычные данные:
Код (Text):
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)Теперь реализации функций и методов.
Код (Text):
.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 должна возвращать результат, зависящий от его значения:
Код (Text):
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:
Код (Text):
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:
Код (Text):
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 должна удалить объект, освобождая занимаемую им память. В нашем же случае никакой объект реально не создавался, все данные были статическими, поэтому и уничтожать ничего не надо.
Код (Text):
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:
Код (Text):
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. В нашем случае замок использует общую со счетчиком объекта глобальную переменную:
Код (Text):
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):
Код (Text):
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):
Код (Text):
.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 ;тот самый счетчик созданных объектовСтатическая структура для "объекта класса":
Код (Text):
pVtbl DWORD offset Vtbl counter DWORD 0За ней следуют две виртуальные таблицы: первая - для реализации интерфейса IClassFactory объекта класса, вторая - для реализации (единственного) интерфейса IUnknown объекта компонента:
Код (Text):
; виртуальная таблица для объекта класса 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Далее идут обычные данные:
Код (Text):
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.
Код (Text):
.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. Теперь он создает настоящие объекты, выделяя для них память из кучи. Кроме того, нужно организовать проверку параметров и соответствующую обработку ошибок:
Код (Text):
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.
Код (Text):
invoke IsEqualGUID,offset IUnk,riid .if eax==0 ; не IUnknown mov [ebx],eax mov eax,E_NOINTERFACE ret .endifТеперь "сердцевина" метода: создаем объект. Как уже упоминалось, он представлен структурой из трех полей размером DWORD каждое, поэтому необходимо выделить из кучи блок памяти в 12 байт:
Код (Text):
invoke LocalAlloc,LPTR,12 .if eax==0 ; ошибка выделения памяти mov [ebx],eax mov eax,E_OUTOFMEMORY ret .endifВыделенную память необходимо инициализировать. Указатель на выделенный блок памяти остался в регистре eax; первый DWORD по этому адресу должен быть указателем на виртуальную таблицу реализации методов нашего объекта (Vtbl1):
Код (Text):
lea ecx,Vtbl1 mov [eax],ecx ; указатель на виртуальную таблицуВторой DWORD содержит счетчик ссылок на экземпляр данного объекта; при создании заносим сюда 0:
Код (Text):
push 0 pop [eax+4] ; счетчик нового объектаТретий DWORD - идентификатор объекта, в качетве которого выступает счетчик созданных объектов фабрики классов:
Код (Text):
inc objcnt ; еще один объект push objcnt pop [eax+8] ; идентификатор нового объектаОсталось вернуть указатель на вновь созданный объект клиенту, который для этой цели передал через аргумент ppvObject адрес соответствующей переменной (в начале метода мы скопировали его в ebx), а также увеличить счетчик ссылок на наш объект на 1:
Код (Text):
mov [ebx],eax ; записать указатель на объект ; по данному клиентом адресу push eax ; указатель 'this' call IUnknown_AddRef mov eax,S_OK ret IClassFactory_CreateInstance endpРеализация метода LockServer осталась без изменений:
Код (Text):
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 нашего компонента. Как и в реализации фабрики классов, методы отображают простые окна сообщений, по наличию которых можно судить об их вызове.
Код (Text):
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Проверка переданных параметров:
Код (Text):
.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 содержится адрес переменной клиента, в которую надо записать возвращаемое значение:
Код (Text):
mov [esi],ebx ; указатель на текущий объект возвращаем ; по адресу, указанному клиентом push ebx ; (указатель 'this') call IUnknown_AddRef mov eax,S_OK ret IUnknown_QueryInterface endpПерейдем к оставшимся двум методам. Их особенность в том, что теперь счетчик объекта располагается в выделенной памяти, и обращаться к ней необходимо через указатель "this". Кроме того, Release должна уничтожить текущий объект при достижении счетчиком значения 0; это осуществляется путем освобождения выделенной ранее под структуру объекта памяти через тот же указатель "this".
Код (Text):
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 endDef-файл и опции ассемблирования такие же, как ранее. Для экспериментов не забудьте изменить значение пути к серверу в реестре.
Exe-серверы
Внепроцессные серверы COM оформляются в виде exe-модулей. Обычно они представляют собой построенные из компонентов самостоятельные приложения (такие как Word или Excel), доступ к которым можно получить с помощью интерфейсов COM. Такое приложение может быть запущено как обычным способом (например, двойным щелчком на названии соответствующего exe-файла в проводнике), так и системой COM с помощью содержащихся в реестре данных. Чтобы приложение могло различать эти два способа запуска, COM запускает приложение с аргументом командной строки '-Embedding'.
Чтобы убедиться в этом, создадим простейшее приложение (COM_9.asm):
Код (Text):
.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):
Код (Text):
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).
Код (Text):
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.
Код (Text):
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 объекта класса) нужно отменить регистрацию фабрики классов. Вот реализации соответствующих методов:
Код (Text):
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):
Код (Text):
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 интерфейса:
Код (Text):
IFoo db 15 dup(0) db 2 ; IID {00000000-0000-0000-0000-000000000002}
- добавить одно смещение для дополнительного метода в виртуальной таблице для компонента:
Код (Text):
Vtbl0 DWORD offset IFoo_QueryInterface DWORD offset IFoo_AddRef DWORD offset IFoo_Release DWORD offset IFoo_Member1 ; новый метод
- переделать код проверки IID интерфейса в методе QueryInterface для интерфейса компонента:
Код (Text):
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
- и, наконец, добавить реализацию самого дополнительного метода:
Код (Text):
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):
Код (Text):
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):
Код (Text):
.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 endDef-файл и опции ассемблирования те же, что использовались ранее при построении 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):
Код (Text):
.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, а это совсем другой разговор. © Roustem
3 кита COM. Кит второй: dll
Дата публикации 3 янв 2007