COM в Ассемблере - Часть II

Дата публикации 27 июн 2002

COM в Ассемблере - Часть II — Архив WASM.RU

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

В этой статье будет затронуто реализация COM-объектов, используя синтаксис MASM. Здесь не будут браться в расчет ассемблеры TASM или NASM, хотя используемые методы легко применить к любому ассемблеру.

В этой статье не будут объсняться продвинутые технологии COM, такие как повтороное использование, трединг, серверы/клиенты и т.д. Об этом будет рассказано в будущих статьях.

Обзор интерфейсов COM

Определение интерфейса задает методы интерфейса, возвращаемые ими значения, количество и типы их параметров и что эти методы должны делать. Далее идет пример простого определения интерфейса:

Код (Text):
  1.  
  2.  IInterface struct
  3.      lpVtbl  dd  ?
  4.  IInterface ends
  5.  
  6.  IInterfaceVtbl struct
  7.      ; методы IUnknown
  8.      STDMETHOD       QueryInterface, :DWORD, :DWORD, :DWORD
  9.      STDMETHOD       AddRef, :DWORD
  10.      STDMETHOD       Release, :DWORD
  11.      ; методы IInterface
  12.      STDMETHOD       Method1, :DWORD
  13.      STDMETHOD       Method2, :DWORD
  14.  IInterfaceVtbl ends

STDMETHOD используется для упрощения объявления интерфейса и определяется следующим образом:

Код (Text):
  1.  
  2.  STDMETHOD MACRO name, argl :VARARG
  3.      LOCAL @tmp_a
  4.      LOCAL @tmp_b
  5.      @tmp_a TYPEDEF PROTO argl
  6.      @tmp_b TYPEDEF PTR @tmp_a
  7.      name @tmp_b ?
ENDM

