3 кита COM. Кит второй: dll

Дата публикации 3 янв 2007

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

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

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

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

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

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

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

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

Код (Text):
  1. add eax,m2
  2. call eax
  3.  

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

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

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

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

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

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

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

Код (Text):
  1.         ; перенаправление:
  2. mov eax,[eax]   ; получить адрес секции кода
  3. add eax,m2  ; добавить смещение второго метода
  4. call eax
  5.  

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

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

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

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

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

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

Код (Text):
  1.             ; первое перенаправление:
  2. mov eax,[eax]       ; получить адрес виртуальной таблицы
  3.             ; второе перенаправление:
  4. mov eax,[eax+m2]    ; получить адрес метода по смещению
  5.             ; в виртуальной таблице
  6. call eax
  7.  

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

Код (Text):
  1. mov eax,[eax]       ; первое перенаправление
  2. call dword ptr [eax+m2] ; косвенный вызов процедуры
  3.             ; со вторым перенаправлением
  4.  

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

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

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

Бинарная структура интерфейса 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):
  1. ; здесь аргументы (если есть) помещаются в стек
  2. mov eax,pSomeInterface
  3. push eax            ; первый аргумент всех методов - "this"
  4. mov eax,[eax]
  5. call dword ptr [eax+x]      ; x=0 для QueryInterface,
  6.                 ; x=4 для AddRef,
  7.                 ; x=8 для Release
  8.  

Обратите внимание, здесь речь идет о наследовании интерфейсов - т.е. абстрактных структур - а не реализаций, как в объектно-ориентированных языках программирования. Для наследования реализаций или, как это называется в 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):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\user32.inc
  7. includelib \masm32\lib\user32.lib
  8.  
  9. .data
  10. ms1 db "DllGetClassObject",0
  11. ms2 db "DllCanUnloadNow",0
  12. app db "FooDll",0
  13. .code
  14.  
  15. DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
  16.     invoke MessageBox,0,addr ms1,addr app,0
  17.     mov eax,CLASS_E_CLASSNOTAVAILABLE
  18.     ret
  19. DllGetClassObject endp
  20.  
  21. DllCanUnloadNow proc
  22.     invoke MessageBox,0,addr ms2,addr app,0
  23.     mov eax,S_OK
  24.     ret
  25. DllCanUnloadNow endp
  26. end
  27.  

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

Код (Text):
  1. LIBRARY COM_6
  2. EXPORTS DllGetClassObject PRIVATE
  3.     DllCanUnloadNow PRIVATE
  4.  

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

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

Код (Text):
  1. \masm32\bin\ml /c /coff /Cp COM_6.asm
  2. \masm32\bin\Link /DLL /DEF:COM_6.def /NOENTRY /SUBSYSTEM:WINDOWS /VERSION:4.0 /LIBPATH:\masm32\lib COM_6.obj
  3.  

Предполагается, что 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". Остальное не описываю - сами увидите. Надеюсь, то, что клиент сообщает об ошибке, никого не удивляет. :smile3:

IClassFactory

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

Код (Text):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\user32.inc
  7. include \masm32\include\kernel32.inc
  8. include \masm32\include\ole32.inc
  9. includelib \masm32\lib\user32.lib
  10. includelib \masm32\lib\kernel32.lib
  11. includelib \masm32\lib\ole32.lib
  12.  
  13. .data
  14. pVtbl   DWORD offset Vtbl
  15. counter DWORD 0
  16.  

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

Код (Text):
  1. Vtbl    DWORD offset IUnknown_QueryInterface   
  2.     DWORD offset IUnknown_AddRef
  3.     DWORD offset IUnknown_Release
  4.     DWORD offset IClassFactory_CreateInstance
  5.     DWORD offset IClassFactory_LockServer
  6.  

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

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

