COM в Ассемблере - Часть II — Архив WASM.RU
В моей предыдущей статье объяснялось, как использовать COM-объекты в ваших программах, написанных на ассемблере. Там говорилось только о том, как вызывать COM-методы, а не как создавать свои COM-объекты. Эта статья расскажет, как это делать.
В этой статье будет затронуто реализация COM-объектов, используя синтаксис MASM. Здесь не будут браться в расчет ассемблеры TASM или NASM, хотя используемые методы легко применить к любому ассемблеру.
В этой статье не будут объсняться продвинутые технологии COM, такие как повтороное использование, трединг, серверы/клиенты и т.д. Об этом будет рассказано в будущих статьях.
Обзор интерфейсов COM
Определение интерфейса задает методы интерфейса, возвращаемые ими значения, количество и типы их параметров и что эти методы должны делать. Далее идет пример простого определения интерфейса:
Код (Text):
IInterface struct lpVtbl dd ? IInterface ends IInterfaceVtbl struct ; методы IUnknown STDMETHOD QueryInterface, :DWORD, :DWORD, :DWORD STDMETHOD AddRef, :DWORD STDMETHOD Release, :DWORD ; методы IInterface STDMETHOD Method1, :DWORD STDMETHOD Method2, :DWORD IInterfaceVtbl endsSTDMETHOD используется для упрощения объявления интерфейса и определяется следующим образом:
ENDMКод (Text):
STDMETHOD MACRO name, argl :VARARG LOCAL @tmp_a LOCAL @tmp_b @tmp_a TYPEDEF PROTO argl @tmp_b TYPEDEF PTR @tmp_a name @tmp_b ?Использование этого макроса значительно упрощает объявления интерфейсов и делает возможным использование команды invoke. (Макрос написан Ewald'ом )
Код (Text):
mov eax, [lpif] ; lpif - указатель на интерфейс mov eax, [eax] ; получаем адрес vtable invoke (IInterfaceVtbl [eax]).Method1, [lpif] ; косвенный вызов функции - or - invoke [eax][IInterfaceVtbl.Method2], [lpif] ; альтернативная форма ; записиКакую форму записи использовать - дело вкуса. В обоих случаях генерируется один и тот же код.
Все интерфейсы должны наследоваться от интерфейса IUnknown. Это означает, что первые 3 метода vtable должны быть QueryInterface, AddRef и Release. Цель и реализация этих методов будет обсуждаться ниже.
GUID'ы
GUID - это глобальный уникальный ID. GUID - это 16-ти байтное число, которое уникально у каждого интерфейса. COM использует GUID'ы, чтобы отличать интерфейсы друг от друга. Использование этого метода предотвращает проблемы с совпадением имен или версий. Чтобы получить GUID, вы можете использовать утилиту, которая включена в большинство пакетов разработки программ под Win32.
GUID можно представить как следующую структуру:
Код (Text):
GUID STRUCT Data1 dd ? Data2 dw ? Data3 dw ? Data4 db 8 dup(?) GUID ENDSЗатем GUID объявляется в секции данных:
Код (Text):
MyGUID GUID <3F2504E0h, 4f89h, 11D3h, <9Ah, 0C3h, 0h, 0h, 0E8h, 2Ch, 3h,1h>>Как только GUID ассоциирован с интерфейсом и опубликован, никаких дальнейших изменений в его определении быть не должно. Обратите внимание, это не означает, что не может меняться реализация интерфейса. Если необходимо изменение интерфейса, должен быть использован новый GUID.
COM-объекты
COM-объект - это просто реализация интерфейса. Детали реализации не затрагиваются стандартами COM, поэтому мы можем реализовывать объекты как угодно, пока это удовлетворяет всем требованиям определения интерфейса.
Типичный объект будет содержать указатели на различные интерфейсы, которые он поддерживает, счетчик ссылок и другие данные, которые могут потребоваться объекту. Вот простое определение объекта, реализованное в виде структуры:
Код (Text):
Object struct interface IInterface <?> ; указатель на IInterface nRefCount dd ? ; счетчик ссылок nValue dd ? ; приватные данные объекта Object endsТакже мы должны определить vtable'ы, которые будут использоваться. Эти таблицы должны быть статическими и не могут меняться во время выполнения программы. Каждый член vtable - это указатель на метод. Далее показывается, как определить vtable.
Код (Text):
@@IInterface segment dword vtblIInterface: dd offset IInterface@QueryInterface dd offset IInterface@AddRef dd offset IInterface@Release dd offset IInterface@GetValue dd offset IInterface@SetValue @@IInterface endsПодсчет ссылок
COM-объект управляет продолжительностью своей жизни с помощью подсчета ссылок. У каждого объекта есть счетчик ссылок, отслеживающий, как много экземпляров указателя на интерфейс было создано. Счетчик объекта должен поддерживать значение до 2^32, то есть он должен быть DWORD.
Когда счетчик ссылок падает до нуля, объект больше не используется и разрушает сам себя. Два метода интерфейса IUnknown AddRef и Release обрабатывают подсчет ссылок для COM-объекта.
QueryInterface
Метод QueryInterface используется, чтобы определить, поддерживается ли объектом определенный интерфейс, и если да, позволяет получить указатель на него. Есть три правила реализации метода QueryInterface:
- Объекты должны быть идентичны - вызов QueryInterface должен всегда возвращать одно и то же значение.
- Набор интерфейсов объекта не должен меняться - например, если вызов QueryInterface с неким IID был однажды успешен, то он должен быть успешным всегда. Таким же образом, если вызов однажды не удался, то не должен удасться в другой раз.
- Должно быть возможно проверить наличие одного интерфейса объекта из другого интерфейса.
QueryInterface возвращает указатель на указанный интерфейс объекта, указатель на интерфейс которого уже есть у клиента. Эта функция должна вызывать метод AddRef указателя, который она возвращает.
Вот описание аргументов QueryInterface:
Код (Text):
pif : [in] указатель на вызывающий интерфейс riid : [in] указатель на IID интерфейса, который запрашивается ppv : [out] указатель на указатель на интерфейс, который запрашивается. Если интерфейс не поддерживается, значение переменной будет приравнено 0.QueryInterface возвращает следующее:
Код (Text):
S_OK, если интерфейс поддерживается E_NOINTERFACE, если не подерживаетсяВот простая ассемблерная реализация QueryInterface:
Код (Text):
IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD ; Следующий код сравнивает затребованный IID с доступными. Так как ; интерфейс IInterface наследуется от IUnknown, эти два интерфейса ; разделяют один и тот же указатель на интерфейс. invoke IsEqualGUID, [riid], addr IID_IInterface or eax,eax jnz @1 invoke IsEqualGUID, [riid], addr IID_IUnknown or eax,eax jnz @1 jmp @NoInterface @1: ; GETOBJECTPOINTER - это макрос, который поместит указатель на объект ; в eax, если дано имя объекта, имя интерфейса и указатель на интерфейс. GETOBJECTPOINTER Object, interface, pif ; теперь получаем указатель на затребованный интерфейс lea eax, (Object ptr [eax]).interface ; заполняем *ppv указателем на интерфейс mov ebx, [ppv] mov dword ptr [ebx], eax ; повышаем значение счетчика ссылок, вызывая AddRef GETOBJECTPOINTER Object, interface, pif mov eax, (Object ptr [eax]).interface invoke (IInterfaceVtbl ptr [eax]).AddRef, pif ; возвpащаем S_OK mov eax, S_OK jmp return @NoInterface: ; интерфейс не поддерживается, поэтому заполняем *ppv нулем mov eax, [ppv] mov dword ptr [eax], 0 ; return E_NOINTERFACE mov eax, E_NOINTERFACE return: ret IInterface@QueryInterface endpAddRef
Метод AddRef используется для повышения значения счетчика ссылок для интерфейса объекта. Он должен вызываться для каждой новой копии указателя на интерфейс объекта.
AddRef не принимает параметров, кроме указателя на интерфейс, что требуется для всех методов. AddRef должен возвращать новое значение счетчика ссылок. Тем не менее, это значение должно использоваться вызывающими только в тестовых целях, так как в определенных ситуациях оно может быть нестабильно.
Далее идет простая реализация метода AddRef:
Код (Text):
IInterface@AddRef proc pif:DWORD GETOBJECTPOINTER Object, interface, pif ; увеличиваем значение счетчика ссылок inc [(Object ptr [eax]).nRefCount] ; теперь вовращяем значение ссылок mov eax, [(Object ptr [eax]).nRefCount] ret IInterface@AddRef endpRelease
Release понижает значение счетчика ссылок вызывающего интерфейса объекта. Если значение счетчика объекта снижается до 0, то объект выгружается из памяти. Эта функция должна вызываться, когда в указателе на интерфейс больше нет надобности.
Как и AddRef, Release пpинимает только один аpгумент - указатель на интеpфейс. Он также возвpащает текущее значение счетчика ссылок, котоpый также следует использовать только для тестиpования.
Вот простая реализация Release:
Код (Text):
IInterface@Release proc pif:DWORD GETOBJECTPOINTER Object, interface, pif ; decrement the reference count ; понижаем значение счетчика ссылок dec [(Object ptr [eax]).nRefCount] ; проверяем, равно ли значение счетчика ссылок нулю. Если так, то ; выгружаем объект mov eax, [(Object ptr [eax]).nRefCount] or eax, eax jnz @1 ; освобождаем объект: здесь мы предполагаем, что объект был ; зарезервирован функцией LocalAlloc и с опцией LMEM_FIXED GETOBJECTPOINTER Object, interface, pif invoke LocalFree, eax @1: ret IInterface@Release endpСоздание COM-объекта
Создание объекта состоит из резервирования памяти для объекта, а затем инициализации его данных. Как правило, инициализируется указатель на vtable и обнуляется счетчик ссылок. Затем можно вызывать QueryInterface, чтобы получить указатель на интерфейс.
Есть и другие методы для создания объектов, такие как CoCreateInstance и использование фабрик классов. Эти методы не будут обсуждаться в данной статье.
Пример реализации COM-объекта
Здесь приводится простая реализация COM-объекта. Демонстрируется, как создать объект, вызвать его методы, а затем освободить их. Вероятно, будет довольно познавательно скомпилировать данный пример и пройтись по нему отладчиком.
Код (Text):
.386 .model flat,stdcall include windows.inc include kernel32.inc include user32.inc includelib kernel32.lib includelib user32.lib includelib uuid.lib ;-------------------------------------------------------------------------- ; Borrowed from Ewald, http://here.is/diamond/ ; Макрос для упрощения объявлений интерфейсов STDMETHOD MACRO name, argl :VARARG LOCAL @tmp_a LOCAL @tmp_b @tmp_a TYPEDEF PROTO argl @tmp_b TYPEDEF PTR @tmp_a name @tmp_b ? ENDM ; Макрос, который получает указатель на интерфейс и возвращает указатель ; на реализацию в eax GETOBJECTPOINTER MACRO Object, Interface, pif mov eax, pif IF (Object.Interface) sub eax, Object.Interface ENDIF ENDM ;-------------------------------------------------------------------------- IInterface@QueryInterface proto :DWORD, :DWORD, :DWORD IInterface@AddRef proto :DWORD IInterface@Release proto :DWORD IInterface@Get proto :DWORD IInterface@Set proto :DWORD, :DWORD CreateObject proto :DWORD IsEqualGUID proto :DWORD, :DWORD externdef IID_IUnknown:GUID ;-------------------------------------------------------------------------- ; объявляем прототип интерфейса IInterface struct lpVtbl dd ? IInterface ends IInterfaceVtbl struct ; методы IUnknown STDMETHOD QueryInterface, pif:DWORD, riid:DWORD, ppv:DWORD STDMETHOD AddRef, pif:DWORD STDMETHOD Release, pif:DWORD ; методы IInterface STDMETHOD GetValue, pif:DWORD STDMETHOD SetValue, pif:DWORD, val:DWORD IInterfaceVtbl ends ; объявляем структуру объекта Object struct ; интерфейс объекта interface IInterface <?> ; данные объекта nRefCount dd ? nValue dd ? Object ends ;-------------------------------------------------------------------------- .data ; define the vtable ; определяем vtable @@IInterface segment dword vtblIInterface: dd offset IInterface@QueryInterface dd offset IInterface@AddRef dd offset IInterface@Release dd offset IInterface@GetValue dd offset IInterface@SetValue @@IInterface ends ; определяем IID интерфейса ; {CF2504E0-4F89-11d3-9AC3-0000E82C0301} IID_IInterface GUID <0cf2504e0h, 04f89h, 011d3h, <09ah, 0c3h, 00h, 00h, \ 0e8h, 02ch, 03h, 01h>> ;-------------------------------------------------------------------------- .code start: StartProc proc LOCAL pif:DWORD ; указатель на интерфейс ; вызываем метод SetValue mov eax, [pif] mov eax, [eax] invoke (IInterfaceVtbl ptr [eax]).SetValue, [pif], 12345h ; вызываем метод GetValue mov eax, [pif] mov eax, [eax] invoke (IInterfaceVtbl ptr [eax]).GetValue, [pif] ; освобождаем объект mov eax, [pif] mov eax, [eax] invoke (IInterfaceVtbl ptr [eax]).Release, [pif] ret StartProc endp ;-------------------------------------------------------------------------- IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD invoke IsEqualGUID, [riid], addr IID_IInterface test eax,eax jnz @F invoke IsEqualGUID, [riid], addr IID_IUnknown test eax,eax jnz @F jmp @Error @@: GETOBJECTPOINTER Object, interface, pif lea eax, (Object ptr [eax]).interface ; устанавливаем значение *ppv mov ebx, [ppv] mov dword ptr [ebx], eax ; увеличиваем значение счетчика ссылок GETOBJECTPOINTER Object, interface, pif mov eax, (Object ptr [eax]).interface invoke (IInterfaceVtbl ptr [eax]).AddRef, [pif] ; возвpащаем S_OK mov eax, S_OK jmp return @Error: ; ошибка, интерфейс не поддерживается mov eax, [ppv] mov dword ptr [eax], 0 mov eax, E_NOINTERFACE return: ret IInterface@QueryInterface endp IInterface@AddRef proc pif:DWORD GETOBJECTPOINTER Object, interface, pif inc [(Object ptr [eax]).nRefCount] mov eax, [(Object ptr [eax]).nRefCount] ret IInterface@AddRef endp IInterface@Release proc pif:DWORD GETOBJECTPOINTER Object, interface, pif dec [(Object ptr [eax]).nRefCount] mov eax, [(Object ptr [eax]).nRefCount] or eax, eax jnz @1 ; free object mov eax, [pif] mov eax, [eax] invoke LocalFree, eax @1: ret IInterface@Release endp IInterface@GetValue proc pif:DWORD GETOBJECTPOINTER Object, interface, pif mov eax, (Object ptr [eax]).nValue ret IInterface@GetValue endp IInterface@SetValue proc uses ebx pif:DWORD, val:DWORD GETOBJECTPOINTER Object, interface, pif mov ebx, eax mov eax, [val] mov (Object ptr [ebx]).nValue, eax ret IInterface@SetValue endp ;-------------------------------------------------------------------------- CreateObject proc uses ebx ecx pobj:DWORD ; set *ppv to 0 mov eax, pobj mov dword ptr [eax], 0 ; pезеpвиpуем объект invoke LocalAlloc, LMEM_FIXED, sizeof Object or eax, eax jnz @1 ; alloc failed, so return mov eax, E_OUTOFMEMORY jmp return @1: mov ebx, eax mov (Object ptr [ebx]).interface.lpVtbl, offset vtblIInterface mov (Object ptr [ebx]).nRefCount, 0 mov (Object ptr [ebx]).nValue, 0 ; Запpашиваем интеpфейс lea ecx, (Object ptr [ebx]).interface mov eax, (Object ptr [ebx]).interface.lpVtbl invoke (IInterfaceVtbl ptr [eax]).QueryInterface, ecx, addr IID_IInterface, [pobj] cmp eax, S_OK je return ; ошибка в QueryInterface, поэтому освобождаем память push eax invoke LocalFree, ebx pop eax return: ret CreateObject endp ;-------------------------------------------------------------------------- IsEqualGUID proc rguid1:DWORD, rguid2:DWORD cld mov esi, [rguid1] mov edi, [rguid2] mov ecx, sizeof GUID / 4 repe cmpsd xor eax, eax or ecx, ecx setz eax ret IsEqualGUID endp end startЗаключение
Мы увидели (надеюсь), как реализовать COM-объект. Мы можем видеть, что это связанно с определенными трудностями и увеличивает избыточность кода нашей программы. Тем не менее, это может добавить гибкость и силу в наши программы. Подробности по данной теме вы можете найти на моей маленькой странице, посвященной COM: http://lordlucifer.cjb.net.
Помните, что COM определяет только интерфейсы, а реализацию оставляет на программиста. Эта статья показывает только одну возможную реализацию. Это не единственный метод и не самый лучший. Читатель не должен бояться экспериментировать с другими методами. © Bill T., пер. Aquila
COM в Ассемблере - Часть II
Дата публикации 27 июн 2002