Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате |
Форум на Исходниках.RU > Assembler FAQ > Регулярные выражения |
Автор: AndNot 08.08.08, 19:42 |
Регулярные выражения Введение. Регулярные выражения - это один из способов поиска подстрок (соответствий) в строках. Поиск осуществляется по заданному шаблону, в соответствии с определенными правилами. Самостоятельное создание подобного парсера занятие довольно трудоемкое и долгое, а потому лучше использовать уже существующие. Но, когда мне потребовалось генерировать HTML-файлы, на основе существующих текстовых файлов, неожиданно столкнулся с проблемой, а именно с отсутствием выбора. То есть выбор был, но довольно скудный, который я остановил на VBScript Regular Expression, поставляемый в составе IE, в файле vbscript.dll, начиная с Win9x. Сразу замечу, что существуют две версии Regular Expression. В Win9x, по умолчанию, используется версия 1.0, достаточно функциональная, но не полная. Следующая версия интерфейса (5.5, а не 2.0, как мог кто-то подумать) вышла вместе с IE 5.5 и обладает уже приемлемым функционалом, пригодным для любых задач. Я рассмотрю обе, указывая их отличия по мере необходимости. Заранее прошу прощения за возможные неточности в терминологии, но я не силен в теории, тем более ООП Инструменты. VBScript Regular Expression написан с использованием технологии COM, поэтому выбор ассемблера напрашивается сам собой - Turbo Assembler, имеющий удобную поддержку ООП. Очень сильно помогла утилита OLEVIEW, из состава Visual Studio, благодаря которой я смог узнать структуру всех используемых интерфейсов и параметры методов. Thanks и создателю OllyDbg, за прекрасный инструмент, без которого данная статья навряд ли бы появилась. Инициализация. Для начала любой работы с COM-объектами необходимо инициализировать COM в текущем потоке, создать объект класса и запросить ссылку на экземпляр нужного интерфейса. После работы необходимо уничтожить созданные объекты и деинсталлировать COM в текущем потоке. Как известно, для создания экземпляра класса, а так же для получения ссылок на любые интерфейсы необходимо знать их уникальные идентификаторы - GUID, вот и начнем с их объявления. Идентификатор класса можно найти в реестре, в ветке \HKEY_CLASSES_ROOT\CLSID\. Идентификаторы интерфейсов легко определить с помощью OLEVIEW. Таким образом, имеем все необходимое для их объявления: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Макрос GUID создает метку gName и парсит gString, переводя строку в массив байт, который и является уникальным идентификатором. Ничего сложного. ; 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 Теперь можно смело переходить к инициализации COM и получению интерфейсов. Оформим это в виде подпрограмм, чтобы больше не возвращаться к этому: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Пояснять здесь нечего, все ясно из комментариев, обычный код инициализации, получения интерфейса и завершения работы с COM, пригодный для работы с любыми COM-объектами. Тонкость только в одном, как только программа удаляет все ссылки на интерфейсы, то автоматически происходит деинициализация COM в текущем потоке. Это сделано только из соображений удобства, но можно сделать и без "автоматики".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 Строки. Поскольку регулярные выражения применяются в основном для обработки строк, то сразу определимся с их форматом, для лучшего понимания последующих примерчиков. Дело в том, что VBScript, как не трудно догадаться из названия, использует тот же тип строк, что и Visual Basic, так называемый BSTR. Это обычные Unicode-строки, со счетчиком длины, который имеет размер в двойное слово. Но, в отличие от привычных Паскаль-строк, указатель на строку указывает на первый символ строки, а не на счетчик длины! Таким образом, их можно использовать и как строки с завершающим нулем. Но нам удобнее работать в ANSI-режиме, преимущества которого очевидны во многих случаях, особенно при интенсивной работе с файлами и консолью, поэтому создадим парочку подпрограмм, для конвертирования ANSI<-->BSTR: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Функция ANSItoBSTR принимает указатель на строку в ANSI-кодировке, выделяет память и переводит строку в Unicode, посредством MultiByteToWideChar, а уж затем переводит Unicode-строку в BSTR, возвращая адрес последней в регистре EAX. BSTRtoANSI выполняет обратную функцию, возвращая указатель на ANSI-строку. Вообще же хочу заметить, что данные функции далеко не оптимальны и годятся только для примера. В частности, перевод Unicode-строки в BSTR лучше осуществлять самим, без использования SysAllocStringLen, так же определение длины BSTR-строки лучше делать чтением счетчика длины, а не как у меня через lstrlenW. Просто для большей наглядности я оставил использование WinAPI ; конвертирует 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 Интерфейсы. Regular Expression предоставляет четыре интерфейса. Вся работа по поиску и замене сосредоточена в одном, а остальные три являются вспомогательными: IRegExp - основной интерфейс, реализующий все функции поиска, замены и выборки соответствий по заданному шаблону. IMatchCollection - вспомогательный интерфейс, содержит коллекцию найденных соответствий, хранимых в виде массива объектов IMatch. IMatch - вспомогательный интерфейс, используется для хранения одного найденного соответствия. ISubMatches - вспомогательный интерфейс, появился в версии 5.5, содержит коллекцию субсоответствий, то есть как бы соответствие в соответствии. Все эти интерфейсы являются потомками интерфейса IDispatch, который в свою очередь является потомком IUnknown, базового интерфейса всех объектов, согласно COM-технологии. Так что вполне логично начать именно с их объявления: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Для тех, кто не знаком с Turbo Assembler, поясню. Ключевое слово METHOD показывает, что мы используем расширенную форму директивы STRUC для описания объекта. Далее, в фигурных скобках, следуют описания методов объекта. Ключевое слово VIRTUAL указывает, что вызов метода следует производить косвенно, через таблицу VMT(Virtual Method Table). Далее следует имя метода. То есть в данном случае будет просто объявлена таблица VMT, каждому методу будет присвоено смещение в этой таблице, но экземпляр таблицы создаваться не будет, что собственно и требовалось.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 Затем объявляем объект 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 - ищет в строке соответствия заданному шаблону и заменяет их на строку, заданную в параметрах. При замене возможна примитивная обработка результатов поиска. Учитывая тот факт, что свойства являются ни чем иным как простыми методами, для чтения/записи некоторых внутренних данных объекта, структура интерфейса будет выглядеть следующим образом: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Для всех методов, как того и требует стандарт СОМ, справедливо соглашение о возвращаемом в регистре EAX значении. Если вернуло NULL, то функция выполнилась успешно, в противном случае нужно анализировать отдельные биты, для установления причин ошибки, в самом простом случае достаточно проанализировать старший бит.; 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 Свойства интерфейса IRegExp. Кто внимательно читал статью, тот помнит, что свойства интерфейса влияют на условия поиска соответствий. Изменять их значения можно с помощью методов SetGlobal, SetIgnoreCase и SetMultiline, которые принимают один параметр типа BOOL. Вот и первый сюрприз Вижен Бейсика - булевые типы имеют размер в слово, но при использовании их в качестве параметров на стэке, используются разумеется двойные слова! Вызовы методов выглядят следующим образом: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Тем, кто не знаком с Turbo Assembler, этот код покажется бредом, поэтому поясню. Поддержка ООП в Turbo Assembler выражена не только созданием/наследованием структур и объявлением методов, но и вызовом этих методов. Причем, в зависимости от объявления, метод может вызываться как статичный, так и как виртуальный. Дело в том, что еще при объявлении объектов, помимо структуры данных объекта, создается таблица методов объекта, в которой запоминается вся необходимая информация о методе, позволяющая компилятору корректно вызывать этот метод. Синтаксис расширенной формы CALL достаточно гибок, поэтому приведу лишь небольшую упрошенную модель вызова методов объектов, достаточную для понимая дальнейших примеров: 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 <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 была крайне кривой Для его правильного толкования рассмотрим вызов виртуального метода: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Как видно вызов происходит в три этапа, на первом мы получаем адрес VMT, на втором заносим в стэк параметры, а на заключительном вызываем нужный метод, беря его адрес из таблицы VMT. Очевидно, что для данного вызова необходимо два регистра - на указатель экземпляра объекта и временный, под указатель на VMT. Указатель на экземпляр объекта мы указываем явно, в параметрах CALL...METHOD, а как компилятору узнать, какой регистр можно использовать как временный, чтобы не испортить его содержимое? Правильно, его мы указываем после ключевого слова USES, причем в паре с сегментным регистром. Если же эту директиву пропустить, то в качестве временного будет использоваться пара ES:EBX, что довольно не удобно.; ebx - указатель на экземпляр объекта mov eax, [ebx] ; eax - адрес таблицы виртуальных методов (VMT) push 0 ; параметры push ebx ; push Self/This call [eax+смещение_метода_в_VMT] Таким образом, вызов: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> будет развернут компилятором в следующую последовательность: call IRegExp2 ebx METHOD SetGlobal USES ds:edx, ebx, -1 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Где 30h - это смещение данного метода в таблице VMT объекта. Тут нужно обратить внимание на то, что первым параметром любых методов всегда указывается указатель на экземпляр объекта, в данном случае регистр EBX. Его приходится передавать через стек, поскольку это недостаток кривой реализации ООП в языках высокого уровня, где этот параметр "не видим" и передается через стэк. Это так называемые Self в Паскале/Дельфи и this в C++. mov edx, [ebx] ; edx = addr VMT push -1 push ebx ; push Self|This call [ds:edx+30h] ; call [VMT+30h] Свойства можно не только задавать, но и получить их состояние вызовом методов GetGlobal, GetIgnoreCase и GetMultiline, которым передается указатель на переменную типа BOOL. Например, получить состояния свойства Global можно так: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> здесь переменная создается в стэке, что иногда предпочтительнее, нежели ее хранение в сегменте данных. Только не стоит забывать, что значение BOOL, имеет размер в слово, что необходимо учитывать при обработке полученного результата! push eax ; выделяем временный буфер в стеке call IRegExp2 ebx METHOD GetGlobal USES ds:eax, ebx, esp pop edx ; dx - полученное значение свойства объекта (word) Осталось рассмотреть еще одно свойство - Pattern. Это свойство имеет тип BSTR и предназначено для хранения шаблона поиска. Шаблон задается через метод SetPattern, имеющий один параметр - указатель на BSTR-строку шаблона. Получить указатель на текущий шаблон можно с помощью метода GetPattern, которому передается указатель на двойное слово, куда и будет записан адрес BSTR-строки шаблона. Например: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Думаю из комментариев все ясно. Причем, если посмотреть под отладчиком, то увидим, что GetPattern возвращает адрес строки, который мы задали свойству через SetPattern, что говорит о том, что память, отведенную под строку шаблона, нельзя освобождать до тех пор, пока эта строка используется в качестве шаблона.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-строки шаблона Метод Test. Для тестирования строки на соответствие шаблону предназначен метод Test. Он просто ищет соответствие по заданному шаблону и возвращает булевое значение TRUE, если соответствие найдено, или FALSE, если таковых не имеется. Метод принимает два параметра, указатель на строку, в которой ищется соответствие и указатель на переменную типа BOOL, в которую будет записан результат поиска. Шаблон поиска уже должен находиться в свойстве Pattern. В качестве примера можно рассмотреть тестирование строки на предмет соответствия ее содержимого вещественному или целому числу: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Как видно тестируются две строки по одному шаблону. Сам шаблон подходит под определение, как целых, так и вещественных чисел. И если запустить этот пример, то увидим следующее: ; 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 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Все правильно, последнее число написано с ошибкой.Source string: +12E-2 Result: -1 Source string: 02+E-a Result: 0 Метод Replace. Теперь можно попробовать и замену. Если требуемая замена несложная, то лучше всего использовать метод Replace, специально для этого и предназначенный. Он принимает три параметра: первым является указатель на BSTR-строку, в которой и будет производиться поиск соответствий шаблону, вторым параметром является переменная типа VARIANT, в которой описываются условия замены, ну а третьим параметром будет указатель на область памяти, куда метод вернет указатель на результатирующую BSTR-строку. Шаблон поиска соответствий указывается в свойстве Pattern. Казалось бы, все легко и просто, но во втором параметре есть засада Для начала смотрим определение типа VARIANT: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Поле vt определяет тип данных. Я пробовал только VT_BSTR, значением которой является 8. Поле datas содержит либо данные, либо ссылку на них. Поскольку у нас строки, то в это поле заносится ссылка на строку. Остальные поля содержат нули. А засада в том, что вопреки здравому смыслу эта структура передается не по ссылке, а непосредственно через стэк. При этом поля размером в слово "склеиваются" попарно, то есть, поскольку vt и wReserved1 имею размер в слово, то передаем их вместе, как одно двойное слово, ну и с остальными поступаем так же. Маразм конечно, но это не я придумал, это тяжелое наследство 16-ти разрядного Вижен Бейсика struc VARIANT vt dw ? wReserved1 dw ? wReserved2 dw ? wReserved2 dw ? datas dd ? dd ? ends В качестве примера рассмотрим замену сишных объявлений констант на ассемблерные, хотя и в ограниченном варианте: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Здесь хочу обратить внимание только на одну вещь, а именно передачу переменной VARIANT, которая передается как четыре двойных слова (8, 0, eax, 0). В остальном коде ничего необычного нет. Строка шаблона, благодаря наличию скобок, разбивает найденное соответствие на пять подстрок, именуемых как $1, $2, $3 и тд. Но в строке szReplace используются только две подстроки ($3 и $5), а остальные отбрасываются. Поэтому, после замены получим следующий результат: 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 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Это только простейший пример, для создания полноценного конвертера сишных хидеров нужно несколько проходов.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 несложен: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Как уже упоминалось, этот объект хранит все найденные соответствия, в виде массива объектов IMatch, по одному объекту на каждое соответствие. Метод Count возвращает общее количество найденных соответствий. Метод Item позволяет получить ссылку на нужное соответствие. Он принимает два параметра, индекс, лежащий в диапазоне от 0 до Count-1, а так же адрес переменной, куда будет записана ссылка на объект IMatch, соответствующий индексу. Метод NewEnum не исследовал. Интерфейс IMatch существует в двух вариантах, в зависимости от версии RegExp: 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 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Как видно разница только в методе SubMatches. Метод Value возвращает указатель на строку найденного соответствия. Метод FirstIndex вернет позицию найденного соответствия в искомой строке. Метод Length возвратит длину найденной строки. Метод SubMatches доступен начиная с версии RegExp 5.5, и возвращает ссылку на объект ISubMatches, в котором хранятся субсоответствия, заданные с помощью круглых скобок. Для примера, допустим, есть следующий шаблон поиска: "(0x)([0-9]+)". Тогда в строке "#define CONST 0x012" будет найдено одно соответствие: "0x012", которое будет храниться в экземпляре IMatch, ссылку на который можно получить по нулевому индексу посредством метода Item, объекта IMatchCollection. В свою очередь, благодаря наличию круглых скобок, данное соответствие будет разбито на два субсоответствия: "0x" и "012", которые и сохранятся в объекте ISubMatches. Интерфейс ISubMatches выглядит следующим образом: ; 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 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Метод Count вернет количество субсоответствий. Все субсоответствия хранятся в виде массива, доступ к элементам которого осуществляется через метод Item, которому передается индекс элемента, лежащий в диапазоне от 0 до Count-1, и указатель на переменную VARIANT, в которой и будет возвращено субсоответствие. То есть, в поле vt будет тип субсоответствия (для BSTR, vt = 8), а в поле datas содержится либо ссылка (для строк), либо непосредственные данные.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 Выглядит все это немного запутано, поэтому покажу на примере. Допустим, имеется полный путь к некоторому файлу. С помощью метода Execute очень легко разделить строку на три компоненты: диск, путь и имя файла. А уж затем можно делать с ними, что душе угодно. Итак, некие пути к файлам: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Первая строка взята в кавычки, как это делает винда для файлов, у которых в полном пути присутствуют пробелы. Но в принципе это и не важно, шаблон поиска все равно их отбросит: dataseg szFiles db '"E:\Program Files\Tasm\Project\RegExpr\exec1.asm"', 13, 10 db '\\Flash0\GdtDump\GdtDump.exe', 13, 10 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Выглядит заумно, зато позволяет разбивать любые корректные строки. ; шаблон для поиска szPattern db '(\b[a-z]:|\\\\[a-z0-9]+)\\([^/:*?";;;<>|\r\n]*\\)?([^\\/:*?"<>|\r\n]*)', 0 Теперь вызываем метод Execute: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Для удобства вся работа с найденными соответствиями вынесена в отдельную подпрограмму - ShowMatch. Поскольку переменную, под возвращаемое методом Execute значение, создали на вершине стэка, то она там и останется, как параметр для подпрограммы ShowMatch, которая в качестве параметра принимает ссылку на экземпляр объекта IMatchCollection, соответственно эта временная переменная будет удалена со стэка при завершении подпрограммы. Сама подпрограмма состоит из двух вложенных циклов. Внешний цикл проходится по всем объектам IMatch, то есть по всем соответствиям, а вложенный цикл проходится по всем субсоответствиям, текущего объекта IMatch: ; настраиваем свойства объекта 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 ; освобождаем память <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> То есть данная подпрограмма просто выводит на экран все соответствия и их субсоответствия, содержащиеся в переданной ей ссылке на коллекцию IMatchCollection, в таком виде: 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 <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> И еще. Не трудно заметить, что при получении ссылок на объекты IMatchCollection, IMatch и ISubMatch я не производил никаких проверок на их корректность, эксперименты показали, что эти ссылки всегда корректны. Даже в случае отсутствия соответствий всегда получим указатель на IMatchCollection, просто его метод Count вернет ноль. Если соответствия есть, но не найдено ни одного субсоответствия, то все равно получим корректный указатель на ISubMatches.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 Заключение. Все необходимые сорсы, инклюды и либы в аттаче. Для компиляции всех примеров достаточно запустить makeall.bat, предварительно поместив в папку компилятор tasm32 и линковщик tlink32, или аналогичные, подправив батник. |