Код (Text):
  1. IUnk    dd 0    ; IID интерфейса IUnknown
  2.     dw 0
  3.     dw 0
  4.     db 0C0h,0,0,0,0,0,0,46h
  5. ICFct   dd 1    ; IID интерфейса IClassFactory
  6.     dw 0
  7.     dw 0
  8.     db 0C0h,0,0,0,0,0,0,46h
  9. ms  db "Unloading",0
  10. ms0 db "DllGetClassObject",0
  11. ms1 db "QueryInterface",0
  12. ms2 db "AddRef",10,13,"counter = %i",0
  13. ms3 db "Release",10,13,"counter = %i",0
  14. ms4 db "CreateInstance",0
  15. ms5 db "LockServer",10,13,"counter = %i",0
  16. app db "ClassFactory",0
  17. buf db 128 dup(0)
  18.  

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

Код (Text):
  1. .code
  2. DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
  3.     invoke MessageBox,0,offset ms0,offset app,0
  4.     ; перенаправить вызов IUnknown_QueryInterface
  5.     push ppv    ; ppvObject
  6.     push riid   ; iid
  7.     lea eax,pVtbl
  8.     push eax    ; указатель 'this'
  9.     call IUnknown_QueryInterface
  10.     ret     ; возвратить результат QueryInterface
  11. DllGetClassObject endp
  12.  

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

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

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

Код (Text):
  1. DllCanUnloadNow proc
  2.     .if counter>0
  3.         mov eax,S_FALSE
  4.     .else
  5.         invoke MessageBox,0,offset ms,offset app,0
  6.         mov eax,S_OK
  7.     .endif
  8.     ret
  9. DllCanUnloadNow endp
  10.  

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

Код (Text):
  1. IUnknown_QueryInterface proc thisptr:DWORD,iid:DWORD,ppvObject:DWORD
  2.     invoke MessageBox,0,offset ms1,offset app,0
  3.     invoke IsEqualGUID,offset IUnk,iid
  4.     .if eax==0      ; не IUnknown
  5.         invoke IsEqualGUID,offset ICFct,iid
  6.         .if eax==0  ; не IClassFactory
  7.             mov eax,E_NOINTERFACE
  8.             ret
  9.         .endif
  10.     .endif
  11.  

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

Код (Text):
  1.     mov eax,ppvObject   ; адрес переменной для результата
  2.     lea ecx,pVtbl       ; указатель на IClassFactory
  3.     mov [eax],ecx
  4.     push ecx        ; указатель 'this'
  5.     call IUnknown_AddRef
  6.     mov eax,S_OK
  7.     ret
  8. IUnknown_QueryInterface endp
  9.  

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

Код (Text):
  1. IUnknown_AddRef proc thisptr:DWORD
  2.     inc counter
  3.     invoke wsprintf,offset buf,offset ms2,counter
  4.     invoke MessageBox,0,offset buf,offset app,0
  5.     mov eax,counter
  6.     ret
  7. IUnknown_AddRef endp
  8.  
  9. IUnknown_Release proc thisptr:DWORD
  10.     dec counter
  11.     invoke wsprintf,offset buf,offset ms3,counter
  12.     invoke MessageBox,0,offset buf,offset app,0
  13.     mov eax,counter
  14.     ret
  15. IUnknown_Release endp
  16.  

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

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

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

Код (Text):
  1. IClassFactory_CreateInstance proc thisptr:DWORD,pUnkOuter:DWORD,riid:DWORD,ppvObject:DWORD
  2.     mov eax,ppvObject
  3.     xor ecx,ecx
  4.     mov [eax],ecx   ; указатель интерфейса = NULL
  5.     invoke MessageBox,0,offset ms4,offset app,0
  6.     mov eax,E_NOINTERFACE
  7.     ret
  8. IClassFactory_CreateInstance endp
  9.  

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

