На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное DigiMania RSS
msm.ru
! Перед отправкой сообщения внимательно прочтите правила раздела!!!
1. Все статьи должны быть оформлены согласно Правил оформления статей.
2. Любые обсуждения должны происходить в специальной теме, обсуждение в любых других темах раздела запрещены.
3. Запрещается писать статьи о создании и распространении вирусов, троянов и других вредоносных программ!
4. За грамотно написанные и правильно оформленные статьи авторы награждаются DigiMoney.

Дополнительные ссылки:
Желаю творческих успехов! ;)
Модераторы: Jin X
  
    > Регулярные выражения, TASM,Windows,Library
      Регулярные выражения


      Введение.

      Регулярные выражения - это один из способов поиска подстрок (соответствий) в строках. Поиск осуществляется по заданному шаблону, в соответствии с определенными правилами. Самостоятельное создание подобного парсера занятие довольно трудоемкое и долгое, а потому лучше использовать уже существующие. Но, когда мне потребовалось генерировать HTML-файлы, на основе существующих текстовых файлов, неожиданно столкнулся с проблемой, а именно с отсутствием выбора. То есть выбор был, но довольно скудный, который я остановил на VBScript Regular Expression, поставляемый в составе IE, в файле vbscript.dll, начиная с Win9x. Сразу замечу, что существуют две версии Regular Expression. В Win9x, по умолчанию, используется версия 1.0, достаточно функциональная, но не полная. Следующая версия интерфейса (5.5, а не 2.0, как мог кто-то подумать) вышла вместе с IE 5.5 и обладает уже приемлемым функционалом, пригодным для любых задач. Я рассмотрю обе, указывая их отличия по мере необходимости.
      Заранее прошу прощения за возможные неточности в терминологии, но я не силен в теории, тем более ООП :D

      Инструменты.

      VBScript Regular Expression написан с использованием технологии COM, поэтому выбор ассемблера напрашивается сам собой - Turbo Assembler, имеющий удобную поддержку ООП. Очень сильно помогла утилита OLEVIEW, из состава Visual Studio, благодаря которой я смог узнать структуру всех используемых интерфейсов и параметры методов. Thanks и создателю OllyDbg, за прекрасный инструмент, без которого данная статья навряд ли бы появилась.

      Инициализация.

      Для начала любой работы с COM-объектами необходимо инициализировать COM в текущем потоке, создать объект класса и запросить ссылку на экземпляр нужного интерфейса. После работы необходимо уничтожить созданные объекты и деинсталлировать COM в текущем потоке.
      Как известно, для создания экземпляра класса, а так же для получения ссылок на любые интерфейсы необходимо знать их уникальные идентификаторы - GUID, вот и начнем с их объявления. Идентификатор класса можно найти в реестре, в ветке \HKEY_CLASSES_ROOT\CLSID\. Идентификаторы интерфейсов легко определить с помощью OLEVIEW. Таким образом, имеем все необходимое для их объявления:
      ExpandedWrap disabled
        ; This macro is from Morten Elling's TaCOM examples, thanks to the author.
        macro GUID gName: req, gString:req
          local @@byte
            label gName byte
            irp t, <8,6,4,2, 13,11, 18,16, 21,23, 26,28,30,32,34,36>
                @@byte SUBSTR <gString>, t, 2
                @@byte CATSTR <0>, @@byte, <h>
                db @@byte
            endm
        endm
         
        dataseg
          ; ID класса
          GUID CLSID_IRegExp {3F4DACA4-160D-11D2-A8E9-00104B365C9F}
          ; ID интерфейсов
          GUID IID_IRegExp   {3F4DACA0-160D-11D2-A8E9-00104B365C9F}     ; v 1.0
          GUID IID_IRegExp2  {3F4DACB0-160D-11D2-A8E9-00104B365C9F}     ; v 5.5
      Макрос GUID создает метку gName и парсит gString, переводя строку в массив байт, который и является уникальным идентификатором. Ничего сложного.
      Теперь можно смело переходить к инициализации COM и получению интерфейсов. Оформим это в виде подпрограмм, чтобы больше не возвращаться к этому:
      ExpandedWrap disabled
        dataseg
          COMInstalled  dd 0
         
        codeseg
         
        ; запрашивает интерфейс @@IID класса @@CLSID
        ; на входе:
        ;    @@CLSID    - GUID запрашиваемого класса
        ;    @@IID      - GUID запрашиваемого интерфейса
        ; на выходе:
        ;    eax        - ссылка на интерфейс, или NULL в случае ошибки
        proc CreateObject uses edx, @@CLSID, @@IID
                cmp     [COMInstalled], 0
                jne     @@create
                call    CoInitialize, 0 ; инициализируем COM
            @@create:
                inc     [COMInstalled]
                ; создаем объект класса и запрашиваем интерфейс
                push    eax             ; выделяем временный буфер, под указатель на запрашиваемый интерфейс
                call    CoCreateInstance, [@@CLSID], 0, 5, [@@IID], esp
                pop     edx             ; edx = LPVOID * ppv
                or      eax, eax
                jns     @@done
                ; ошибка, уничтожаем COM
                call    DestroyObject, 0
                xor     edx, edx
            @@done:
                xchg    eax, edx        ; возвращаем результат в EAX
                ret
        endp
         
        ; уничтожает объект и, в случае необходимости, деинсталлирует COM в текущем потоке
        ; на входе:
        ;    @@lpObj    - ссылка на экземпляр объекта
        ;    если уничтожили последний объект, то автоматом вызовится CoUninitialize
        proc DestroyObject uses edx, @@lpObj
                cmp     [COMInstalled], 0
                je      @@done
                mov     edx, [@@lpObj]
                or      edx, edx
                jz      @@isuninst
                call    IUnknown edx METHOD IUnknown:Release USES ds:eax, edx
            @@isuninst:
                dec     [COMInstalled]
                jnz     @@done
                call    CoUninitialize
            @@done:
                ret
        endp
      Пояснять здесь нечего, все ясно из комментариев, обычный код инициализации, получения интерфейса и завершения работы с COM, пригодный для работы с любыми COM-объектами. Тонкость только в одном, как только программа удаляет все ссылки на интерфейсы, то автоматически происходит деинициализация COM в текущем потоке. Это сделано только из соображений удобства, но можно сделать и без "автоматики".

      Строки.

      Поскольку регулярные выражения применяются в основном для обработки строк, то сразу определимся с их форматом, для лучшего понимания последующих примерчиков. Дело в том, что VBScript, как не трудно догадаться из названия, использует тот же тип строк, что и Visual Basic, так называемый BSTR. Это обычные Unicode-строки, со счетчиком длины, который имеет размер в двойное слово. Но, в отличие от привычных Паскаль-строк, указатель на строку указывает на первый символ строки, а не на счетчик длины! Таким образом, их можно использовать и как строки с завершающим нулем. Но нам удобнее работать в ANSI-режиме, преимущества которого очевидны во многих случаях, особенно при интенсивной работе с файлами и консолью, поэтому создадим парочку подпрограмм, для конвертирования ANSI<-->BSTR:
      ExpandedWrap disabled
        ; конвертирует ANSI строку в BSTR(unicode)
        ; возвращает указатель на сформированную строку
        proc ANSItoBSTR uses ebx, @@ansistr
          local @@lena, @@lenw
                call    lstrlenA, [@@ansistr]
                mov     [@@lena], eax
                inc     eax                     ; резервируем место для завершающего нуля
                shl     eax, 1                  ; eax = eax*2
                mov     [@@lenw], eax
                call    GlobalAlloc, 0, eax
                or      eax, eax
                jz      @@done
                mov     ebx, eax
                call    MultiByteToWideChar, 0, 0, [@@ansistr], [@@lena], eax, [@@lenw]
                call    SysAllocStringLen, ebx, eax
                push    eax
                call    GlobalFree, ebx
                pop     eax
            @@done:
                ret
        endp
         
        ; конвертирует BSTR строку в ANSI
        ; возвращает указатель на сформированную строку
        proc BSTRtoANSI uses edx, @@lpBStr
          local @@len
                call    lstrlenW, [@@lpBStr]
                inc     eax                     ; резервируем место для завершающего нуля
                mov     [@@len], eax
                call    GlobalAlloc, 0, eax
                or      eax, eax
                jz      @@done
                push    eax
                xor     edx, edx
                call    WideCharToMultiByte, edx, edx, [@@lpBStr], -1, eax, [@@len], edx, edx
                pop     eax
            @@done:
                ret
        endp
      Функция ANSItoBSTR принимает указатель на строку в ANSI-кодировке, выделяет память и переводит строку в Unicode, посредством MultiByteToWideChar, а уж затем переводит Unicode-строку в BSTR, возвращая адрес последней в регистре EAX. BSTRtoANSI выполняет обратную функцию, возвращая указатель на ANSI-строку. Вообще же хочу заметить, что данные функции далеко не оптимальны и годятся только для примера. В частности, перевод Unicode-строки в BSTR лучше осуществлять самим, без использования SysAllocStringLen, так же определение длины BSTR-строки лучше делать чтением счетчика длины, а не как у меня через lstrlenW. Просто для большей наглядности я оставил использование WinAPI :)

      Интерфейсы.

      Regular Expression предоставляет четыре интерфейса. Вся работа по поиску и замене сосредоточена в одном, а остальные три являются вспомогательными:
      IRegExp - основной интерфейс, реализующий все функции поиска, замены и выборки соответствий по заданному шаблону.
      IMatchCollection - вспомогательный интерфейс, содержит коллекцию найденных соответствий, хранимых в виде массива объектов IMatch.
      IMatch - вспомогательный интерфейс, используется для хранения одного найденного соответствия.
      ISubMatches - вспомогательный интерфейс, появился в версии 5.5, содержит коллекцию субсоответствий, то есть как бы соответствие в соответствии.
      Все эти интерфейсы являются потомками интерфейса IDispatch, который в свою очередь является потомком IUnknown, базового интерфейса всех объектов, согласно COM-технологии. Так что вполне логично начать именно с их объявления:
      ExpandedWrap disabled
        struc IUnknown METHOD {
          virtual QueryInterface   ; ([in] GUID* riid, [out] void** ppvObj)
          virtual AddRef           ; ()
          virtual Release          ; ()
          }
        ends
         
        struc IDispatch IUnknown METHOD {
          virtual GetTypeInfoCount ; ([out] unsigned int* pctinfo)
          virtual GetTypeInfo      ; [in] unsigned int itinfo, [in] unsigned long lcid, [out] void** pptinfo)
          virtual GetIDsOfNames    ;
          virtual Invoke           ;
          }
        ends
      Для тех, кто не знаком с Turbo Assembler, поясню. Ключевое слово METHOD показывает, что мы используем расширенную форму директивы STRUC для описания объекта. Далее, в фигурных скобках, следуют описания методов объекта. Ключевое слово VIRTUAL указывает, что вызов метода следует производить косвенно, через таблицу VMT(Virtual Method Table). Далее следует имя метода. То есть в данном случае будет просто объявлена таблица VMT, каждому методу будет присвоено смещение в этой таблице, но экземпляр таблицы создаваться не будет, что собственно и требовалось.
      Затем объявляем объект IDispatch, но уже в качестве потомка IUnknown, что автоматом добавит в его таблицу VMT все методы IUnknown. Еще отмечу, что параметры методов указаны в комментариях, в том виде, в котором их выдает OLEVIEW, что довольно удобно, но не дает полной картины правильной передачи параметров в методы. В квадратных скобках находится уточнение о типе параметра - входящие(in) или возвращаемые(out) значения.
      Более подробно описывать эти интерфейсы нет смысла, первый описан в любом учебнике по COM-технологии, а второй напрямую не относится к теме и используется для вызова методов по их именам, что нам не пригодится.

      Интерфейс IRegExp.

      Это основной интерфейс, предоставляющий базовые методы для работы с регулярными выражениями. Всего интерфейс предоставляет три (для версии 5.5 - четыре) свойства и три метода.
      Свойства:
      Pattern - строка с регулярным выражением, являющаяся шаблоном для поиска подстрок. Обязательно должна быть задана до первого использования любых методов объекта.
      IgnoreCase - булевое значение. Если задано TRUE(-1), то при поиске соответствий игнорируется регистр символов. По умолчанию имеет значение FALSE, как и остальные свойства.
      Global - булевое значение. Если TRUE, то будут искаться все возможные соответствия в строке. При значении FALSE поиск остановится после первого найденного соответствия.
      Multiline - булевое значение, появилось в версии 5.5. Значение TRUE соответствует многострочному поиску, т.е. учитываются переносы строк. В противном случае исходная строка рассматривается просто как набор символов. В принципе можно обойтись и без этого свойства, указывая начало и конец строк в шаблоне поиска, но все же отказываться от него не стоит, так проще.
      Методы:
      Test - ищет в строке соответствия заданному шаблону поиска, в случае успеха возвращает TRUE.
      Execute - то же, что и предыдущий метод, но с одним существенным отличием - все найденные соответствия будут помещены в коллекцию IMatchCollection.
      Replace - ищет в строке соответствия заданному шаблону и заменяет их на строку, заданную в параметрах. При замене возможна примитивная обработка результатов поиска.
      Учитывая тот факт, что свойства являются ни чем иным как простыми методами, для чтения/записи некоторых внутренних данных объекта, структура интерфейса будет выглядеть следующим образом:
      ExpandedWrap disabled
        ; version 1.0
        struc IRegExp IDispatch METHOD {
          virtual GetPattern       ; [out, retval] BSTR* pPattern
          virtual SetPattern       ; [in] BSTR pPattern
          virtual GetIgnoreCase    ; [out, retval] VARIANT_BOOL* pIgnoreCase
          virtual SetIgnoreCase    ; [in] VARIANT_BOOL pIgnoreCase
          virtual GetGlobal        ; [out, retval] VARIANT_BOOL* pGlobal
          virtual SetGlobal        ; [in] VARIANT_BOOL pGlobal
          virtual Execute          ; [in] BSTR sourceString, [out, retval] IDispatch** ppMatches
          virtual Tests            ; [in] BSTR sourceString, [out, retval] VARIANT_BOOL* pMatch
          virtual Replace          ; [in] BSTR sourceString, [in] VARIANT replaceVar, [out, retval] BSTR* pDestString
          }
        ends
         
        ; version 5.5
        struc IRegExp2 IDispatch METHOD {
          virtual GetPattern       ; [out, retval] BSTR* pPattern
          virtual SetPattern       ; [in] BSTR pPattern
          virtual GetIgnoreCase    ; [out, retval] VARIANT_BOOL* pIgnoreCase
          virtual SetIgnoreCase    ; [in] VARIANT_BOOL pIgnoreCase
          virtual GetGlobal        ; [out, retval] VARIANT_BOOL* pGlobal
          virtual SetGlobal        ; [in] VARIANT_BOOL pGlobal
          virtual GetMultiline     ; [out, retval] VARIANT_BOOL* pMultiline
          virtual SetMultiline     ; [in] VARIANT_BOOL pMultiline
          virtual Execute          ; [in] BSTR sourceString, [out, retval] IDispatch** ppMatches
          virtual Tests            ; [in] BSTR sourceString, [out, retval] VARIANT_BOOL* pMatch
          virtual Replace          ; [in] BSTR sourceString, [in] VARIANT replaceVar, [out, retval] BSTR* pDestString
          }
        ends
      Для всех методов, как того и требует стандарт СОМ, справедливо соглашение о возвращаемом в регистре EAX значении. Если вернуло NULL, то функция выполнилась успешно, в противном случае нужно анализировать отдельные биты, для установления причин ошибки, в самом простом случае достаточно проанализировать старший бит.

      Свойства интерфейса IRegExp.

      Кто внимательно читал статью, тот помнит, что свойства интерфейса влияют на условия поиска соответствий. Изменять их значения можно с помощью методов SetGlobal, SetIgnoreCase и SetMultiline, которые принимают один параметр типа BOOL. Вот и первый сюрприз Вижен Бейсика - булевые типы имеют размер в слово, но при использовании их в качестве параметров на стэке, используются разумеется двойные слова! Вызовы методов выглядят следующим образом:
      ExpandedWrap disabled
                call    IRegExp2 ebx METHOD SetGlobal USES ds:eax, ebx, -1
                call    IRegExp2 ebx METHOD SetIgnoreCase USES ds:eax, ebx, -1
                call    IRegExp2 ebx METHOD SetMultiline USES ds:eax, ebx, -1
      Тем, кто не знаком с Turbo Assembler, этот код покажется бредом, поэтому поясню. Поддержка ООП в Turbo Assembler выражена не только созданием/наследованием структур и объявлением методов, но и вызовом этих методов. Причем, в зависимости от объявления, метод может вызываться как статичный, так и как виртуальный. Дело в том, что еще при объявлении объектов, помимо структуры данных объекта, создается таблица методов объекта, в которой запоминается вся необходимая информация о методе, позволяющая компилятору корректно вызывать этот метод. Синтаксис расширенной формы CALL достаточно гибок, поэтому приведу лишь небольшую упрошенную модель вызова методов объектов, достаточную для понимая дальнейших примеров:
      CALL <class> <instance> METHOD [class:]<method> [USES segreg:reg] [mode][, parameters]
      где:
      class - имя объекта.
      instance - указатель на экземпляр объекта.
      method - разумеется имя вызываемого метода.
      USES - определяет временный регистр, используемый для вызова виртуальных методов.
      mode - соглашение о модели вызова подпрограммы (C, Pascal, StdCall и т.д.), позволяющее переопределить текущую модель (заданную директивой MODEL) для данного вызова.
      parameters - параметры, разделенные либо запятой, либо пробелами, пишущиеся, как и в языках высокого уровня, слева направо, но ложиться в стэк они будут в порядке, определенным директивой MODEL, либо в порядке заданным mode.
      Думаю тут все ясно, осталось только уточнить назначение ключевого слова USES. Это слово сам Том Сван, в своей замечательной книге "Освоение Turbo Assembler" истолковал неверно, сделав совершенно ошибочные выводы, оно и понятно, документация на tasm была крайне кривой :) Для его правильного толкования рассмотрим вызов виртуального метода:
      ExpandedWrap disabled
        ; ebx - указатель на экземпляр объекта
                mov      eax, [ebx]     ; eax - адрес таблицы виртуальных методов (VMT)
                push     0              ; параметры
                push     ebx            ; push Self/This
                call     [eax+смещение_метода_в_VMT]
      Как видно вызов происходит в три этапа, на первом мы получаем адрес VMT, на втором заносим в стэк параметры, а на заключительном вызываем нужный метод, беря его адрес из таблицы VMT. Очевидно, что для данного вызова необходимо два регистра - на указатель экземпляра объекта и временный, под указатель на VMT. Указатель на экземпляр объекта мы указываем явно, в параметрах CALL...METHOD, а как компилятору узнать, какой регистр можно использовать как временный, чтобы не испортить его содержимое? Правильно, его мы указываем после ключевого слова USES, причем в паре с сегментным регистром. Если же эту директиву пропустить, то в качестве временного будет использоваться пара ES:EBX, что довольно не удобно.
      Таким образом, вызов:
      ExpandedWrap disabled
                call    IRegExp2 ebx METHOD SetGlobal USES ds:edx, ebx, -1
      будет развернут компилятором в следующую последовательность:
      ExpandedWrap disabled
                mov      edx, [ebx]     ; edx = addr VMT
                push     -1
                push     ebx            ; push Self|This
                call     [ds:edx+30h]   ; call [VMT+30h]
      Где 30h - это смещение данного метода в таблице VMT объекта. Тут нужно обратить внимание на то, что первым параметром любых методов всегда указывается указатель на экземпляр объекта, в данном случае регистр EBX. Его приходится передавать через стек, поскольку это недостаток кривой реализации ООП в языках высокого уровня, где этот параметр "не видим" и передается через стэк. Это так называемые Self в Паскале/Дельфи и this в C++.
      Свойства можно не только задавать, но и получить их состояние вызовом методов GetGlobal, GetIgnoreCase и GetMultiline, которым передается указатель на переменную типа BOOL. Например, получить состояния свойства Global можно так:
      ExpandedWrap disabled
                push    eax             ; выделяем временный буфер в стеке
                call    IRegExp2 ebx METHOD GetGlobal USES ds:eax, ebx, esp
                pop     edx             ; dx - полученное значение свойства объекта (word)
      здесь переменная создается в стэке, что иногда предпочтительнее, нежели ее хранение в сегменте данных. Только не стоит забывать, что значение BOOL, имеет размер в слово, что необходимо учитывать при обработке полученного результата!
      Осталось рассмотреть еще одно свойство - Pattern. Это свойство имеет тип BSTR и предназначено для хранения шаблона поиска. Шаблон задается через метод SetPattern, имеющий один параметр - указатель на BSTR-строку шаблона. Получить указатель на текущий шаблон можно с помощью метода GetPattern, которому передается указатель на двойное слово, куда и будет записан адрес BSTR-строки шаблона. Например:
      ExpandedWrap disabled
        dataseg
          szPattern      db '^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$', 0
         
        codeseg
        ;       .....
                 ; задаем шаблон поиска соответствий
                call    ANSItoBSTR, offset szPattern    ; готовим BSTR-строку шаблона поиска соответствий
                call    IRegExp2 ebx METHOD SetPattern USES ds:edx, ebx, eax
         
        ;       .....
         
                 ; получаем текущий шаблон поиска соответствий
                push    eax             ; выделяем временный буфер под указатель на строку шаблона
                call    IRegExp2 ebx METHOD GetPattern USES ds:eax, ebx, esp
                pop     eax             ; eax - адрес BSTR-строки шаблона
      Думаю из комментариев все ясно. Причем, если посмотреть под отладчиком, то увидим, что GetPattern возвращает адрес строки, который мы задали свойству через SetPattern, что говорит о том, что память, отведенную под строку шаблона, нельзя освобождать до тех пор, пока эта строка используется в качестве шаблона.

      Метод Test.

      Для тестирования строки на соответствие шаблону предназначен метод Test. Он просто ищет соответствие по заданному шаблону и возвращает булевое значение TRUE, если соответствие найдено, или FALSE, если таковых не имеется. Метод принимает два параметра, указатель на строку, в которой ищется соответствие и указатель на переменную типа BOOL, в которую будет записан результат поиска. Шаблон поиска уже должен находиться в свойстве Pattern. В качестве примера можно рассмотреть тестирование строки на предмет соответствия ее содержимого вещественному или целому числу:
      ExpandedWrap disabled
        ; file: test.asm
                ideal
                p586
                model flat, stdcall
                locals @@
         
                %NOINCL
         
        include 'kernel32.inc'
        include 'regexpr.inc'
        include 'ole32.inc'
        include 'oleaut32.inc'
         
        includelib 'imp32i.lib'
         
         
        dataseg
          ; ID класса и интерфейсов
          GUID CLSID_IRegExp {3F4DACA4-160D-11D2-A8E9-00104B365C9F}
          GUID IID_IRegExp   {3F4DACA0-160D-11D2-A8E9-00104B365C9F}     ; v 1.0
          GUID IID_IRegExp2  {3F4DACB0-160D-11D2-A8E9-00104B365C9F}     ; v.5.5
         
          ; тестовые строки, содержащие "числа"
          szTest1       db '+12E-2', 0
          szTest2       db '02+E-a', 0
         
          ; шаблон для поиска
          szPatNum      db '^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$', 0
         
        codeseg
            Begin:  ; program entry point ...
         
                ; инициализируем COM и получаем экземпляр объекта RegExp2
                call    CreateObject, lpCLSID_IRegExp, lpIID_IRegExp2
                _error  nz, "Sorry, this programm request VBScript Regular Expression ver 5.5 or later!"
                mov     ebx, eax
                ; настраиваем свойства объекта
                call    IRegExp2 ebx METHOD SetGlobal USES ds:eax, ebx, -1
                call    IRegExp2 ebx METHOD SetIgnoreCase USES ds:eax, ebx, -1
                call    IRegExp2 ebx METHOD SetMultiline USES ds:eax, ebx, -1
         
                 ; задаем шаблон поиска
                call    ANSItoBSTR, offset szPatNum
                call    IRegExp2 ebx METHOD SetPattern USES ds:edx, ebx, eax
         
                 ; подготавливаем строку для поиска
                call    ANSItoBSTR, offset szTest1
                push    eax             ; сохраняем указатель на BSTR, для SysFreeString
                ; проверяем строку на соответствие шаблону
                push    eax             ; выделяем временный буфер для результата поиска
                call    IRegExp2 ebx METHOD Test USES ds:edx, ebx, eax, esp
                pop     eax             ; ax - булевое значение, результат поиска соответствия
                movsx   eax, ax         ; расширяем слово до двойного слова со знаком
                _printf <"Source string: %s",13,10,"Result: %i",13,10>, offset szTest1, eax
                call    SysFreeString   ; удаляем BSTR строку
         
                ; подготавливаем строку для поиска
                call    ANSItoBSTR, offset szTest2
                push    eax             ; сохраняем указатель на BSTR, для SysFreeString
                ; проверяем строку на соответствие шаблону
                push    eax             ; выделяем временный буфер для результата поиска
                call    IRegExp2 ebx METHOD Test USES ds:edx, ebx, eax, esp
                pop     eax             ; ax - булевое значение, результат поиска соответствия
                movsx   eax, ax         ; расширяем слово до двойного слова со знаком
                _printf <"Source string: %s",13,10,"Result: %i",13,10>, offset szTest2, eax
                call    SysFreeString   ; удаляем BSTR строку
         
                ; уничтожаем строку шаблона
                push    eax             ; выделяем временный буфер под указатель на строку шаблона
                call    IRegExp2 ebx METHOD GetPattern USES ds:eax, ebx, esp
                call    SysFreeString   ; освобождаем память
         
                ; уничтожаем объект и завершаем COM в текущем потоке
                call    DestroyObject, ebx
                ; ждем нажатия любой клавиши и завершаем программу
                call    _getch
                call    ExitProcess, 0
        end Begin
      Как видно тестируются две строки по одному шаблону. Сам шаблон подходит под определение, как целых, так и вещественных чисел. И если запустить этот пример, то увидим следующее:
      ExpandedWrap disabled
        Source string: +12E-2
        Result: -1
        Source string: 02+E-a
        Result: 0
      Все правильно, последнее число написано с ошибкой.

      Метод Replace.

      Теперь можно попробовать и замену. Если требуемая замена несложная, то лучше всего использовать метод Replace, специально для этого и предназначенный. Он принимает три параметра: первым является указатель на BSTR-строку, в которой и будет производиться поиск соответствий шаблону, вторым параметром является переменная типа VARIANT, в которой описываются условия замены, ну а третьим параметром будет указатель на область памяти, куда метод вернет указатель на результатирующую BSTR-строку. Шаблон поиска соответствий указывается в свойстве Pattern.
      Казалось бы, все легко и просто, но во втором параметре есть засада :) Для начала смотрим определение типа VARIANT:
      ExpandedWrap disabled
        struc VARIANT
          vt            dw ?
          wReserved1    dw ?
          wReserved2    dw ?
          wReserved2    dw ?
          datas         dd ?
                        dd ?
        ends
      Поле vt определяет тип данных. Я пробовал только VT_BSTR, значением которой является 8. Поле datas содержит либо данные, либо ссылку на них. Поскольку у нас строки, то в это поле заносится ссылка на строку. Остальные поля содержат нули. А засада в том, что вопреки здравому смыслу эта структура передается не по ссылке, а непосредственно через стэк. При этом поля размером в слово "склеиваются" попарно, то есть, поскольку vt и wReserved1 имею размер в слово, то передаем их вместе, как одно двойное слово, ну и с остальными поступаем так же. Маразм конечно, но это не я придумал, это тяжелое наследство 16-ти разрядного Вижен Бейсика :D
      В качестве примера рассмотрим замену сишных объявлений констант на ассемблерные, хотя и в ограниченном варианте:
      ExpandedWrap disabled
        dataseg
          ; исходный текст
          szCode        db '#define RIGHT_ALT_PRESSED     0x0001', 13, 10
                        db '#define LEFT_ALT_PRESSED      0x0002', 13, 10
                        db '#define RIGHT_CTRL_PRESSED    0x0004', 13, 10
                        db '#define LEFT_CTRL_PRESSED     0x0008', 13, 10
                        db '#define NLS_ALPHANUMERIC      0x00000000', 13, 10
                        db '#define NLS_KATAKANA          0x00020000', 13, 10
                        db 0
         
          ; шаблон для поиска
          szPattern     db '^(#define)(\s+)(\w+\s+)(0x)([a-fA-F0-9]{1,8})', 0
          ; шаблон замены
          szReplace     db '$3equ 0$5h', 0
         
        codeseg
        ; .........
                 ; задаем шаблон поиска
                call    ANSItoBSTR, offset szPattern
                call    IRegExp2 ebx METHOD SetPattern USES ds:edx, ebx, eax
         
                 ; подготавливаем строку для поиска
                call    ANSItoBSTR, offset szCode
                mov     esi, eax
                ; подготавливаем строку для замены найденных соответствий
                call    ANSItoBSTR, offset szReplace
                push    eax                     ; сохраняем адрес для SysFreeString
         
                ; вызываем метод Replace
                push    eax                     ; выделяем место под результат
                call    IRegExp2 ebx METHOD Replace USES ds:edx, ebx, esi, 8, 0, eax, 0, esp
                call    BSTRtoANSI              ; переводим в ANSI
                push    eax                     ; сохраняем адрес для GlobalFree
                ; вводим на экран исходный текст и результат замены
                _printf <"Source:",13,10,"%s",13,10,"Dest:",13,10,"%s",13,10>, offset szCode, eax
                call    GlobalFree              ; освобождаем ANSI-строку
                call    SysFreeString           ; освобождаем BSTR-строку szReplace
                call    SysFreeString, esi      ; освобождаем BSTR-строку szCode
      Здесь хочу обратить внимание только на одну вещь, а именно передачу переменной VARIANT, которая передается как четыре двойных слова (8, 0, eax, 0). В остальном коде ничего необычного нет. Строка шаблона, благодаря наличию скобок, разбивает найденное соответствие на пять подстрок, именуемых как $1, $2, $3 и тд. Но в строке szReplace используются только две подстроки ($3 и $5), а остальные отбрасываются. Поэтому, после замены получим следующий результат:
      ExpandedWrap disabled
        Source:
        #define RIGHT_ALT_PRESSED     0x0001
        #define LEFT_ALT_PRESSED      0x0002
        #define RIGHT_CTRL_PRESSED    0x0004
        #define LEFT_CTRL_PRESSED     0x0008
        #define NLS_ALPHANUMERIC      0x00000000
        #define NLS_KATAKANA          0x00020000
         
        Dest:
        RIGHT_ALT_PRESSED     equ 00001h
        LEFT_ALT_PRESSED      equ 00002h
        RIGHT_CTRL_PRESSED    equ 00004h
        LEFT_CTRL_PRESSED     equ 00008h
        NLS_ALPHANUMERIC      equ 000000000h
        NLS_KATAKANA          equ 000020000h
      Это только простейший пример, для создания полноценного конвертера сишных хидеров нужно несколько проходов.

      Метод Execute.

      Этот метод предназначен для поиска, с анализом и последующей обработкой найденных соответствий. Методу передаются два параметра. Указатель на BSTR-строку, в которой будет производиться поиск и указатель на переменную, под возвращаемое значение, которое является ни чем иным, как ссылкой на объект IMatchCollection, в котором и хранятся все найденные соответствия. Интерфейс IMatchCollection несложен:
      ExpandedWrap disabled
        struc IMatchCollection IDispatch METHOD {
          virtual Item             ; [in] long index, [out, retval] IDispatch** ppMatch
          virtual Count            ; [out, retval] long* pCount
          virtual NewEnum          ; [out, retval] IUnknown** ppEnum
          }
        ends
      Как уже упоминалось, этот объект хранит все найденные соответствия, в виде массива объектов IMatch, по одному объекту на каждое соответствие. Метод Count возвращает общее количество найденных соответствий. Метод Item позволяет получить ссылку на нужное соответствие. Он принимает два параметра, индекс, лежащий в диапазоне от 0 до Count-1, а так же адрес переменной, куда будет записана ссылка на объект IMatch, соответствующий индексу. Метод NewEnum не исследовал. Интерфейс IMatch существует в двух вариантах, в зависимости от версии RegExp:
      ExpandedWrap disabled
        ; version 1.0
        struc IMatch IDispatch METHOD {
          virtual Value            ; [out, retval] BSTR* pValue
          virtual FirstIndex       ; [out, retval] long* pFirstIndex
          virtual Length           ; [out, retval] long* pLength
          }
        ends
         
        ; version 5.5
        struc IMatch2 IDispatch METHOD {
          virtual Value            ; [out, retval] BSTR* pValue
          virtual FirstIndex       ; [out, retval] long* pFirstIndex
          virtual Length           ; [out, retval] long* pLength
          virtual SubMatches       ; [out, retval] IDispatch** ppSubMatches
          }
        ends
      Как видно разница только в методе SubMatches. Метод Value возвращает указатель на строку найденного соответствия. Метод FirstIndex вернет позицию найденного соответствия в искомой строке. Метод Length возвратит длину найденной строки. Метод SubMatches доступен начиная с версии RegExp 5.5, и возвращает ссылку на объект ISubMatches, в котором хранятся субсоответствия, заданные с помощью круглых скобок. Для примера, допустим, есть следующий шаблон поиска: "(0x)([0-9]+)". Тогда в строке "#define CONST 0x012" будет найдено одно соответствие: "0x012", которое будет храниться в экземпляре IMatch, ссылку на который можно получить по нулевому индексу посредством метода Item, объекта IMatchCollection. В свою очередь, благодаря наличию круглых скобок, данное соответствие будет разбито на два субсоответствия: "0x" и "012", которые и сохранятся в объекте ISubMatches. Интерфейс ISubMatches выглядит следующим образом:
      ExpandedWrap disabled
        struc ISubMatches IDispatch METHOD {
          virtual Item             ; [in] long index, [out, retval] VARIANT* pSubMatch
          virtual Count            ; [out, retval] long* pCount
          virtual NewEnum          ; [out, retval] IUnknown** ppEnum
          }
        ends
      Метод Count вернет количество субсоответствий. Все субсоответствия хранятся в виде массива, доступ к элементам которого осуществляется через метод Item, которому передается индекс элемента, лежащий в диапазоне от 0 до Count-1, и указатель на переменную VARIANT, в которой и будет возвращено субсоответствие. То есть, в поле vt будет тип субсоответствия (для BSTR, vt = 8), а в поле datas содержится либо ссылка (для строк), либо непосредственные данные.
      Выглядит все это немного запутано, поэтому покажу на примере. Допустим, имеется полный путь к некоторому файлу. С помощью метода Execute очень легко разделить строку на три компоненты: диск, путь и имя файла. А уж затем можно делать с ними, что душе угодно. Итак, некие пути к файлам:
      ExpandedWrap disabled
        dataseg
          szFiles       db '"E:\Program Files\Tasm\Project\RegExpr\exec1.asm"', 13, 10
                        db '\\Flash0\GdtDump\GdtDump.exe', 13, 10
      Первая строка взята в кавычки, как это делает винда для файлов, у которых в полном пути присутствуют пробелы. Но в принципе это и не важно, шаблон поиска все равно их отбросит:
      ExpandedWrap disabled
          ; шаблон для поиска
          szPattern     db '(\b[a-z]:|\\\\[a-z0-9]+)\\([^/:*?";;;<>|\r\n]*\\)?([^\\/:*?"<>|\r\n]*)', 0
      Выглядит заумно, зато позволяет разбивать любые корректные строки.
      Теперь вызываем метод Execute:
      ExpandedWrap disabled
                ; настраиваем свойства объекта
                call    IRegExp2 ebx METHOD SetGlobal USES ds:eax, ebx, -1
                call    IRegExp2 ebx METHOD SetIgnoreCase USES ds:eax, ebx, -1
                call    IRegExp2 ebx METHOD SetMultiline USES ds:eax, ebx, -1
         
                 ; задаем шаблон поиска
                call    ANSItoBSTR, offset szPattern
                call    IRegExp2 ebx METHOD SetPattern USES ds:edx, ebx, eax
         
                 ; подготавливаем строку для поиска
                call    ANSItoBSTR, offset szFiles
                push    eax                     ; сохраняем указатель для SysFreeString
                ; вызываем метод Replace
                push    eax                     ; выделяем место под результат
                call    IRegExp2 ebx METHOD IRegExp2:Execute USES ds:edx, ebx, eax, esp
                call    ShowMatch
                call    SysFreeString           ; освобождаем BSTR-строку szFiles
         
                ; уничтожаем строку шаблона
                push    eax             ; выделяем временный буфер под указатель на строку шаблона
                call    IRegExp2 ebx METHOD GetPattern USES ds:eax, ebx, esp
                call    SysFreeString   ; освобождаем память
      Для удобства вся работа с найденными соответствиями вынесена в отдельную подпрограмму - ShowMatch. Поскольку переменную, под возвращаемое методом Execute значение, создали на вершине стэка, то она там и останется, как параметр для подпрограммы ShowMatch, которая в качестве параметра принимает ссылку на экземпляр объекта IMatchCollection, соответственно эта временная переменная будет удалена со стэка при завершении подпрограммы. Сама подпрограмма состоит из двух вложенных циклов. Внешний цикл проходится по всем объектам IMatch, то есть по всем соответствиям, а вложенный цикл проходится по всем субсоответствиям, текущего объекта IMatch:
      ExpandedWrap disabled
        proc ShowMatch uses esi edi ebx, @@lpMatchCollection
          local @@count, @@index, @@variant:dword:4
          local @@firstidx, @@length
                mov     esi, [@@lpMatchCollection]
                ; узнаем количество объектов IMatch в коллекции
                push    eax             ; выделяем место под результат
                call    IMatchCollection esi METHOD Count USES ds:edx, esi, esp
                pop     ecx             ; ecx - количество найденных совпадений
                or      ecx, ecx
                jz      @@nofound       ; <- ничего не нашли, уходим
                mov     [@@count], ecx
                mov     [@@index], 0    ; индекс текущего объекта IMatch2
         
                ; цикл по всем найденным соответствиям
            @@loop:
                ; получаем ссылку на очередной объект IMatch
                push    eax             ; выделяем место под результат
                call    IMatchCollection esi METHOD Item USES ds:eax, esi, [@@index], esp
                mov     edi, [esp]      ; edi - экземпляр объекта IMatch
                ; узнаем значения полей FirstIndex и Length
                lea     edx, [@@firstidx]
                call    IMatch edi METHOD FirstIndex USES ds:eax, edi, edx
                lea     edx, [@@length]
                call    IMatch edi METHOD Length USES ds:eax, edi, edx
                ; получаем ссылку на строку соответствия
                call    IMatch edi METHOD Value USES ds:eax, edi, esp
                call    BSTRtoANSI
                ; выводим на экран данные о найденном соответствии
                _printf <"Match[%i]: %s",13,10," First index: %i",13,10," Length: %i",13,10>, [@@index],eax,[@@fidx],[@@len]
         
                ; теперь можно получить субсоответствия, для чего получаем ссылку на ISubMatches
                ; для версии 1.0 этот код нужно закомментировать, до метки @@next!
                push    eax             ; выделяем место под результат
                call    IMatch2 edi METHOD SubMatches USES ds:eax, edi, esp
                mov     edi, [esp]      ; edi - экземпляр объекта ISubMatches
                call    ISubMatches edi METHOD Count USES ds:eax, edi, esp
                pop     ebx             ; ebx - количество элементов в ISubMatches
                ; цикл по всем субсоответствиям очередного объекта IMatch
            @@subloop:
                dec     ebx
                js      @@next
                lea     eax, [@@variant]
                call    ISubMatches edi METHOD Item USES ds:edx, edi, ebx, eax
                call    BSTRtoANSI, [dword @@variant+2*4]
                _printf <"  Sub matches [%i]: %s",13,10>, ebx, eax
                jmp     @@subloop
         
            @@next:     ; переход к следующему IMatch
                inc     [@@index]
                dec     [@@count]
                jnz     @@loop
                _printf '-------------------'
                ret
         
            @@nofound:
                _printf 'no matches found!'
                ret
        endp
      То есть данная подпрограмма просто выводит на экран все соответствия и их субсоответствия, содержащиеся в переданной ей ссылке на коллекцию IMatchCollection, в таком виде:
      ExpandedWrap disabled
        Match[0]: E:\Program Files\Tasm\Project\RegExpr\exec1.asm
         First index: 1
         Length: 47
          Sub matches [2]: exec1.asm
          Sub matches [1]: Program Files\Tasm\Project\RegExpr\
          Sub matches [0]: E:
        Match[1]: \\Flash0\GdtDump\GdtDump.exe
         First index: 51
         Length: 28
          Sub matches [2]: GdtDump.exe
          Sub matches [1]: GdtDump\
          Sub matches [0]: \\Flash0
      И еще. Не трудно заметить, что при получении ссылок на объекты IMatchCollection, IMatch и ISubMatch я не производил никаких проверок на их корректность, эксперименты показали, что эти ссылки всегда корректны. Даже в случае отсутствия соответствий всегда получим указатель на IMatchCollection, просто его метод Count вернет ноль. Если соответствия есть, но не найдено ни одного субсоответствия, то все равно получим корректный указатель на ISubMatches.

      Заключение.

      Все необходимые сорсы, инклюды и либы в аттаче. Для компиляции всех примеров достаточно запустить makeall.bat, предварительно поместив в папку компилятор tasm32 и линковщик tlink32, или аналогичные, подправив батник.
      Прикреплённый файлПрикреплённый файлRegExpr.rar (141.7 Кбайт, скачиваний: 234)
      0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
      0 пользователей:


      Рейтинг@Mail.ru
      [ Script Execution time: 0,1898 ]   [ 17 queries used ]   [ Generated: 15.12.18, 00:43 GMT ]