Использование этого макроса значительно упрощает объявления интерфейсов и делает возможным использование команды invoke. (Макрос написан Ewald'ом :smile3: )

Код (Text):
  1.  
  2.  mov     eax, [lpif]                         ; lpif - указатель на интерфейс
  3.  mov     eax, [eax]                          ; получаем адрес vtable
  4.  invoke  (IInterfaceVtbl [eax]).Method1, [lpif] ; косвенный вызов функции
  5.  - or -
  6.  invoke  [eax][IInterfaceVtbl.Method2], [lpif]  ; альтернативная форма
  7.                                                 ; записи

Какую форму записи использовать - дело вкуса. В обоих случаях генерируется один и тот же код.

Все интерфейсы должны наследоваться от интерфейса IUnknown. Это означает, что первые 3 метода vtable должны быть QueryInterface, AddRef и Release. Цель и реализация этих методов будет обсуждаться ниже.

GUID'ы

GUID - это глобальный уникальный ID. GUID - это 16-ти байтное число, которое уникально у каждого интерфейса. COM использует GUID'ы, чтобы отличать интерфейсы друг от друга. Использование этого метода предотвращает проблемы с совпадением имен или версий. Чтобы получить GUID, вы можете использовать утилиту, которая включена в большинство пакетов разработки программ под Win32.

GUID можно представить как следующую структуру:

Код (Text):
  1.  
  2.  GUID STRUCT
  3.      Data1   dd ?
  4.      Data2   dw ?
  5.      Data3   dw ?
  6.      Data4   db 8 dup(?)
  7.  GUID ENDS

Затем GUID объявляется в секции данных:

Код (Text):
  1.  
  2.  MyGUID GUID <3F2504E0h, 4f89h, 11D3h, <9Ah, 0C3h, 0h, 0h, 0E8h, 2Ch, 3h,1h>>

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

COM-объекты

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

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

Код (Text):
  1.  
  2.  Object struct
  3.      interface   IInterface  <?>     ; указатель на IInterface
  4.      nRefCount   dd          ?       ; счетчик ссылок
  5.      nValue      dd          ?       ; приватные данные объекта
  6.  Object ends

Также мы должны определить vtable'ы, которые будут использоваться. Эти таблицы должны быть статическими и не могут меняться во время выполнения программы. Каждый член vtable - это указатель на метод. Далее показывается, как определить vtable.

Код (Text):
  1.  
  2.  @@IInterface segment dword
  3.  vtblIInterface:
  4.      dd      offset IInterface@QueryInterface
  5.      dd      offset IInterface@AddRef
  6.      dd      offset IInterface@Release
  7.      dd      offset IInterface@GetValue
  8.      dd      offset IInterface@SetValue
  9.  @@IInterface ends

Подсчет ссылок

COM-объект управляет продолжительностью своей жизни с помощью подсчета ссылок. У каждого объекта есть счетчик ссылок, отслеживающий, как много экземпляров указателя на интерфейс было создано. Счетчик объекта должен поддерживать значение до 2^32, то есть он должен быть DWORD.

Когда счетчик ссылок падает до нуля, объект больше не используется и разрушает сам себя. Два метода интерфейса IUnknown AddRef и Release обрабатывают подсчет ссылок для COM-объекта.

QueryInterface

Метод QueryInterface используется, чтобы определить, поддерживается ли объектом определенный интерфейс, и если да, позволяет получить указатель на него. Есть три правила реализации метода QueryInterface:

  1. Объекты должны быть идентичны - вызов QueryInterface должен всегда возвращать одно и то же значение.
  2. Набор интерфейсов объекта не должен меняться - например, если вызов QueryInterface с неким IID был однажды успешен, то он должен быть успешным всегда. Таким же образом, если вызов однажды не удался, то не должен удасться в другой раз.
  3. Должно быть возможно проверить наличие одного интерфейса объекта из другого интерфейса.

QueryInterface возвращает указатель на указанный интерфейс объекта, указатель на интерфейс которого уже есть у клиента. Эта функция должна вызывать метод AddRef указателя, который она возвращает.

Вот описание аргументов QueryInterface:

Код (Text):
  1.  
  2.      pif  : [in] указатель на вызывающий интерфейс
  3.      riid : [in] указатель на IID интерфейса, который запрашивается
  4.      ppv  : [out] указатель на указатель на интерфейс, который запрашивается.
  5.             Если интерфейс не поддерживается, значение переменной будет
  6.             приравнено 0.

QueryInterface возвращает следующее:

Код (Text):
  1.  
  2.     S_OK, если интерфейс поддерживается
  3.     E_NOINTERFACE, если не подерживается

Вот простая ассемблерная реализация QueryInterface:

Код (Text):
  1.  
  2.  IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD
  3.      ; Следующий код сравнивает затребованный IID с доступными. Так как
  4.      ; интерфейс IInterface наследуется от IUnknown, эти два интерфейса
  5.      ; разделяют один и тот же указатель на интерфейс.
  6.      invoke  IsEqualGUID, [riid], addr IID_IInterface
  7.      or      eax,eax
  8.      jnz     @1
  9.      invoke  IsEqualGUID, [riid], addr IID_IUnknown
  10.      or      eax,eax
  11.      jnz     @1
  12.      jmp     @NoInterface
  13.  
  14.  @1:
  15.      ; GETOBJECTPOINTER - это макрос, который поместит указатель на объект
  16.      ; в eax, если дано имя объекта, имя интерфейса и указатель на интерфейс.
  17.      GETOBJECTPOINTER    Object, interface, pif
  18.  
  19.      ; теперь получаем указатель на затребованный интерфейс
  20.      lea     eax, (Object ptr [eax]).interface
  21.  
  22.      ; заполняем *ppv указателем на интерфейс
  23.      mov     ebx, [ppv]
  24.      mov     dword ptr [ebx], eax
  25.  
  26.      ; повышаем значение счетчика ссылок, вызывая AddRef
  27.      GETOBJECTPOINTER    Object, interface, pif
  28.      mov     eax, (Object ptr [eax]).interface
  29.      invoke  (IInterfaceVtbl ptr [eax]).AddRef, pif
  30.  
  31.      ; возвpащаем S_OK
  32.      mov     eax, S_OK
  33.      jmp     return
  34.  
  35.  @NoInterface:
  36.      ; интерфейс не поддерживается, поэтому заполняем *ppv нулем
  37.      mov     eax, [ppv]
  38.      mov     dword ptr [eax], 0
  39.  
  40.      ; return E_NOINTERFACE
  41.      mov     eax, E_NOINTERFACE
  42.  
  43.  return:
  44.      ret
  45.  IInterface@QueryInterface endp

AddRef

Метод AddRef используется для повышения значения счетчика ссылок для интерфейса объекта. Он должен вызываться для каждой новой копии указателя на интерфейс объекта.

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

Далее идет простая реализация метода AddRef:

Код (Text):
  1.  
  2.  IInterface@AddRef proc pif:DWORD
  3.      GETOBJECTPOINTER    Object, interface, pif
  4.      ; увеличиваем значение счетчика ссылок
  5.      inc     [(Object ptr [eax]).nRefCount]
  6.      ; теперь вовращяем значение ссылок
  7.      mov     eax, [(Object ptr [eax]).nRefCount]
  8.      ret
  9.  IInterface@AddRef endp

Release

Release понижает значение счетчика ссылок вызывающего интерфейса объекта. Если значение счетчика объекта снижается до 0, то объект выгружается из памяти. Эта функция должна вызываться, когда в указателе на интерфейс больше нет надобности.

Как и AddRef, Release пpинимает только один аpгумент - указатель на интеpфейс. Он также возвpащает текущее значение счетчика ссылок, котоpый также следует использовать только для тестиpования.

Вот простая реализация Release:

Код (Text):
  1.  
  2.  IInterface@Release proc pif:DWORD
  3.      GETOBJECTPOINTER    Object, interface, pif
  4.  
  5.      ; decrement the reference count
  6.      ; понижаем значение счетчика ссылок
  7.      dec     [(Object ptr [eax]).nRefCount]
  8.  
  9.      ; проверяем, равно ли значение счетчика ссылок нулю. Если так, то
  10.      ; выгружаем объект
  11.      mov     eax, [(Object ptr [eax]).nRefCount]
  12.      or      eax, eax
  13.      jnz     @1
  14.  
  15.      ; освобождаем объект: здесь мы предполагаем, что объект был
  16.      ; зарезервирован функцией LocalAlloc и с опцией LMEM_FIXED
  17.      GETOBJECTPOINTER    Object, interface, pif
  18.      invoke  LocalFree, eax
  19.  @1:
  20.      ret
  21.  IInterface@Release endp

Создание COM-объекта

Создание объекта состоит из резервирования памяти для объекта, а затем инициализации его данных. Как правило, инициализируется указатель на vtable и обнуляется счетчик ссылок. Затем можно вызывать QueryInterface, чтобы получить указатель на интерфейс.

Есть и другие методы для создания объектов, такие как CoCreateInstance и использование фабрик классов. Эти методы не будут обсуждаться в данной статье.

Пример реализации COM-объекта

Здесь приводится простая реализация COM-объекта. Демонстрируется, как создать объект, вызвать его методы, а затем освободить их. Вероятно, будет довольно познавательно скомпилировать данный пример и пройтись по нему отладчиком.

Код (Text):
  1.  
  2.  .386
  3.  .model flat,stdcall
  4.  
  5.  include windows.inc
  6.  include kernel32.inc
  7.  include user32.inc
  8.  
  9.  includelib kernel32.lib
  10.  includelib user32.lib
  11.  includelib uuid.lib
  12.  
  13.  ;--------------------------------------------------------------------------
  14.  
  15.  ; Borrowed from Ewald, http://here.is/diamond/
  16.  ; Макрос для упрощения объявлений интерфейсов
  17.  STDMETHOD   MACRO   name, argl :VARARG
  18.  LOCAL @tmp_a
  19.  LOCAL @tmp_b
  20.  @tmp_a  TYPEDEF PROTO argl
  21.  @tmp_b  TYPEDEF PTR @tmp_a
  22.  name    @tmp_b      ?
  23.  ENDM
  24.  
  25.  ; Макрос, который получает указатель на интерфейс и возвращает указатель
  26.  ; на реализацию в eax
  27.  GETOBJECTPOINTER MACRO Object, Interface, pif
  28.      mov     eax, pif
  29.      IF (Object.Interface)
  30.          sub     eax, Object.Interface
  31.      ENDIF
  32.  ENDM
  33.  
  34.  ;--------------------------------------------------------------------------
  35.  
  36.  IInterface@QueryInterface   proto :DWORD, :DWORD, :DWORD
  37.  IInterface@AddRef           proto :DWORD
  38.  IInterface@Release          proto :DWORD
  39.  IInterface@Get              proto :DWORD
  40.  IInterface@Set              proto :DWORD, :DWORD
  41.  
  42.  CreateObject                proto :DWORD
  43.  IsEqualGUID                 proto :DWORD, :DWORD
  44.  
  45.  externdef                   IID_IUnknown:GUID
  46.  
  47.  ;--------------------------------------------------------------------------
  48.  
  49.  ; объявляем прототип интерфейса
  50.  IInterface struct
  51.      lpVtbl  dd  ?
  52.  IInterface ends
  53.  
  54.  IInterfaceVtbl struct
  55.      ; методы IUnknown
  56.      STDMETHOD       QueryInterface, pif:DWORD, riid:DWORD, ppv:DWORD
  57.      STDMETHOD       AddRef, pif:DWORD
  58.      STDMETHOD       Release, pif:DWORD
  59.      ; методы IInterface
  60.      STDMETHOD       GetValue, pif:DWORD
  61.      STDMETHOD       SetValue, pif:DWORD, val:DWORD
  62.  IInterfaceVtbl ends
  63.  
  64.  ; объявляем структуру объекта
  65.  Object struct
  66.      ; интерфейс объекта
  67.      interface   IInterface  <?>
  68.  
  69.      ; данные объекта
  70.      nRefCount   dd          ?
  71.      nValue      dd          ?
  72.  Object ends
  73.  
  74.  ;--------------------------------------------------------------------------
  75.  
  76.  .data
  77.  ; define the vtable
  78.  ; определяем vtable
  79.  @@IInterface segment dword
  80.  vtblIInterface:
  81.      dd      offset IInterface@QueryInterface
  82.      dd      offset IInterface@AddRef
  83.      dd      offset IInterface@Release
  84.      dd      offset IInterface@GetValue
  85.      dd      offset IInterface@SetValue
  86.  @@IInterface ends
  87.  
  88.  ; определяем IID интерфейса
  89.  ; {CF2504E0-4F89-11d3-9AC3-0000E82C0301}
  90.  IID_IInterface GUID <0cf2504e0h, 04f89h, 011d3h, <09ah, 0c3h, 00h, 00h, \
  91.                       0e8h, 02ch, 03h, 01h>>
  92.  
  93.  ;--------------------------------------------------------------------------
  94.  
  95.  .code
  96.  start:
  97.  StartProc proc
  98.      LOCAL   pif:DWORD       ; указатель на интерфейс
  99.  
  100.      ; вызываем метод SetValue
  101.      mov     eax, [pif]
  102.      mov     eax, [eax]
  103.      invoke  (IInterfaceVtbl ptr [eax]).SetValue, [pif], 12345h
  104.  
  105.      ; вызываем метод GetValue
  106.      mov     eax, [pif]
  107.      mov     eax, [eax]
  108.      invoke  (IInterfaceVtbl ptr [eax]).GetValue, [pif]
  109.  
  110.      ; освобождаем объект
  111.      mov     eax, [pif]
  112.      mov     eax, [eax]
  113.      invoke  (IInterfaceVtbl ptr [eax]).Release, [pif]
  114.  
  115.      ret
  116.  StartProc endp
  117.  
  118.  ;--------------------------------------------------------------------------
  119.  
  120.  IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD
  121.      invoke  IsEqualGUID, [riid], addr IID_IInterface
  122.      test    eax,eax
  123.      jnz     @F
  124.      invoke  IsEqualGUID, [riid], addr IID_IUnknown
  125.      test    eax,eax
  126.      jnz     @F
  127.      jmp     @Error
  128.  
  129.  @@:
  130.      GETOBJECTPOINTER    Object, interface, pif
  131.      lea     eax, (Object ptr [eax]).interface
  132.  
  133.      ; устанавливаем значение *ppv
  134.      mov     ebx, [ppv]
  135.      mov     dword ptr [ebx], eax
  136.  
  137.      ; увеличиваем значение счетчика ссылок
  138.      GETOBJECTPOINTER    Object, interface, pif
  139.      mov     eax, (Object ptr [eax]).interface
  140.      invoke  (IInterfaceVtbl ptr [eax]).AddRef, [pif]
  141.  
  142.      ; возвpащаем S_OK
  143.      mov     eax, S_OK
  144.      jmp     return
  145.  
  146.  @Error:
  147.      ; ошибка, интерфейс не поддерживается
  148.      mov     eax, [ppv]
  149.      mov     dword ptr [eax], 0
  150.      mov     eax, E_NOINTERFACE
  151.  
  152.  return:
  153.      ret
  154.  IInterface@QueryInterface endp
  155.  
  156.  
  157.  IInterface@AddRef proc pif:DWORD
  158.      GETOBJECTPOINTER    Object, interface, pif
  159.      inc     [(Object ptr [eax]).nRefCount]
  160.      mov     eax, [(Object ptr [eax]).nRefCount]
  161.      ret
  162.  IInterface@AddRef endp
  163.  
  164.  
  165.  IInterface@Release proc pif:DWORD
  166.      GETOBJECTPOINTER    Object, interface, pif
  167.      dec     [(Object ptr [eax]).nRefCount]
  168.      mov     eax, [(Object ptr [eax]).nRefCount]
  169.      or      eax, eax
  170.      jnz     @1
  171.      ; free object
  172.      mov     eax, [pif]
  173.      mov     eax, [eax]
  174.      invoke  LocalFree, eax
  175.  @1:
  176.      ret
  177.  IInterface@Release endp
  178.  
  179.  
  180.  IInterface@GetValue proc pif:DWORD
  181.      GETOBJECTPOINTER    Object, interface, pif
  182.      mov     eax, (Object ptr [eax]).nValue
  183.      ret
  184.  IInterface@GetValue endp
  185.  
  186.  
  187.  IInterface@SetValue proc uses ebx pif:DWORD, val:DWORD
  188.      GETOBJECTPOINTER    Object, interface, pif
  189.      mov     ebx, eax
  190.      mov     eax, [val]
  191.      mov     (Object ptr [ebx]).nValue, eax
  192.      ret
  193.  IInterface@SetValue endp
  194.  
  195.  ;--------------------------------------------------------------------------
  196.  
  197.  CreateObject proc uses ebx ecx pobj:DWORD
  198.      ; set *ppv to 0
  199.      mov     eax, pobj
  200.      mov     dword ptr [eax], 0
  201.  
  202.      ; pезеpвиpуем объект
  203.      invoke  LocalAlloc, LMEM_FIXED, sizeof Object
  204.      or      eax, eax
  205.      jnz     @1
  206.      ; alloc failed, so return
  207.      mov     eax, E_OUTOFMEMORY
  208.      jmp     return
  209.  @1:
  210.  
  211.      mov     ebx, eax
  212.      mov     (Object ptr [ebx]).interface.lpVtbl, offset vtblIInterface
  213.      mov     (Object ptr [ebx]).nRefCount, 0
  214.      mov     (Object ptr [ebx]).nValue, 0
  215.  
  216.      ; Запpашиваем интеpфейс
  217.      lea     ecx, (Object ptr [ebx]).interface
  218.      mov     eax, (Object ptr [ebx]).interface.lpVtbl
  219.      invoke  (IInterfaceVtbl ptr [eax]).QueryInterface,
  220.              ecx,
  221.              addr IID_IInterface,
  222.              [pobj]
  223.      cmp     eax, S_OK
  224.      je      return
  225.  
  226.      ; ошибка в QueryInterface, поэтому освобождаем память
  227.      push    eax
  228.      invoke  LocalFree, ebx
  229.      pop     eax
  230.  
  231.  return:
  232.      ret
  233.  CreateObject endp
  234.  
  235.  ;--------------------------------------------------------------------------
  236.  
  237.  IsEqualGUID proc rguid1:DWORD, rguid2:DWORD
  238.      cld
  239.      mov     esi, [rguid1]
  240.      mov     edi, [rguid2]
  241.      mov     ecx, sizeof GUID / 4
  242.      repe    cmpsd
  243.      xor     eax, eax
  244.      or      ecx, ecx
  245.      setz    eax
  246.      ret
  247.  IsEqualGUID endp
  248.  
  249.  end start

Заключение

Мы увидели (надеюсь), как реализовать COM-объект. Мы можем видеть, что это связанно с определенными трудностями и увеличивает избыточность кода нашей программы. Тем не менее, это может добавить гибкость и силу в наши программы. Подробности по данной теме вы можете найти на моей маленькой странице, посвященной COM: http://lordlucifer.cjb.net.

Помните, что COM определяет только интерфейсы, а реализацию оставляет на программиста. Эта статья показывает только одну возможную реализацию. Это не единственный метод и не самый лучший. Читатель не должен бояться экспериментировать с другими методами. © Bill T., пер. Aquila


0 1.427
archive

archive
New Member

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