Код (Text):
  1. IClassFactory_LockServer proc thisptr:DWORD,fLock:DWORD
  2.     .if fLock
  3.         inc counter
  4.     .else
  5.         dec counter
  6.     .endif
  7.     invoke wsprintf,offset buf,offset ms5,counter
  8.     invoke MessageBox,0,offset buf,offset app,0
  9.     mov eax,S_OK
  10.     ret
  11. IClassFactory_LockServer endp
  12. end
  13.  

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

Код (Text):
  1. LIBRARY COM_7
  2. EXPORTS DllGetClassObject   PRIVATE
  3.         DllCanUnloadNow PRIVATE
  4.  

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

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

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

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

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

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

Код (Text):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\user32.inc
  7. include \masm32\include\kernel32.inc
  8. include \masm32\include\ole32.inc
  9. includelib \masm32\lib\user32.lib
  10. includelib \masm32\lib\kernel32.lib
  11. includelib \masm32\lib\ole32.lib
  12.  
  13. .data
  14. objcnt  DWORD 0 ;тот самый счетчик созданных объектов
  15.  

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

Код (Text):
  1. pVtbl       DWORD offset Vtbl
  2. counter DWORD 0
  3.  

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

Код (Text):
  1. ; виртуальная таблица для объекта класса
  2. Vtbl    DWORD offset IClassFactory_QueryInterface  
  3.     DWORD offset IClassFactory_AddRef
  4.     DWORD offset IClassFactory_Release
  5.     DWORD offset IClassFactory_CreateInstance
  6.     DWORD offset IClassFactory_LockServer
  7.  
  8. ; виртуальная таблица для объекта компонента
  9. Vtbl1   DWORD offset IUnknown_QueryInterface
  10.     DWORD offset IUnknown_AddRef
  11.     DWORD offset IUnknown_Release
  12.  

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

Код (Text):
  1. IUnk    dd 0    ; IID интерфейса IUnknown
  2.     dw 0
  3.     dw 0
  4.     db 0C0h,0,0,0,0,0,0,46h
  5. ICFct   dd 1    ; IID интерфейса IClassFactory
  6.     dw 0
  7.     dw 0
  8.     db 0C0h,0,0,0,0,0,0,46h
  9. ms  db "Unloading",0
  10. ms0 db "DllGetClassObject",0
  11. ms1 db "ClassFactroy:",10,13,"QueryInterface",0
  12. ms2 db "ClassFactory:",10,13,"AddRef",10,13,"counter = %i",0
  13. ms3 db "ClassFactory:",10,13,"Release",10,13,"counter = %i",0
  14. ms4 db "ClassFactory:",10,13,"CreateInstance",0
  15. ms5 db "ClassFactory:",10,13,"LockServer",10,13,"counter = %i",0
  16. app db "DllServer",0
  17. ms6 db "Object %i:",10,13,"QueryInterface",0
  18. ms7 db "Object %i:",10,13,"AddRef",10,13,"objref: %i",0
  19. ms8 db "Object %i:",10,13,"Release",10,13,"objref: %i",0
  20. buf db 128 dup(0)
  21.  

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

