В моей предыдущей статье объяснялось, как использовать COM-объекты в ваших программах, написанных на ассемблере. Там говорилось только о том, как вызывать COM-методы, а не как создавать свои COM-объекты. Эта статья расскажет, как это делать.
В этой статье будет затронуто реализация COM-объектов, используя синтаксис MASM. Здесь не будут браться в расчет ассемблеры TASM или NASM, хотя используемые методы легко применить к любому ассемблеру.
В этой статье не будут объсняться продвинутые технологии COM, такие как повтороное использование, трединг, серверы/клиенты и т.д. Об этом будет рассказано в будущих статьях.
Обзор интерфейсов COM
Определение интерфейса задает методы интерфейса, возвращаемые ими значения, количество и типы их параметров и что эти методы должны делать. Далее идет пример простого определения интерфейса:
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 ends
STDMETHOD используется для упрощения объявления интерфейса и определяется следующим образом:
ENDM
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'ом :) )
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 можно представить как следующую структуру:
GUID STRUCT
Data1 dd ?
Data2 dw ?
Data3 dw ?
Data4 db 8 dup(?)
GUID ENDS
Затем GUID объявляется в секции данных:
MyGUID GUID <3F2504E0h, 4f89h, 11D3h, <9Ah, 0C3h, 0h, 0h, 0E8h, 2Ch, 3h,1h>>
Как только GUID ассоциирован с интерфейсом и опубликован, никаких дальнейших изменений в его определении быть не должно. Обратите внимание, это не означает, что не может меняться реализация интерфейса. Если необходимо изменение интерфейса, должен быть использован новый GUID.
COM-объекты
COM-объект - это просто реализация интерфейса. Детали реализации не затрагиваются стандартами COM, поэтому мы можем реализовывать объекты как угодно, пока это удовлетворяет всем требованиям определения интерфейса.
Типичный объект будет содержать указатели на различные интерфейсы, которые он поддерживает, счетчик ссылок и другие данные, которые могут потребоваться объекту. Вот простое определение объекта, реализованное в виде структуры:
Object struct
interface IInterface <?> ; указатель на IInterface
nRefCount dd ? ; счетчик ссылок
nValue dd ? ; приватные данные объекта
Object ends
Также мы должны определить vtable'ы, которые будут использоваться. Эти таблицы должны быть статическими и не могут меняться во время выполнения программы. Каждый член 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
Подсчет ссылок
COM-объект управляет продолжительностью своей жизни с помощью подсчета ссылок. У каждого объекта есть счетчик ссылок, отслеживающий, как много экземпляров указателя на интерфейс было создано. Счетчик объекта должен поддерживать значение до 2^32, то есть он должен быть DWORD.
Когда счетчик ссылок падает до нуля, объект больше не используется и разрушает сам себя. Два метода интерфейса IUnknown AddRef и Release обрабатывают подсчет ссылок для COM-объекта.
QueryInterface
Метод QueryInterface используется, чтобы определить, поддерживается ли объектом определенный интерфейс, и если да, позволяет получить указатель на него. Есть три правила реализации метода QueryInterface:
QueryInterface возвращает указатель на указанный интерфейс объекта, указатель на интерфейс которого уже есть у клиента. Эта функция должна вызывать метод AddRef указателя, который она возвращает.
Вот описание аргументов QueryInterface:
pif : [in] указатель на вызывающий интерфейс
riid : [in] указатель на IID интерфейса, который запрашивается
ppv : [out] указатель на указатель на интерфейс, который запрашивается.
Если интерфейс не поддерживается, значение переменной будет
приравнено 0.
QueryInterface возвращает следующее:
S_OK, если интерфейс поддерживается
E_NOINTERFACE, если не подерживается
Вот простая ассемблерная реализация QueryInterface:
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 endp
AddRef
Метод AddRef используется для повышения значения счетчика ссылок для интерфейса объекта. Он должен вызываться для каждой новой копии указателя на интерфейс объекта.
AddRef не принимает параметров, кроме указателя на интерфейс, что требуется для всех методов. AddRef должен возвращать новое значение счетчика ссылок. Тем не менее, это значение должно использоваться вызывающими только в тестовых целях, так как в определенных ситуациях оно может быть нестабильно.
Далее идет простая реализация метода AddRef:
IInterface@AddRef proc pif:DWORD
GETOBJECTPOINTER Object, interface, pif
; увеличиваем значение счетчика ссылок
inc [(Object ptr [eax]).nRefCount]
; теперь вовращяем значение ссылок
mov eax, [(Object ptr [eax]).nRefCount]
ret
IInterface@AddRef endp
Release
Release понижает значение счетчика ссылок вызывающего интерфейса объекта. Если значение счетчика объекта снижается до 0, то объект выгружается из памяти. Эта функция должна вызываться, когда в указателе на интерфейс больше нет надобности.
Как и AddRef, Release пpинимает только один аpгумент - указатель на интеpфейс. Он также возвpащает текущее значение счетчика ссылок, котоpый также следует использовать только для тестиpования.
Вот простая реализация Release:
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-объекта. Демонстрируется, как создать объект, вызвать его методы, а затем освободить их. Вероятно, будет довольно познавательно скомпилировать данный пример и пройтись по нему отладчиком.
.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 определяет только интерфейсы, а реализацию оставляет на программиста. Эта статья показывает только одну возможную реализацию. Это не единственный метод и не самый лучший. Читатель не должен бояться экспериментировать с другими методами.
[C] Bill T., пер. Aquila