Код (Text):
  1. .code
  2.  
  3. DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
  4.     invoke MessageBox,0,offset ms0,offset app,0
  5.     ; перенаправить вызов IClassFactory_QueryInterface
  6.     push ppv    ; ppvObject
  7.     push riid   ; iid
  8.     lea eax,pVtbl
  9.     push eax    ; указатель 'this'
  10.     call IClassFactory_QueryInterface
  11.     ret     ; возвращает результат вызова QueryInterface
  12. DllGetClassObject endp
  13.  
  14. DllCanUnloadNow proc
  15.     .if counter>0 || objcnt>0
  16.         mov eax,S_FALSE
  17.     .else
  18.         invoke MessageBox,0,offset ms,offset app,0
  19.         mov eax,S_OK
  20.     .endif
  21.     ret
  22. DllCanUnloadNow endp
  23.  
  24. IClassFactory_QueryInterface    proc thisptr:DWORD,iid:DWORD,ppvObject:DWORD
  25.     invoke MessageBox,0,offset ms1,offset app,0
  26.     .if ppvObject==0
  27.         mov eax, E_INVALIDARG
  28.         ret
  29.     .endif
  30.     invoke IsEqualGUID,offset IUnk,iid
  31.     .if eax==0      ; не  IUnknown
  32.         invoke IsEqualGUID,offset ICFct,iid
  33.         .if eax==0  ; не IClassFactory
  34.             mov eax,E_NOINTERFACE
  35.             ret
  36.         .endif
  37.     .endif
  38.     ; iid является либо IUnknown, либо IClassFactory
  39.     mov eax,ppvObject   ; адрес переменной для указателя интерфейса
  40.     lea ecx,pVtbl       ; указатель на IClassFactory
  41.     mov [eax],ecx
  42.     push ecx        ; указатель 'this'
  43.     call IClassFactory_AddRef
  44.     mov eax,S_OK
  45.     ret
  46. IClassFactory_QueryInterface    endp
  47.  
  48. IClassFactory_AddRef    proc thisptr:DWORD
  49.     inc counter
  50.     invoke wsprintf,offset buf,offset ms2,counter
  51.     invoke MessageBox,0,offset buf,offset app,0
  52.     mov eax,counter
  53.     ret
  54. IClassFactory_AddRef    endp
  55.  
  56. IClassFactory_Release proc thisptr:DWORD
  57.     dec counter
  58.     invoke wsprintf,offset buf,offset ms3,counter
  59.     invoke MessageBox,0,offset buf,offset app,0
  60.     mov eax,counter
  61.     ret
  62. IClassFactory_Release endp
  63.  

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

Код (Text):
  1. IClassFactory_CreateInstance proc uses ebx thisptr:DWORD,pUnkOuter:DWORD,
  2. riid:DWORD,ppvObject:DWORD
  3.     invoke MessageBox,0,offset ms4,offset app,0
  4.     .if ppvObject==0
  5.         mov eax,E_INVALIDARG
  6.         ret
  7.     .endif
  8.     mov ebx,ppvObject   ; сохраним в регистре адрес переменной
  9.                 ; для указателя интерфейса объекта
  10. .if pUnkOuter!=0
  11.     xor ecx,ecx
  12.     mov [ebx],ecx
  13.         mov eax,CLASS_E_NOAGGREGATION
  14.         ret
  15.     .endif
  16.  

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

Код (Text):
  1.     invoke IsEqualGUID,offset IUnk,riid
  2.     .if eax==0  ; не IUnknown
  3.         mov [ebx],eax
  4.         mov eax,E_NOINTERFACE
  5.         ret
  6.     .endif
  7.  

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

Код (Text):
  1.     invoke LocalAlloc,LPTR,12
  2.     .if eax==0  ; ошибка выделения памяти
  3.         mov [ebx],eax
  4.         mov eax,E_OUTOFMEMORY
  5.         ret
  6.     .endif
  7.  

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

Код (Text):
  1.     lea ecx,Vtbl1
  2.     mov [eax],ecx   ; указатель на виртуальную таблицу
  3.  

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

Код (Text):
  1.     push 0
  2.     pop [eax+4] ; счетчик нового объекта
  3.  

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

Код (Text):
  1.     inc objcnt  ; еще один объект
  2.     push objcnt
  3.     pop [eax+8] ; идентификатор нового объекта
  4.  

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

Код (Text):
  1.     mov [ebx],eax   ; записать указатель на объект
  2.             ; по данному клиентом адресу
  3.     push eax    ; указатель 'this'
  4.     call IUnknown_AddRef
  5.     mov eax,S_OK
  6.     ret
  7. IClassFactory_CreateInstance endp
  8.  

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

Код (Text):
  1. IClassFactory_LockServer proc thisptr:DWORD,fLock:DWORD
  2.     .if fLock
  3.         inc counter
  4.     .else
  5.         dec counter
  6.     .endif
  7.     invoke wsprintf,offset buf,offset ms5,counter
  8.     invoke MessageBox,0,offset buf,offset app,0
  9.     mov eax,S_OK
  10.     ret
  11. IClassFactory_LockServer endp
  12.  

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

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

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

Код (Text):
  1.     .if ppvObject==0
  2.         mov eax,E_INVALIDARG
  3.         ret
  4.     .endif
  5.     invoke IsEqualGUID,offset IUnk,iid
  6.     .if eax==0      ; не IUnknown
  7.         mov [esi],eax
  8.         mov eax,E_NOINTERFACE
  9.         ret
  10.     .endif
  11.  

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

Код (Text):
  1.     mov [esi],ebx       ; указатель на текущий объект возвращаем
  2.                 ; по адресу, указанному клиентом
  3.     push ebx        ; (указатель 'this')
  4.     call IUnknown_AddRef
  5.     mov eax,S_OK
  6.     ret
  7. IUnknown_QueryInterface endp
  8.  

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

Код (Text):
  1. IUnknown_AddRef proc uses ebx thisptr:DWORD
  2.     mov ebx,thisptr
  3.     inc dword ptr [ebx+4]       ; счетчик
  4.     invoke wsprintf,offset buf,offset ms7,dword ptr [ebx+8],dword ptr [ebx+4]
  5.     invoke MessageBox,0,offset buf,offset app,0
  6.     mov eax,dword ptr [ebx+4]   ; значение счетчика
  7.     ret
  8. IUnknown_AddRef endp
  9.  
  10. IUnknown_Release proc uses ebx thisptr:DWORD
  11.     mov ebx,thisptr
  12.     dec dword ptr [ebx+4]       ; счетчик
  13.     invoke wsprintf,offset buf,offset ms8,dword ptr [ebx+8],dword ptr [ebx+4]
  14.     invoke MessageBox,0,offset buf,offset app,0
  15.     .if dword ptr [ebx+4]==0
  16.         invoke LocalFree,ebx    ; удалить объект
  17.         dec objcnt
  18.         xor eax,eax
  19.         ret
  20.     .endif
  21.     mov eax,dword ptr [ebx+4]   ; значение счетчика
  22.     ret
  23. IUnknown_Release endp
  24.  
  25. end
  26.  

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

Exe-серверы

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

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

Код (Text):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\user32.inc
  7. include \masm32\include\kernel32.inc
  8. includelib \masm32\lib\user32.lib
  9. includelib \masm32\lib\kernel32.lib
  10.  
  11. .data
  12.  
  13. app db "CmdLine",0
  14.  
  15. .code
  16.  
  17. start:
  18.     invoke GetCommandLine
  19.     mov ecx,eax
  20.     invoke MessageBox,0,ecx,offset app,0
  21.     invoke ExitProcess,0
  22. end start
  23.  

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

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

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

Код (Text):
  1. invoke CoInitialize,0
  2. .if eax!=S_OK
  3.     invoke MessageBox,0,offset err1,offset app,MB_ICONERROR
  4.     jmp errexit
  5. .endif
  6.  

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

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

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

Код (Text):
  1. invoke CoRegisterClassObject,ADDR clsid,ADDR pVtbl,
  2. CLSCTX_LOCAL_SERVER,REGCLS_SINGLEUSE,ADDR rgstr
  3. .if eax!=S_OK
  4.     invoke MessageBox,0,offset err2,offset app,MB_ICONERROR
  5.     jmp errexit
  6. .endif
  7.  

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

Код (Text):
  1. mnloop:
  2.     invoke GetMessage,ADDR m,0,0,0
  3.     cmp eax,0
  4.     je exit
  5.     invoke DispatchMessage,ADDR m
  6.     jmp mnloop
  7. exit:
  8.     invoke CoUninitialize
  9.     invoke MessageBox,0,offset ms,offset app,0
  10.     invoke ExitProcess,m.wParam
  11. errexit:   
  12.     invoke ExitProcess,-1
  13.     ret
  14.  

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

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

Код (Text):
  1. IClassFactory_LockServer proc thisptr:DWORD,fLock:DWORD
  2.     .if fLock
  3.         invoke wsprintf,offset buf,offset ms5_1,objcount
  4.         invoke MessageBox,0,offset buf,offset app,0
  5.         inc objcount
  6.     .else
  7.         invoke wsprintf,offset buf,offset ms5_2,objcount
  8.         invoke MessageBox,0,offset buf,offset app,0
  9.         dec objcount
  10.         jnz a2
  11.         ; здесь objcount==0; удалить последнюю ссылку
  12.         ; на IClassFactory, отменив ее регистрацию
  13.         invoke CoRevokeClassObject,rgstr
  14. a2:
  15.     .endif
  16.     mov eax,S_OK
  17.     ret
  18. IClassFactory_LockServer endp
  19.  
  20. IUnknown_Release proc thisptr:DWORD
  21.     invoke wsprintf,offset buf,offset ms9,objcount
  22.     invoke MessageBox,0,offset buf,offset app,0
  23.     dec objcount
  24.     jnz d1
  25.     ; здесь objcount==0; удалить последнюю ссылку
  26.     ; на IClassFactory, отменив ее регистрацию
  27.     invoke CoRevokeClassObject,rgstr
  28. d1:
  29.     mov eax,objcount
  30.     ret
  31. IUnknown_Release endp
  32.  

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

Код (Text):
  1. IClassFactory_Release proc thisptr:DWORD
  2.     invoke wsprintf,offset buf,offset ms3,counter
  3.     invoke MessageBox,0,offset buf,offset app,0
  4.     dec counter
  5.     jnz e1
  6.     ; поскольку нет явно созданного окна,
  7.     ; послать сообщение о завершении потоку
  8.     invoke PostMessage,0,WM_QUIT,0,0   
  9. e1:
  10.     mov eax,counter
  11.     ret
  12. IClassFactory_Release endp
  13.  

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

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

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

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

Код (Text):
  1. IFoo    db 15 dup(0)
  2.     db 2    ; IID {00000000-0000-0000-0000-000000000002}
  3.  

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

Код (Text):
  1. Vtbl0   DWORD offset IFoo_QueryInterface
  2.     DWORD offset IFoo_AddRef
  3.     DWORD offset IFoo_Release
  4.     DWORD offset IFoo_Member1   ; новый метод
  5.  

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

Код (Text):
  1.     invoke IsEqualGUID,iid,offset IUnk
  2.     .if eax==0  ; not IUnknown
  3.         invoke IsEqualGUID,iid,offset IFoo
  4.         .if eax==0  ; none of 2 interfaces
  5.             mov [ebx],eax
  6.             invoke MessageBox,0,offset ms7_3,offset app,0
  7.             mov eax,E_NOINTERFACE
  8.             ret
  9.         .else       ; IFoo
  10.             invoke MessageBox,0,offset ms7_2,offset app,0
  11.         .endif
  12.     .else   ; IUnknown
  13.         invoke MessageBox,0,offset ms7_1,offset app,0
  14.     .endif
  15.  

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

Код (Text):
  1. IFoo_Member1 proc thisptr:DWORD
  2.     invoke MessageBox,0,offset fooms,offset app,0
  3.     mov eax,S_OK
  4.     ret
  5. IFoo_Member1 endp
  6.  

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

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

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

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

Код (Text):
  1. invoke IsEqualGUID,iid,offset IFoo
  2. .if eax==0  ; none of 2 interfaces
  3.     mov [ebx],eax
  4.     invoke StringFromCLSID,iid,offset wbufptr
  5.     invoke WideCharToMultiByte,CP_ACP,0,wbufptr,-1,offset iidbuf,128,0,0
  6.     invoke lstrcpy,offset buf,offset ms7_3
  7.     invoke lstrcat,offset buf,offset iidbuf
  8.     invoke MessageBox,0,offset buf,offset app,0
  9.     mov eax,E_NOINTERFACE
  10.     ret
  11.  

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

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

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

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

Код (Text):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\user32.inc
  7. include \masm32\include\kernel32.inc
  8. include \masm32\include\ole32.inc
  9. includelib \masm32\lib\user32.lib
  10. includelib \masm32\lib\kernel32.lib
  11. includelib \masm32\lib\ole32.lib
  12.  
  13. .data
  14.  
  15. app     db "ProxyStub32",0
  16. clsms       db "Requested CLSID:",13,10,0
  17. iidms       db "Requested interface:",13,10,0
  18. crlf        db 13,10,0
  19. ms1     db "Dll is loading",0
  20. ms2     db "Dll can unload",0
  21.  
  22. .data?
  23. wbuf        db 256 dup (?)
  24. iidbuf  db 128 dup (?)
  25. buf     db 256 dup (?)
  26. wbufptr DWORD ?
  27.  
  28. .code
  29.  
  30. DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
  31.     invoke StringFromCLSID,rclsid,offset wbufptr
  32.     invoke WideCharToMultiByte,CP_ACP,0,wbufptr,-1,offset iidbuf,128,0,0
  33.     invoke lstrcpy,offset buf,offset clsms
  34.     invoke lstrcat,offset buf,offset iidbuf
  35.     invoke lstrcat,offset buf,offset crlf
  36.     invoke StringFromCLSID,riid,offset wbufptr
  37.     invoke WideCharToMultiByte,CP_ACP,0,wbufptr,-1,offset iidbuf,128,0,0
  38.     invoke lstrcat,offset buf,offset iidms
  39.     invoke lstrcat,offset buf,offset iidbuf
  40.     invoke MessageBox,0,offset buf,offset app,0
  41.     xor ecx,ecx
  42.     lea eax,ppv
  43.     mov [eax],ecx
  44.     mov eax,E_OUTOFMEMORY
  45.     ret
  46. DllGetClassObject endp
  47.  
  48. DllCanUnloadNow proc
  49.     invoke MessageBox,0,offset ms2,offset app,0
  50.     mov eax,S_OK
  51.     ret
  52. DllCanUnloadNow endp
  53.  
  54. end
  55.  

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

Код (Text):
  1. .386
  2. .model flat,stdcall
  3. option casemap:none
  4.  
  5. include \masm32\include\windows.inc
  6. include \masm32\include\user32.inc
  7. include \masm32\include\kernel32.inc
  8. include \masm32\include\ole32.inc
  9. includelib \masm32\lib\user32.lib
  10. includelib \masm32\lib\kernel32.lib
  11. includelib \masm32\lib\ole32.lib
  12.  
  13. .data
  14. ; объект класса
  15. pVtbl       DWORD offset Vtbl
  16. counter DWORD 0
  17.  
  18. ; виртуальная таблица для интерфейса IPSFactoryBuffer
  19. Vtbl    DWORD offset IPSFactoryBuffer_QueryInterface
  20.     DWORD offset IPSFactoryBuffer_AddRef
  21.     DWORD offset IPSFactoryBuffer_Release
  22.     DWORD offset IPSFactoryBuffer_CreateProxy
  23.     DWORD offset IPSFactoryBuffer_CreateStub
  24.  
  25. ; IID интерфейсов
  26. IUnk        dd 0            ; IID для IUnknown
  27.         dw 0
  28.         dw 0
  29.         db 0C0h,0,0,0,0,0,0,46h
  30. IPSFB       dd 0D5F569D0h   ; IID для IPSFactoryBuffer
  31.         dw 593Bh
  32.         dw 101Ah
  33.         db 0B5h,69h,8,0,2Bh,2Dh,0BFh,7Ah
  34.  
  35. app     db "ProxyStub32",0
  36. ms2     db "Dll can unload",0
  37. msproxy db "CreateProxy requested",0
  38. msstub  db "CreateStub requested",0
  39. msiunk  db "QueryInterface:",13,10,"IUnknown requested",0
  40. msipsfb     db "QueryInterface:",13,10,"IPSFactoryBuffer requested",0
  41. msnone  db "QueryInterface:",13,10,"other interface requested",0
  42.  
  43. .code
  44.  
  45. DllGetClassObject proc rclsid:DWORD, riid:DWORD, ppv:DWORD
  46.     ; Проверка интерфейса осуществляется в функции
  47.     ; QueryInterface, которой этот вызов и перенаправляется
  48.     push ppv
  49.     push riid
  50.     lea eax,pVtbl
  51.     push eax    ; this
  52.     call IPSFactoryBuffer_QueryInterface
  53.     ret ; переправить значение, возвращенное QueryInterface
  54. DllGetClassObject endp
  55.  
  56. DllCanUnloadNow proc
  57.     .if counter>0
  58.         mov eax,S_FALSE
  59.     .else
  60.         invoke MessageBox,0,offset ms2,offset app,0
  61.         mov eax,S_OK
  62.     .endif
  63.     ret
  64. DllCanUnloadNow endp
  65.  
  66. IPSFactoryBuffer_QueryInterface proc uses ebx,thisptr:DWORD,
  67. iid:DWORD,ppvObject:DWORD
  68.     .if ppvObject==0    ; неверный параметр
  69.         mov eax,E_INVALIDARG
  70.     .endif
  71.     mov ebx,ppvObject
  72.     ; принимает IID лишь для IUnknown и IPSFactoryBuffer
  73.     invoke IsEqualGUID,iid,offset IUnk
  74.     .if eax==0      ; не IUnknown
  75.         invoke IsEqualGUID,iid,offset IPSFB
  76.         .if eax==0  ; ни один из 2 интерфейсов
  77.             mov [ebx],eax
  78.             invoke MessageBox,0,offset msnone,offset app,0
  79.             mov eax,E_NOINTERFACE
  80.             ret
  81.         .else       ; IPSFactoryBuffer
  82.             invoke MessageBox,0,offset msipsfb,offset app,0
  83.         .endif
  84.     .else           ; IUnknown
  85.         invoke MessageBox,0,offset msiunk,offset app,0
  86.     .endif
  87.     lea eax,pVtbl
  88.     mov [ebx],eax
  89.     push eax    ; 'this'
  90.     call IPSFactoryBuffer_AddRef
  91.     mov eax,S_OK
  92.     ret
  93. IPSFactoryBuffer_QueryInterface endp
  94.  
  95. IPSFactoryBuffer_AddRef proc thisptr:DWORD
  96.     inc counter
  97.     mov eax,counter
  98.     ret
  99. IPSFactoryBuffer_AddRef endp
  100.  
  101. IPSFactoryBuffer_Release proc thisptr:DWORD
  102.     dec counter
  103.     mov eax,counter
  104.     ret
  105. IPSFactoryBuffer_Release endp
  106.  
  107. IPSFactoryBuffer_CreateProxy proc thisptr:DWORD,pUnkOuter:DWORD,
  108. iid:DWORD,ppProxy:DWORD,ppv:DWORD
  109.     invoke MessageBox,0,offset msproxy,offset app,0
  110.     mov eax,E_NOINTERFACE
  111.     ret
  112. IPSFactoryBuffer_CreateProxy endp
  113.  
  114. IPSFactoryBuffer_CreateStub proc thisptr:DWORD,iid:DWORD,
  115. pUnkServer:DWORD,ppStub:DWORD
  116.     invoke MessageBox,0,offset msstub,offset app,0
  117.     mov eax,E_NOINTERFACE
  118.     ret
  119. IPSFactoryBuffer_CreateStub endp
  120.  
  121. end
  122.  

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

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


0 1.604
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532